Как можно использовать кешированные результаты Knitr для воспроизведения среды в заданном фрагменте?

tl;dr

Мой вопрос: есть ли в сеансе R какой-либо способ использовать кэшированные результаты knitr для "быстрой перемотки вперед" к доступной среде (т. е. к набору объектов) в данном блоке кода, в том же смысле, что и сам knit()?


Настраивать:

Встроенное кэширование фрагментов кода knitr — одна из его замечательных функций.

Это особенно полезно, когда некоторые фрагменты содержат трудоемкие вычисления. Если они (или чанк, от которого они зависят) не изменены, вычисления нужно выполнять только при первом knitредактировании документа: при всех последующих вызовах knit объекты, созданные чанком, будут просто загружены из кеша.

Вот минимальный пример, файл с именем "lotsOfComps.Rnw":

\documentclass{article}
\begin{document}

The calculations in this chunk take a looooong time.

<<slowChunk, cache=TRUE>>=
Sys.sleep(30)  ## Stands in for some time-consuming computation
x <- sample(1:10, size=2)
@

I wish I could `fast-forward' to this chunk, to view the cached value of 
\texttt{x}

<<interestingChunk>>=
y <- prod(x)^2
y
@

\end{document}

Время, необходимое для вязания и TeXify "lotsOfComps.Rnw":

## First time
system.time(knit2pdf("lotsOfComps.Rnw"))
##   user  system elapsed
##   0.07    0.02   31.81

## Second (and subsequent) runs
system.time(knit2pdf("lotsOfComps.Rnw"))
##   user  system elapsed
##   0.03    0.02    1.28

Мой вопрос:

Есть ли в сеансе R какой-либо способ использовать кэшированные результаты knitr для «быстрой перемотки вперед» в среду (т. е. набор объектов), доступных в данном блоке кода, в том же смысле что knit() сам делает?


Выполнение purl("lotsOfComps.Rnw"), а затем запуск кода в "lotsOfComps.R" не работает, потому что все объекты на этом пути должны быть пересчитаны.

В идеале можно было бы сделать что-то подобное, чтобы оказаться в среде, которая существует в начале <<interestingChunk>>=:

spin("lotsOfComps.Rnw", chunk="interestingChunk")
ls()
# [1] "x"
x
# [1] 3 8

Поскольку spin() (пока?) недоступен, как лучше всего получить эквивалентный результат?


person Josh O'Brien    schedule 29.03.2013    source источник
comment
Хороший вопрос. Барри Роулингсон задал мне аналогичный вопрос в прошлом году, и мое собственное решение было спрятано глубоко здесь (мне даже потребовалось некоторое время, чтобы откопать его): gist.github.com/yihui/2629886#file-knitr-checkpoint-rnw Я оставлю другим людям портировать его сюда если это полезно :)   -  person Yihui Xie    schedule 30.03.2013
comment
@Yihui - Спасибо, что нашли время, чтобы посмотреть это. Это очень полезно, и я планирую включить его в ответ на этот вопрос, как только у меня появится такая возможность.   -  person Josh O'Brien    schedule 31.03.2013
comment
@Yihui - у меня чертовски много времени, чтобы среда фрагмента checkpoint была сброшена / сохранена в глобальной среде, чтобы она была доступна после запуска knit(). trace(knit, quote(on.exit({assign("ChunkEnv", envir, envir = .GlobalEnv)}))) - самое близкое, что я получил, но, похоже, оно сохраняет среду последнего фрагмента независимо от того, какой из них я установил в качестве контрольной точки. Похоже, мне придется намного глубже вникнуть в код Knitr, прежде чем я смогу расколоть этот орех.   -  person Josh O'Brien    schedule 31.03.2013
comment
Зачем вам эта среда, ведь все ее объекты доступны в .GlobalEnv?   -  person Yihui Xie    schedule 31.03.2013
comment
@Yihui - Ну, я получаю неожиданные результаты, когда knit работаю с вашим "knitr-checkpoint.Rnw". Первый раз делаю knit("knitr-checkpoint.Rnw"); ls(), только x есть в .GlobalEnv. Все хорошо. Во второй и последующие разы я делаю knit("knitr-checkpoint.Rnw"), однако он игнорирует контрольную точку и запускает все куски. ls() затем показывает x и y в .GlobalEnv. Пока что мой единственный обходной путь — сбросить на checkpoint = 'example-a', knit(), а затем сбросить на checkpoint = 'example-b' и knit(). Потом опять хорошо, но только на один прогон без смены контрольной точки. Сбивает с толку!   -  person Josh O'Brien    schedule 31.03.2013
comment
@Yihui - только что опубликовал ответ, который демонстрирует вашу крутую суть, а также показывает странное поведение, о котором я упоминал в предыдущем комментарии.   -  person Josh O'Brien    schedule 01.04.2013


Ответы (5)


Вот одно решение, которое все еще немного неудобно, но работает. Идея состоит в том, чтобы добавить параметр блока с именем mute, который по умолчанию принимает NULL, но также может принимать выражение R, например. mute_later() ниже. Когда knitr оценивает параметры чанка, можно оценить mute_later() и вернуть NULL; в то же время в opts_chunk есть побочные эффекты (установка глобальных параметров чанка, таких как eval = FALSE).

Теперь вам нужно поместить mute=mute_later() в фрагмент, после которого вы хотите пропустить остальные фрагменты, например. вы можете переместить эту опцию с example-a на example-b. Поскольку mute_later() возвращает NULL, что является значением по умолчанию для параметров mute, кеш не будет нарушен, даже если вы переместите этот параметр.

\documentclass{article}
\begin{document}

<<setup, include=FALSE, cache=FALSE>>=
rm(list = ls(all.names = TRUE), envir = globalenv())
opts_chunk$set(cache = TRUE) # enable cache to make it faster
opts_chunk$set(eval = TRUE, echo = TRUE, include = TRUE)

# set global options to mute later chunks
mute_later = function() {
  opts_chunk$set(cache = FALSE, eval = FALSE, echo = FALSE, include = FALSE)
  NULL
}
# a global option mute=NULL so that using mute_later() will not break cache
opts_chunk$set(mute = NULL)
@

<<example-a, mute=mute_later()>>=
x = rnorm(4)
Sys.sleep(5)
@

<<example-b>>=
y = rpois(10,5)
Sys.sleep(5)
@

<<example-c>>=
z = 1:10
Sys.sleep(3)
@

\end{document}

Это неудобно в том смысле, что вам нужно вырезать и вставлять , mute=mute_later(). В идеале вы должны просто установить метку фрагмента, как суть, которую я написал для Барри.

Причина, по которой мой первоначальный смысл не работал, заключается в том, что перехватчики фрагментов игнорируются при кэшировании фрагмента. Во второй раз, когда вы knit() файл, хук чанка checkpoint для example-a был пропущен, поэтому eval=TRUE для остальных чанков, и вы видели, что все чанки были оценены. Для сравнения, параметры фрагмента всегда оцениваются динамически.

person Yihui Xie    schedule 04.04.2013
comment
Гениально установить mute=NULL и вернуть mute_later() NULL, чтобы избежать поломки кеша. Отличный материал. Спасибо! - person Josh O'Brien; 04.04.2013
comment
Я полагаю, что можно также создать функцию checkpoint(), похожую на mute_later(), за исключением того, что она проверяет, является ли options$label == checkpoint, и отключает звук только для последующих фрагментов, если это TRUE. Затем, поместив mute = checkpoint() в выбранные заголовки чанков, можно было выбрать, до какого из них обрабатывать, изменив значение checkpoint в преамбуле. (Можно даже передать значение перед выполнением knit(), прикрепив его на второй позиции в пути поиска, хотя это быстро становится уродливым). В любом случае еще раз спасибо. - person Josh O'Brien; 04.04.2013
comment
@JoshO'Brien, проблема в том, что у вас нет доступа к текущей метке фрагмента в mute_later() (также недоступна options); есть opts_current$get('label'), но он отстает на один шаг; это уродливый факт в knitr, и мне придется подумать об этом. - person Yihui Xie; 04.04.2013
comment
Опять провалился! Однако, если серьезно, для моих реальных случаев использования предложенное вами решение идеально. Это намного намного лучше, чем вручную оборачивать каждую из моих контрольных точек в if(file.exists(f <- "filename.Rdata")) {load(f)} else {.......}, как я иногда делал в прошлом. Еще раз спасибо за мысль, которую вы вложили в это. - person Josh O'Brien; 05.04.2013

Это должно быть один из самых уродливых кладжей, которые я когда-либо писал...

Основная идея состоит в том, чтобы сканировать файл .Rnw на наличие фрагментов, извлекать их имена, определять, какие из них кэшируются, а затем определять, какие из них необходимо загрузить. Как только мы это сделаем, мы шаг за шагом сканируем, берем имя каждого фрагмента, которое необходимо загрузить, определяем имя базы данных из папки кеша и загружаем его, используя lazyLoad. После того, как мы загрузим все фрагменты, нам нужно принудительно выполнить оценку. Уродливо, и я уверен, что есть несколько ошибок, но, похоже, он работает на простом примере, который вы привели, и на некоторых других минимальных примерах, которые я создал. Это делает предположение, что файл .Rnw находится в текущем рабочем каталоге...

load_cache_until <- function(file, chunk, envir = parent.frame()){
    require(knitr)

    # kludge to detect chunk names, which come before the chunk of
    # interest, and which are cached... there has to be a nicer way...
    text <- readLines(file)
    chunks <- grep("^<<.*>>=", text, value = T)
    chunknames <- gsub("^<<([^,>]*)[,>]*.*", "\\1", chunks)
    #detect unnamed chunks
    tmp <- grep("^\\s*$", chunknames)
    chunknames[tmp] <- paste0("unnamed-chunk-", seq_along(tmp))
    id <- which(chunk == chunknames)
    previouschunks <- chunknames[seq_len(id - 1)]
    cachedchunks <- chunknames[grep("cache\\s*=\\s*T", chunks)]

    # These are the names of the chunks we want to load
    extractchunks <- cachedchunks[cachedchunks %in% previouschunks]

    oldls <- ls(envir, all = TRUE)
    # For each chunk...
    for(ch in extractchunks){   
        # Detect the file name of the database...
        pat <- paste0("^", ch, ".*\\.rdb")
        val <- gsub(".rdb", "", dir("cache", pattern = pat))
        # Lazy load the database
        lazyLoad(file.path("cache", val), envir = envir)
    }
    # Detect the new objects added
    newls <- ls(envir, all = TRUE)
    # Force evaluation...  There is probably a better way
    # to do this too...
    lapply(setdiff(newls, oldls), get)

    invisible()

}

load_cache_until("lotsOfComps.Rnw", "interestingChunk")

Читателю остается сделать код более надежным.

person Dason    schedule 29.03.2013
comment
Интересная идея, и я посмотрю на нее, когда вернусь к компьютеру с поддержкой R. Есть идеи, как это будет работать, если один или несколько фрагментов объектных вычислений будут безымянными? - person Josh O'Brien; 29.03.2013
comment
Я предполагаю, что это не будет работать правильно, если только именованный фрагмент не будет работать с интересующей переменной после того, как это сделает безымянный. Я предполагаю, что можно было бы учитывать безымянные фрагменты, но код как есть ничего не делает с безымянными фрагментами или встроенным кодом. - person Dason; 29.03.2013
comment
Хорошо, я добавил код, который обнаруживает неназванные фрагменты и загружает их. - person Dason; 30.03.2013
comment
+1 Это доблестное усилие, но я думаю, что весь подход может быть слишком хрупким. например мое первое испытание с ним включало фрагмент с заголовком <<cache=TRUE>>=, который ломал функцию. Кроме того, я обычно включаю разделы документа, используя child= директивы, подобные этой <<child-1, child="1-Make-Figures.Rnw", eval=TRUE>>=, что вызовет проблемы. также взломать этот код. Я предполагаю, что код, использующий код, используемый knit(), будет более успешным. - person Josh O'Brien; 30.03.2013
comment
Еще одна нелепая идея — вставить сразу после заголовка чанка отладочный код, сохраняющий всю рабочую область в файл .Rdata, который впоследствии можно будет открыть. - person Josh O'Brien; 30.03.2013
comment
@JoshO'Brien О, это определенно неряшливо и очень хрупко. Опять же, если вы хотите добавить код, который сохраняет всю рабочую область в .Rdata в интересующем фрагменте, это кажется слишком похожим на предложение purl, которое вы уже исключили для меня. Я лично жду, чтобы увидеть, придет ли Yihui и прокомментирует это... - person Dason; 30.03.2013
comment
Вы правы, это была плохая идея (хотя это немного лучше, чем использование purl(), так как это не потребовало бы пересчета всех фрагментов до того, который я хочу ввести) . И да, я тоже надеюсь на указатель от Yihui... - person Josh O'Brien; 30.03.2013

Yihui указывает на суть, которая близка к тому, что я попросил о.

В ответ на вопрос Барри Роулингсона (он же Spacedman) Yihui создал хук «контрольная точка», который позволяет пользователю установить имя последнего фрагмента, который будет обработан вызовом для вязания. Чтобы обработать фрагменты вверх через один с именем example-a, просто выполните opts_chunk$set(checkpoint = 'example-a') где-нибудь в начальном фрагменте «настройки».

Решение прекрасно работает --- при первом запуске с заданной контрольной точкой. К сожалению, во второй и последующие разы knit, по-видимому, игнорирует контрольную точку и обрабатывает все фрагменты. (Я обсуждаю обходной путь ниже, но он не идеален).

Вот немного сокращенная версия суть Yihui:

\documentclass{article}
\begin{document}

<<setup, include=FALSE>>=
rm(list = ls(all.names = TRUE), envir = globalenv())
opts_chunk$set(cache = TRUE) # enable cache to make it faster
opts_chunk$set(eval = TRUE, echo = TRUE, include = TRUE)

# Define hook that will skip all chunks after the one named in checkpoint
knit_hooks$set(checkpoint = function(before, options, envir) {
if (!before && options$label == options$checkpoint) {
opts_chunk$set(cache = FALSE, eval = FALSE, echo = FALSE, include = FALSE)
}
})

## Set the checkpoint
opts_chunk$set(checkpoint = 'example-a') # restore objects up to example-a
@

<<example-a>>=
x = rnorm(4)
@

<<example-b>>=
y = rpois(10,5)
@

<<example-c>>=
z = 1:10
@

\end{document}

Поскольку checkpoint="example-a", приведенный выше сценарий должен пройти через второй фрагмент, а затем подавить все последующие фрагменты, включая те, которые создают y и z. Давайте попробуем это пару раз, чтобы посмотреть, что произойдет:

library(knitr)

## First time, works like a charm
knit("checkpoint.Rnw")
ls()
[1] "x"

## Second time, Oops!, runs right past the checkpoint
knit("checkpoint.Rnw")
ls()
[1] "x" "y" "z"

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

  1. Отредактируйте checkpoint.Rnw, чтобы установить другую контрольную точку (выполнив, например, opts_chunk$set(checkpoint = 'example-b'))
  2. Выполнить knit("checkpoint.Rnw"),
  3. Отредактируйте checkpoint.Rnw, чтобы снова установить контрольную точку на 'example-a (выполнив opts_chunk$set(checkpoint = 'example-a'))
  4. Запустите knit("checkpoint.Rnw) еще раз. Это снова обработает все фрагменты до, но не дальше example-a.

Это может быть намного быстрее, чем пересчет всех объектов в чанках, так что полезно знать об этом, даже если это не идеально.

person Josh O'Brien    schedule 01.04.2013
comment
Кажется, я понимаю, что сейчас происходит. Я пытаюсь найти решение. - person Yihui Xie; 04.04.2013
comment
@Yihui -- это потрясающие новости. Я буду очарован, чтобы узнать, что происходит, и просто хочу, чтобы я мог быть больше помощи! - person Josh O'Brien; 04.04.2013

Как насчет добавления следующего фрагмента кода в конец вашего файла уценки?

```{r save_workspace_if_not_saved_yet, echo=FALSE}
if(!file.exists('knitr_session.RData')) {
  save.image(file = 'knitr_session.RData')
}
```

При первом вязании состояние рабочей области в конце процесса будет сохранено (при условии, что процесс не вызывает ошибок). Каждый раз, когда вам нужна последняя версия вашего рабочего пространства, просто удаляйте файл в своем рабочем каталоге.

person Mathias Versichele    schedule 10.12.2014
comment
Это хорошая вещь, но она не приблизится к выполнению того, о чем спрашивает этот вопрос. Я заинтересован в том, чтобы иметь возможность посетить, не пересчитывая их все, точную среду (т.е. набор объектов R), существующую в произвольном фрагменте. - person Josh O'Brien; 10.12.2014

Они такие же, как и любой файл данных, созданный save. Если вы возьмете пример Knitr-кэша из нового местоположения, это просто:

> library(knitr)
> knit("./005-latex.Rtex")
> load("cache/latex-my-cache_d9835aca7e54429f59d22eeb251c8b29.RData")
> ls()
 [1] "x"
person jamie.f.olson    schedule 29.03.2013
comment
Попробуйте загрузить .RData из чистого сеанса R. Это не работает. Вы получаете только x, потому что вы запустили knit в этом сеансе. - person Dason; 29.03.2013