выборка подграфов из разных размеров с использованием igraph

у меня есть объект igraph mygraph с ~10,000 узлами и ~145,000 ребрами, и мне нужно создать несколько подграфов из этого графика, но с разными размерами. Мне нужно создать подграфы из определенного размера (от 5 узлов до 500 узлов), где все узлы соединены в каждом подграфе. Мне нужно создать ~1,000 подграфов для каждого размера (i.e, 1000 подграфов для size5, 1000 для размера 6 и т. д.), а затем вычислить некоторые значения для каждого графика в соответствии с различным узлом атрибуты. У меня есть код, но для выполнения всех вычислений требуется много времени. Я думал, используя graphlets функция для того, чтобы получить различные размеры, но каждый раз, когда я запускаю его на своем компьютере, он падает из-за проблем с памятью.

вот код, который я использую:

первым шагом было создание функции для создания подграфов разных размеров и выполнения необходимых вычислений.

random_network<-function(size,G){
     score_fun<-function(g){                                                        
          subsum <- sum(V(g)$weight*V(g)$RWRNodeweight)/sqrt(sum(V(g)$RWRNodeweight^2))
           subsum
           } 

      genes.idx <- V(G)$name
      perm <- c()
      while(length(perm)<1000){
           seed<-sample(genes.idx,1) 
           while( length(seed)<size ){
                tmp.neigh <- V(G)[unlist(neighborhood(G,1,seed))]$name
                tmp.neigh <- setdiff(tmp.neigh, seed)
                if( length(tmp.neigh)>0 )  
                seed<-c(seed,sample(tmp.neigh,1)) else break 
            }
      if( length(seed)==size )
      perm <- c(perm,score_fun(induced.subgraph(G,seed)))
      } 
      perm
     } 

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

 ### generate some example data
 library(igraph)
 my_graph <- erdos.renyi.game(10000, 0.0003)
 V(my_graph)$name <- 1:vcount(my_graph)
 V(my_graph)$weight <- rnorm(10000)
 V(my_graph)$RWRNodeweight <- runif(10000, min=0, max=0.05)

 ### Run the code to get the subgraphs from different size and do calculations based on nodes
 genesets.length<- seq(5:500)
 genesets.length.null.dis <- list()
 for(k in 5:max(genesets.length){ 
     genesets.length.null.dis[[as.character(k)]] <- random_network(size=k,G=my_graph)
  }

5 ответов


один подход для ускорения вашего кода дальше, чем это возможно в базе R, - использовать пакет Rcpp. Рассмотрим следующую реализацию Rcpp полной операции. Он принимает в качестве входных данных следующее:

  • valid: индексы всех узлов, которые находятся в достаточно большом компоненте
  • el, deg, firstPos: представление списка ребер графика (node iсоседи el[firstPos[i]], el[firstPos[i]+1], ..., el[firstPos[i]+deg[i]-1]).
  • size: размер подграфа для образца
  • nrep: количество повторений
  • weights: веса ребер, хранящиеся в V(G)$weight
  • RWRNodeweight: веса ребер, хранящиеся в V(G)$RWRNodeweight
library(Rcpp)
cppFunction(
"NumericVector scores(IntegerVector valid, IntegerVector el, IntegerVector deg,
                      IntegerVector firstPos, const int size, const int nrep,
                      NumericVector weights, NumericVector RWRNodeweight) {
  const int n = deg.size();
  std::vector<bool> used(n, false);
  std::vector<bool> neigh(n, false);
  std::vector<int> neighList;
  std::vector<double> scores(nrep);
  for (int outerIter=0; outerIter < nrep; ++outerIter) {
    // Initialize variables
    std::fill(used.begin(), used.end(), false);
    std::fill(neigh.begin(), neigh.end(), false);
    neighList.clear();

    // Random first node
    int recent = valid[rand() % valid.size()];
    used[recent] = true;
    double wrSum = weights[recent] * RWRNodeweight[recent];
    double rrSum = RWRNodeweight[recent] * RWRNodeweight[recent];

    // Each additional node
    for (int idx=1; idx < size; ++idx) {
      // Add neighbors of recent
      for (int p=firstPos[recent]; p < firstPos[recent] + deg[recent]; ++p) {
        if (!neigh[el[p]] && !used[el[p]]) {
          neigh[el[p]] = true;
          neighList.push_back(el[p]);
        }
      }

      // Compute new node to add from all neighbors
      int newPos = rand() % neighList.size();
      recent = neighList[newPos];
      used[recent] = true;
      wrSum += weights[recent] * RWRNodeweight[recent];
      rrSum += RWRNodeweight[recent] * RWRNodeweight[recent];

      // Remove from neighList
      neighList[newPos] = neighList[neighList.size() - 1];
      neighList.pop_back();
    }

    // Compute score from wrSum and rrSum
    scores[outerIter] = wrSum / sqrt(rrSum);
  }
  return NumericVector(scores.begin(), scores.end());
}
")

теперь все, что нам нужно сделать в базе R, это создать аргументы для scores, что можно сделать довольно легко:

josilber.rcpp <- function(size, num.rep, G) {
  n <- length(V(G)$name)

  # Determine which nodes fall in sufficiently large connected components
  comp <- components(G)
  valid <- which(comp$csize[comp$membership] >= size)

  # Construct an edge list representation for use in the Rcpp code
  el <- get.edgelist(G, names=FALSE) - 1
  el <- rbind(el, el[,2:1])
  el <- el[order(el[,1]),]
  deg <- degree(G)
  first.pos <- c(0, cumsum(head(deg, -1)))

  # Run the proper number of replications
  scores(valid-1, el[,2], deg, first.pos, size, num.rep,
         as.numeric(V(G)$weight), as.numeric(V(G)$RWRNodeweight))
}

время для выполнения 1000 репликации пылает быстро по сравнению с исходным кодом и все igraph решения, которые мы видели до сих пор (обратите внимание, что для большей части этого бенчмаркинга я тестировал оригинал josilber и random_network функции для 1 репликации вместо 1000, потому что тестирование для 1000 займет непомерно много времени):

  • Size=10: 0.06 секунд (ускорение 1200x по сравнению с моим ранее предложенным

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

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

tmp.neigh <- V(G)[unlist(neighborhood(G,1,seed))]$name
tmp.neigh <- setdiff(tmp.neigh, seed)

короче, вы смотрите на соседей каждого узла в выбранное подмножество на каждой итерации. Более эффективным подходом было бы хранить вектор соседей и обновлять его каждый раз, когда вы добавляете новый узел; это будет более эффективно, потому что вам нужно только рассмотреть соседей нового узла.

josilber <- function(size, num.rep, G) {
  score_fun <- function(vert) sum(vert$weight*vert$RWRNodeweight)/sqrt(sum(vert$RWRNodeweight^2))
  n <- length(V(G)$name)

  # Determine which nodes fall in sufficiently large connected components
  comp <- components(G)
  valid <- which(comp$csize[comp$membership] >= size)

  perm <- replicate(num.rep, {
    first.node <- sample(valid, 1)
    used <- (1:n) == first.node  # Is this node selected?
    neigh <- (1:n) %in% neighbors(G, first.node)  # Does each node neighbor our selections?
    for (iter in 2:size) {
      new.node <- sample(which(neigh & !used), 1)
      used[new.node] <- TRUE
      neigh[neighbors(G, new.node)] <- TRUE
    }
    score_fun(V(G)[used])
  })
  perm
}

для одной репликации это дает значительные ускорения по одной репликации кода в вопросе:

  • для size=50 одна репликация занимает 0,3 секунды для этого кода и 3,8 секунды для опубликованного код
  • для размера=100 одна репликация занимает 0,6 секунды для этого кода и 15,2 секунды для размещенного кода
  • для size=200 одна репликация занимает 1,5 секунды для этого кода и 69,4 секунды для опубликованного кода
  • для size=500 одна репликация для этого кода занимает 2,7 секунды (поэтому 1000 реплик должно занять около 45 минут); я не тестировал одну репликацию опубликованного кода.

как упоминалось в других ответах, распараллеливание может еще больше повысить производительность выполнения 1000 реплик для заданного размера графика. Следующие использует doParallel пакет для параллелизации повторяющегося шага (настройка в значительной степени идентична той, которую сделал @Chris в своем ответе):

library(doParallel)
cl <- makeCluster(4)
registerDoParallel(cl)
josilber2 <- function(size, num.rep, G) {
  score_fun <- function(vert) sum(vert$weight*vert$RWRNodeweight)/sqrt(sum(vert$RWRNodeweight^2))
  n <- length(V(G)$name)

  # Determine which nodes fall in sufficiently large connected components
  comp <- components(G)
  valid <- which(comp$csize[comp$membership] >= size)

  perm <- foreach (i=1:num.rep, .combine='c') %dopar% {
    library(igraph)
    first.node <- sample(valid, 1)
    used <- (1:n) == first.node  # Is this node selected?
    neigh <- (1:n) %in% neighbors(G, first.node)  # Does each node neighbor our selections?
    for (iter in 2:size) {
      new.node <- sample(which(neigh & !used), 1)
      used[new.node] <- TRUE
      neigh[neighbors(G, new.node)] <- TRUE
    }
    score_fun(V(G)[used])
  }
  perm
}

на моем Macbook Air,josilber(100, 1000, my_graph) занимает 670 секунд для запуска (это непараллельная версия), в то время как josilber2(100, 1000, my_graph) для запуска требуется 239 секунд (это параллельная версия, настроенная с 4 рабочими). Для size=100 случае, мы поэтому получили ускорение 20x от улучшений кода и дополнительное ускорение 3x от распараллеливания, для общего ускорения 60x.


у меня нет полного ответа, но вот некоторые вещи, которые нужно рассмотреть, чтобы помочь ускорить его (предполагая, что нет гораздо более быстрого подхода с использованием другого метода).

  1. удалите из графика любые узлы, которые не являются частью компонента такого размера, как вы ищете. Это действительно будет зависеть от вашей сетевой структуры, но похоже, что ваши сети являются генами, поэтому, вероятно, есть много генов с очень низкой степенью, и вы можете получить некоторые ускорения, удалив их. Что-то вроде этого кода:

    cgraph <- clusters(G)
    tooSmall <- which(cgraph$csize < size)
    toKeep <- setdiff(1:length(V(G)), which(cgraph$membership %in% tooSmall))
    graph <- induced.subgraph(G, vids=toKeep)
    
  2. рассмотрите возможность запуска этого параллельно, чтобы воспользоваться несколькими ядрами. Например, используя parallel пакета и mclapply.

    library(parallel)
    genesets.length<- seq(5, 500)
    names(genesets.length) <- genesets.length
    genesets.length.null.dis <- mclapply(genesets.length, mc.cores=7,
                                         function(length) {
                                           random_network(size=length, G=my_graph)
                                         })
    

Я думаю, что было бы гораздо эффективнее использовать функцию cliques в igraph, поскольку клика является подграфом полностью связанных узлов. Просто установите min и max равными размеру подграфа, который вы ищете, и он вернет все клики размера 5. Вы можете взять любое подмножество из них, которое отвечает вашим потребностям. К сожалению, в Примере erdos-Renyi graph вы сгенерировали часто, когда самая большая клика меньше 5, поэтому это не будет работать для примера. Однако, он должен отлично работать для реальной сети, которая демонстрирует больше кластеризации, чем график Эрдоса-Рени, как это, скорее всего, делает ваш.

library(igraph)
##Should be 0.003, not 0.0003 (145000/choose(10000,2))
my_graph <- erdos.renyi.game(10000, 0.003)

cliques(my_graph,min=5,max=5)

у вас есть ряд проблем с вашим кодом(вы не предварительно выделяете векторы и т. д.). Пожалуйста, посмотрите код, который я придумал ниже. Однако я протестировал его только до подграфа размера 100. Тем не менее, экономия скорости увеличивается по мере увеличения размера подграфа по сравнению с вашим кодом. Вы должны установить foreach пакет также. Я запустил это на ноутбуке с ядрами 4, 2.1 GHz.

random_network_new <- function(gsize, G) {
  score_fun <- function(g) {
    subsum <- sum(V(g)$weight * V(g)$RWRNodeweight) / sqrt(sum(V(g)$RWRNodeweight^2))
  }

  genes.idx <- V(G)$name

  perm <- foreach (i=seq_len(1e3), .combine='c') %dopar% {
    seed <- rep(0, length=gsize)
    seed[1] <- sample(genes.idx, 1)

    for (j in 2:gsize) {
      tmp.neigh <- neighbors(G, as.numeric(seed[j-1]))
      tmp.neigh <- setdiff(tmp.neigh, seed)
      if (length(tmp.neigh) > 0) {
        seed[j] <- sample(tmp.neigh, 1)
      } else {
        break
      }
    }
    score_fun(induced.subgraph(G, seed))
  }
  perm
}

обратите внимание, что я переименовал функцию random_network_new и аргумент gsize.

system.time(genesets <- random_network_new(gsize=100, G=my_graph))                                            
   user   system  elapsed 
1011.157    2.974  360.925 
system.time(genesets <- random_network_new(gsize=50, G=my_graph))
   user  system elapsed 
822.087   3.119 180.358 
system.time(genesets <- random_network_new(gsize=25, G=my_graph))
   user  system elapsed 
379.423   1.130  74.596 
system.time(genesets <- random_network_new(gsize=10, G=my_graph))
   user  system elapsed 
144.458   0.677  26.508 

один пример использования вашего кода (мой более 10 раз быстрее для размера подграфа 10; это было бы намного быстрее с большими подграфами):

system.time(genesets_slow <- random_network(10, my_graph))
   user  system elapsed 
350.112   0.038 350.492