Оптимизация функции преобразования растра в кадр данных с точки зрения баланса оперативной памяти и скорости в r

Проблема

Я пытаюсь использовать модель, которая требует от меня преобразования очень больших растров (около 10 миллионов ячеек) в data.frame, я делаю это на общем сервере, и поэтому я пытаюсь оптимизировать, чтобы уменьшить Используемая оперативная память и, надеюсь, не сильно снизит скорость. Для этого я написал 2 функции, но безуспешно. RPUBS с результатами моих попыток находится по этой ссылке, а github с rmd для этого здесь, включая файлы rds для профилирования profvis.

Первая функция

Сначала давайте загрузим пакеты, которые нам понадобятся:

# For spaital manipulation
library(raster)
# For benchmarking speed and memory
library(profvis)
# To parallelize operations
library(doParallel)
# To parallelize operations
library(foreach)
# For combination and looping
library(purrr)
# Data wranggling
library(dplyr)
library(data.table)

плитка

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

SplitRas <- function(Raster,ppside, nclus = 1){
  TempRast <- paste0(getwd(), "/Temp")
  h        <- ceiling(ncol(Raster)/ppside)
  v        <- ceiling(nrow(Raster)/ppside)
  agg      <- aggregate(Raster,fact=c(h,v))
  agg[]    <- 1:ncell(agg)
  agg_poly <- rasterToPolygons(agg)
  names(agg_poly) <- "polis"
  r_list <- list()
  if(nclus == 1){
    for(i in 1:ncell(agg)){
      dir.create(TempRast)
      rasterOptions(tmpdir=TempRast)
      e1          <- extent(agg_poly[agg_poly$polis==i,])
      r_list[[i]] <- crop(Raster,e1)
      if((freq(r_list[[i]], value=NA)/ncell(r_list[[i]])) != 1){
        writeRaster(r_list[[i]],filename=paste("SplitRas",i,sep=""),
                  format="GTiff",datatype="FLT4S",overwrite=TRUE)
      }
      unlink(TempRast, recursive = T, force = T)
    }
  } else if(nclus != 1){
    cl <- parallel::makeCluster(nclus)
    doParallel::registerDoParallel(cl)
    r_list <- foreach(i = 1:ncell(agg), .packages = c("raster")) %dopar% {
      dir.create(TempRast)
      rasterOptions(tmpdir=TempRast)
      e1          <- extent(agg_poly[agg_poly$polis==i,])
      Temp <- crop(Raster,e1)
      if((raster::freq(Temp, value=NA)/ncell(Temp)) != 1){
        writeRaster(Temp,filename=paste("SplitRas",i,sep=""),
                    format="GTiff",datatype="FLT4S",overwrite=TRUE)
      }
      unlink(TempRast, recursive = T, force = T)
      Temp
    }
    parallel::stopCluster(cl)
  }
}

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

Эта функция имеет 3 аргумента:

  • Растр. Стопка, которую вы хотите разделить на плитки.
  • ppside: количество горизонтальных и вертикальных плиток, которые вы получите в итоге, окончательное количество плиток будет ppside*ppside, поэтому, если ppside равно 3, у вас будет 9 плиток.
  • nclus: количество кластеров, которое вы будете использовать для распараллеливания и ускорения обработки.

В конце этой функции у вас будет ppside*ppside количество плиток, сохраненных в вашем рабочем каталоге, и все они будут называться SplitRasN.tif, где N — номер плитки. В качестве примера воспользуемся биоклиматическими переменными, доступными в растровом пакете:

Bios <- getData('worldclim', var='bio', res=10)

Я протестирую это преобразование в другое количество плиток, как показано на следующем рисунке.

введите здесь описание изображения

Преобразование растра в тайлы, а затем из тайлов во фрейм данных (пример)

поэтому сначала мы будем использовать функцию SplitRas, чтобы получить 16 плиток, используя 4 ядра:

SplitRas(Raster = Bios, ppside = 4, nclus = 4)

Это вернет следующие файлы: r list.files(pattern = "SplitRas")

Чтобы собрать эти плитки в один фрейм данных со всеми ячейками, не относящимися к NA, нам нужен список плиток, который мы получаем с помощью:

Files <- list.files(pattern = "SplitRas", full.names = T)

Который мы можем использовать затем в следующей функции:

SplitsToDataFrame <- function(Splits, ncores = 1){
  TempRast <- paste0(getwd(), "/Temp")
  if(ncores == 1){
    Temps <- list()
    for(i in 1:length(Splits)){
      dir.create(TempRast)
      rasterOptions(tmpdir=TempRast)
      Temp <- stack(Splits[i])
      Temp <- as.data.frame(Temp, row.names = NULL, col.names = NULL, xy =TRUE)
      colnames(Temp)[3:ncol(Temp)] <- paste0("Var", 1:length(3:ncol(Temp)))
      Temps[[i]] <- Temp[complete.cases(Temp),]
      gc()
      unlink(TempRast, recursive = T, force = T)
      message(i)
    }
    Temps <- data.table::rbindlist(Temps)
  } else if(ncores > 1){
    cl <- parallel::makeCluster(ncores)
    doParallel::registerDoParallel(cl)
    Temps <- foreach(i = 1:length(Splits), .packages = c("raster", "data.table")) %dopar%{
      dir.create(TempRast)
      rasterOptions(tmpdir=TempRast)
      Temp <- stack(Splits[i])
      Temp <- as.data.frame(Temp, row.names = NULL, col.names = NULL, xy =TRUE)
      colnames(Temp)[3:ncol(Temp)] <- paste0("Var", 1:length(3:ncol(Temp)))
      gc()
      unlink(TempRast, recursive = T, force = T)
      Temp[complete.cases(Temp),]
    }
    Temps <- data.table::rbindlist(Temps)
    parallel::stopCluster(cl)
  }
  return(Temps)
}

