Оптимизация гауссовского процесса в Stan / rstan

Недавно я столкнулся с гауссовскими моделями процессов и случайно подумал, что они могут быть решением проблемы, над которой я работал в своей лаборатории. У меня есть открытый и связанный с этим вопрос о перекрестной проверке, но я хотел отделить свои вопросы по моделированию / математике от вопросов по программированию. Следовательно, этот второй, связанный пост. Если более подробная информация об истории моей проблемы может помочь, вот ссылка на мой открытый вопрос CV.

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

functions{
    //covariance function for main portion of the model
    matrix main_GP(
        int Nx,
        vector x,
        int Ny,
        vector y, 
        real alpha1,
        real alpha2,
        real alpha3,
        real rho1,
        real rho2,
        real rho3,
        real rho4,
        real rho5,
        real HR_f,
        real R_f){
                    matrix[Nx, Ny] K1;
                    matrix[Nx, Ny] K2;
                    matrix[Nx, Ny] K3;
                    matrix[Nx, Ny] Sigma;

                    //specifying random Gaussian process that governs covariance matrix
                    for(i in 1:Nx){
                        for (j in 1:Ny){
                            K1[i,j] = alpha1*exp(-square(x[i]-y[j])/2/square(rho1));
                        }
                    }

                    //specifying random Gaussian process incorporates heart rate
                    for(i in 1:Nx){
                        for(j in 1:Ny){
                            K2[i, j] = alpha2*exp(-2*square(sin(pi()*fabs(x[i]-y[j])*HR_f))/square(rho2))*
                            exp(-square(x[i]-y[j])/2/square(rho3));
                        }
                    }

                    //specifying random Gaussian process incorporates heart rate as a function of respiration
                    for(i in 1:Nx){
                        for(j in 1:Ny){
                            K3[i, j] = alpha3*exp(-2*square(sin(pi()*fabs(x[i]-y[j])*HR_f))/square(rho4))*
                            exp(-2*square(sin(pi()*fabs(x[i]-y[j])*R_f))/square(rho5));
                        }
                    }

                    Sigma = K1+K2+K3;
                    return Sigma;
                }
    //function for posterior calculations
    vector post_pred_rng(
        real a1,
        real a2,
        real a3,
        real r1, 
        real r2,
        real r3,
        real r4,
        real r5,
        real HR,
        real R,
        real sn,
        int No,
        vector xo,
        int Np, 
        vector xp,
        vector yobs){
                matrix[No,No] Ko;
                matrix[Np,Np] Kp;
                matrix[No,Np] Kop;
                matrix[Np,No] Ko_inv_t;
                vector[Np] mu_p;
                matrix[Np,Np] Tau;
                matrix[Np,Np] L2;
                vector[Np] yp;

    //--------------------------------------------------------------------
    //Kernel Multiple GPs for observed data
    Ko = main_GP(No, xo, No, xo, a1, a2, a3, r1, r2, r3, r4, r5, HR, R);
    Ko = Ko + diag_matrix(rep_vector(1, No))*sn;

    //--------------------------------------------------------------------
    //kernel for predicted data
    Kp = main_GP(Np, xp, Np, xp, a1, a2, a3, r1, r2, r3, r4, r5, HR, R);
    Kp = Kp + diag_matrix(rep_vector(1, Np))*sn;

    //--------------------------------------------------------------------
    //kernel for observed and predicted cross 
    Kop = main_GP(No, xo, Np, xp, a1, a2, a3, r1, r2, r3, r4, r5, HR, R);

    //--------------------------------------------------------------------
    //Algorithm 2.1 of Rassmussen and Williams... 
    Ko_inv_t = Kop'/Ko;
    mu_p = Ko_inv_t*yobs;
    Tau=Kp-Ko_inv_t*Kop;
    L2 = cholesky_decompose(Tau);
    yp = mu_p + L2*rep_vector(normal_rng(0,1), Np);
    return yp;
    }
}

data { 
    int<lower=1> N1;
    int<lower=1> N2;
    vector[N1] X; 
    vector[N1] Y;
    vector[N2] Xp;
    real<lower=0> mu_HR;
    real<lower=0> mu_R;
}

transformed data { 
    vector[N1] mu;
    for(n in 1:N1) mu[n] = 0;
}

parameters {
    real loga1;
    real loga2;
    real loga3;
    real logr1;
    real logr2;
    real logr3;
    real logr4;
    real logr5;
    real<lower=.5, upper=3.5> HR;
    real<lower=.05, upper=.75> R;
    real logsigma_sq;
}

transformed parameters {
    real a1 = exp(loga1);
    real a2 = exp(loga2);
    real a3 = exp(loga3);
    real r1 = exp(logr1);
    real r2 = exp(logr2);
    real r3 = exp(logr3);
    real r4 = exp(logr4);
    real r5 = exp(logr5);
    real sigma_sq = exp(logsigma_sq);
}