Где Splits — это векторы символов с путями к тайлам, а ncores — количество ядер, используемых для распараллеливания. Это приведет к кадру данных с непустыми ячейками.

DF <- SplitsToDataFrame(Splits = Files, ncores = 4)

Тестирование памяти (что я пробовал)

Сначала я сгенерировал плитки для 1, 2, 4, 8, 10 и 12 сторон.

sides <- c(1,2,4,8,10, 12)

Home <- getwd()
AllFiles <- list()
for(i in 1:length(sides)){
  dir.create(path = paste0(Home, "/Sides_", sides[i]))
  setwd(paste0(Home, "/Sides_", sides[i]))
  SplitRas(Raster = Bios, ppside = sides[i], nclus = ifelse(sides[i] < 4, sides[i], 4))
  AllFiles[[i]] <- list.files(pattern = "SplitRas", full.names = T) %>% stringr::str_replace_all("\\./", paste0(getwd(), "/"))
}
setwd(Home)

А затем профилировал функцию, используя только последовательный вариант функции

library(profvis)
P <- profvis({
    P1 <- SplitsToDataFrame(Splits = AllFiles[[1]])
    P2 <- SplitsToDataFrame(Splits = AllFiles[[2]])
    P3 <- SplitsToDataFrame(Splits = AllFiles[[3]])
    P4 <- SplitsToDataFrame(Splits = AllFiles[[4]])
    P5 <- SplitsToDataFrame(Splits = AllFiles[[5]])
})
P
htmlwidgets::saveWidget(P, "profile.html")
saveRDS(P, "P.rds")

Однако, как видно на следующем рисунке (более подробную информацию можно найти в ссылках Rpubs выше), оперативная память более или менее такая же, как и раньше, но время увеличилось (эта последняя часть ожидалась).

введите здесь описание изображения.

Некоторые дополнительные вещи

Наконец, когда я попытался сравнить использование ОЗУ параллельно, мне показалось, что profvis этого не фиксирует. Любая идея о том, как это проверить?

library(profvis)
PPar <- profvis({
    P1 <- SplitsToDataFrame(Splits = AllFiles[[3]], ncores = 1)
    P2 <- SplitsToDataFrame(Splits = AllFiles[[3]], ncores = 2)
    P3 <- SplitsToDataFrame(Splits = AllFiles[[3]], ncores = 4)
    P4 <- SplitsToDataFrame(Splits = AllFiles[[3]], ncores = 7)
})
PPar
htmlwidgets::saveWidget(PPar, "profileParallel.html")
saveRDS(PPar, "PPar.rds")

введите здесь описание изображения


person Derek Corcoran    schedule 17.05.2021    source источник


Ответы (2)


arrow_upward
3
arrow_downward

Вы можете использовать rasterToPoints. Обратите внимание, что любые строки, которые полностью являются NA, исключаются из вывода. Также стоит отметить, что вы можете контролировать, сколько оперативной памяти используется с помощью rasterOptions(maxmemory = XXX).

x = as.data.frame(rasterToPoints(Bios))
head(x)
#           x        y bio1 bio2 bio3  bio4 bio5 bio6 bio7 bio8 bio9 bio10 bio11 bio12 bio13 bio14 bio15 bio16 bio17 bio18 bio19
# 1 -37.91667 83.58333 -174   67   17 11862   37 -356  393  -31 -307    -7  -307   144    22     7    38    59    24    50    24
# 2 -37.75000 83.58333 -174   67   17 11870   37 -355  392  -30 -219    -7  -307   143    22     7    42    59    23    50    24
# 3 -36.91667 83.58333 -172   68   17 11872   39 -354  393  -29 -217    -5  -305   136    22     6    42    57    22    49    23
# 4 -36.75000 83.58333 -173   68   17 11887   39 -354  393  -29 -217    -5  -306   136    22     6    42    57    22    49    23
# 5 -36.58333 83.58333 -173   68   17 11877   39 -354  393  -29 -217    -6  -306   136    22     6    42    57    22    49    23
# 6 -36.41667 83.58333 -173   68   17 11879   38 -354  392  -29 -217    -6  -306   137    22     6    41    57    22    49    24
person dww    schedule 17.05.2021

arrow_upward
1
arrow_downward

Я пытаюсь использовать модель, которая требует от меня преобразования очень больших растровых стеков (около 10 миллионов ячеек) в data.frame,

Вместо этого я бы использовал raster::predict или terra::predict; и, возможно, запустить их с распараллеливанием.

terra есть метод makeTiles, который может быть полезен.

person Robert Hijmans    schedule 17.05.2021
comment
Большое спасибо @RobertHijmans, интересно, как бы вы использовали для этого функцию прогнозирования! Я очень заинтригован этой идеей! - person Derek Corcoran; 17.05.2021
comment
Вы можете сделать predict(raster data, model) Возможно, аргумент fun=myfun функции прогнозирования модели не является стандартным - person Robert Hijmans; 17.05.2021