model{ 
    matrix[N1,N1] Sigma;
    matrix[N1,N1] L_S;

    //using GP function from above 
    Sigma = main_GP(N1, X, N1, X, a1, a2, a3, r1, r2, r3, r4, r5, HR, R);
    Sigma = Sigma + diag_matrix(rep_vector(1, N1))*sigma_sq;

    L_S = cholesky_decompose(Sigma);
    Y ~ multi_normal_cholesky(mu, L_S);

    //priors for parameters
    loga1 ~ student_t(3,0,1);
    loga2 ~ student_t(3,0,1);
    loga3 ~ student_t(3,0,1);
    logr1 ~ student_t(3,0,1);
    logr2 ~ student_t(3,0,1);
    logr3 ~ student_t(3,0,1);
    logr4 ~ student_t(3,0,1);
    logr5 ~ student_t(3,0,1);
    logsigma_sq ~ student_t(3,0,1);
    HR ~ normal(mu_HR,.25);
    R ~ normal(mu_R, .03);
}

generated quantities {
    vector[N2] Ypred;
    Ypred = post_pred_rng(a1, a2, a3, r1, r2, r3, r4, r5, HR, R, sigma_sq, N1, X, N2, Xp, Y);
}

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

Я пытаюсь предсказать значения для данных 3,5 с (выборка с частотой 10 Гц - то есть 35 точек данных), используя данные за 15 секунд до и после зараженного участка (с выборкой с частотой 3,33 Гц, итого 100 точек данных).

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

 fit.pred2 <- stan(file = 'Fast_GP6_all.stan',
                 data = dat, 
                 warmup = 1000,
                 iter = 1500,
                 refresh=5,
                 chains = 3,
                 pars= pars.to.monitor
                 )

Честно говоря, я не знаю, нужно ли мне так много итераций разминки. Я полагаю, что часть медленной оценки является результатом довольно неинформативных априорных значений (за исключением частоты сердечных сокращений и дыхания HR & R, поскольку они имеют довольно хорошо известные диапазоны в состоянии покоя у здорового взрослого человека).

Любые предложения по ускорению работы моей программы приветствуются.

Спасибо.


person Matt Barstead    schedule 02.02.2018    source источник


Ответы (1)


Вы можете взять ветку разработки стандартной математической библиотеки, в которой есть недавно обновленная версия multi_normal_cholesky, которая использует внутренние аналитические градиенты вместо autodiff. Для этого вы можете выполнить в R source("https://raw.githubusercontent.com/stan-dev/rstan/develop/install_StanHeaders.R") , но вам необходимо иметь CXXFLAGS+=-std=c++11 в вашем файле ~ / .R / Makevars и, возможно, впоследствии потребуется переустановить пакет rstan.

И multi_normal_cholesky, и ваш main_GP имеют значение O (N ^ 3), поэтому ваша программа Stan никогда не будет особенно быстрой, но инкрементная оптимизация этих двух функций будет иметь самое большое значение.

Помимо этого, есть некоторые мелочи, такие как Sigma = Sigma + diag_matrix(rep_vector(1, N1))*sigma_sq; , который следует заменить на for (n in 1:N1) Sigma[n,n] += sigma_sq; Причина в том, что умножение sigma_sq на диагональную матрицу помещает N1 узлов в квадрате в дерево автодифференции, так же как и добавление его к Sigma, которое выделяет много памяти и освобождение. Явный цикл по диагонали помещает только N1 узлов в дерево автодифференцирования, или, может быть, он просто обновляет существующее дерево, если мы умны с оператором +=. То же самое и внутри вашей post_pred_rng функции, хотя это менее критично, потому что сгенерированный блок количеств оценивается двойными, а не пользовательским типом Stan для автодифференциального режима в обратном режиме.

Я думаю / надеюсь, что vector[N2] Ypred = post_pred_rng(...); будет немного быстрее, чем vector[N2] Ypred; // preallocates Ypred with NaNs Ypred = post_pred_rng(...); , за счет исключения этапа предварительного распределения; в любом случае читать лучше.

Наконец, хотя это не влияет на скорость, вы не обязаны указывать свои параметры в форме журнала, а затем блокировать их в блоке преобразованных параметров. Вы можете просто объявить вещи с помощью <lower=0>, и это приведет к тому же результату, хотя тогда вы будете указывать свои априорные значения для положительно ограниченных вещей, а не для неограниченных вещей. В большинстве случаев это более интуитивно понятно. Те ученики-приоры с 3 степенями свободы имеют очень тяжелый хвост, из-за чего Стэн может делать много прыжков (до предела в 10 по умолчанию), по крайней мере, во время разминки. Количество шагов чехарда является основным фактором времени выполнения, поскольку каждая итерация требует 2^s - 1 оценок функции / градиента.

person Ben Goodrich    schedule 02.02.2018
comment
Прежде всего, Бен, большое спасибо за этот подробный и вдумчивый ответ. Некоторые из ваших ответов приобретают для меня очевидный смысл при чтении. Остальные я попробую и проработаю по мере их реализации. Как человек, который провел последние несколько недель, изучая доску сообщений, связанных со станами, я был чрезвычайно впечатлен ответом команды разработчиков на запросы пользователей. Большое спасибо за вашу постоянную помощь в прицеливании для тех из нас, кто изо всех сил старается забить свои крошечные неверно указанные гвозди своим хорошо продуманным молотком. - person Matt Barstead; 02.02.2018