base_name = paste(knitr::current_input(), '_figs/', sep='') knitr::opts_chunk$set( cache.path=paste('_knitr_cache/', base_name, sep='/'), fig.path=paste('figure/', base_name, sep='/'), dpi=300 ) suppressPackageStartupMessages(library(tidyverse)) suppressPackageStartupMessages(library(mvtnorm)) suppressPackageStartupMessages(library(viridis)) suppressPackageStartupMessages(library(lhs))
f = function(theta) { theta = matrix(theta, ncol=2) x = cbind(theta[,1], theta[,2]+0.03*theta[,1]^2-3) dmvnorm(x, c(0,0), diag(c(100, 1))) } # true solution df = crossing(theta1 = seq(-22, 22, .5), theta2 = seq(-12,7,.5)) %>% mutate(f = f(cbind(theta1,theta2))) ggplot(df) + geom_raster(aes(x=theta1, y=theta2, fill=f)) + geom_contour(aes(x=theta1, y=theta2, z=f), col='black', bins=10) + scale_fill_viridis()
set.seed(123) lhs = maximinLHS(n=100, k=2) %>% as_data_frame %>% setNames(c('theta1', 'theta2')) %>% mutate(theta1 = theta1 * 40 - 20, theta2 = theta2 * 15 - 10) design = lhs %>% mutate(f = f(cbind(theta1, theta2))) print(design)
doit_estimate_sigma = function(design, sigma_0=NULL, optim_control=NULL) { stopifnot(nrow(design) > 1) m = nrow(design) theta = design %>% select(-f) %>% as.matrix dd = ncol(theta) ff = design$f GGfun = function(sigma2) { mat_ = matrix(0, nrow=m, ncol=m) for (ii in seq_len(dd)) { mat_ = mat_ - outer(theta[,ii], theta[,ii], '-')^2 / (2 * sigma2[ii]) } return(drop(exp(mat_))) } # leave-one-out MSE as function of the kernel width wmscv = function(sigma2) { GGinv_ = solve(GGfun(sigma2)) ee_ = drop(1/diag(GGinv_) * GGinv_ %*% sqrt(ff)) wmscv = sum(ee_ * diag(GGinv_) * ee_) / m return(wmscv) } # minimise wmscv wrt sigma2, use `optimize` for 1d and `optim` for >1d if (dd == 1) { sigma2 = optimize(wmscv, c(1e-6, diff(range(design$xx))))$minimum } else { if (is.null(sigma_0)) { # use component-wise silvermans rule as starting point sigma_0 = 1.06 * m^(-.2) * apply(theta, 2, sd) } sigma2 = optim(sigma_0, wmscv)$par } return(sigma2) }
doit_fit = function(design, sigma2=NULL) { m = nrow(design) theta = design %>% select(-f) %>% as.matrix dd = ncol(theta) ff = design$f GGfun = function(xx, yy, sigma2) { if (is.null(dim(xx))) xx = matrix(xx, nrow=1) if (is.null(dim(yy))) yy = matrix(yy, nrow=1) stopifnot(ncol(xx) == ncol(yy), ncol(xx) == length(sigma2)) mat_ = matrix(0, nrow=nrow(xx), ncol=nrow(yy)) for (ii in seq_along(sigma2)) { mat_ = mat_ - outer(xx[,ii], yy[,ii], '-')^2 / (2 * sigma2[ii]) } return(drop(exp(mat_))) } if(is.null(sigma2)) { sigma2 = doit_estimate_sigma(design) } # calculate parameters GG = GGfun(theta, theta, sigma2) GGinv = solve(GG) bb = drop(solve(GG, sqrt(ff))) ee = drop(1/diag(GGinv) * GGinv %*% sqrt(ff)) ans = list(GGfun=GGfun, theta=theta, sigma2=sigma2, ff=ff, bb=bb, GG=GG, GG2=sqrt(GG), ee=ee, GGinv=GGinv) return(ans) }
doit_approx = function(doit, theta_eval) { # calculate expecation and variance of target function at matrix of input # points r with(doit, { bGG2b_ = drop(bb %*% GG2 %*% bb) gg_ = GGfun(theta_eval, theta, sigma2) ggbb2_ = drop(gg_ %*% bb)^2 ee_ = ggbb2_ / (sqrt(prod(pi*sigma2)) * bGG2b_) vv_ = ggbb2_ * drop(1 - rowSums(gg_ * (gg_ %*% GGinv))) return(as_data_frame(cbind(theta_eval, ee=ee_, vv=vv_))) }) }
doit = doit_fit(design) theta_eval = select(df, starts_with('theta')) %>% as.matrix doit_eval = doit_approx(doit, theta_eval) %>% as_data_frame
ggplot(mapping=aes(x=theta1, y=theta2)) + geom_raster(data=df, aes(fill=f)) + geom_contour(data=doit_eval, aes(z=ee, colour=..level..)) + geom_point(data=design, pch=1, cex=4, colour='white') + scale_colour_viridis() + scale_fill_viridis()
ggplot(data=doit_eval, mapping=aes(x=theta1, y=theta2)) + geom_raster(aes(fill=vv)) + geom_contour(aes(z=ee, colour=..level..)) + geom_point(data=design, pch=1, cex=4, colour='white') + scale_colour_viridis() + scale_fill_viridis()
... at locations with the maximum predictive variance. Since the predictive variance surface is multimodal, the starting point used in the numerical maximisation matters. We use the design point with highest leave-one-out predictive variance as the starting point.
propose_new = function(doit) with(doit, { fn = function(r) { gg = GGfun(r, theta, sigma2) sum(bb * gg)^2 * drop(1 - gg %*% GGinv %*% gg) } vv = (sqrt(ff) - ee)^2 / diag(GGinv) r0 = theta[which.max(vv), ] optim(r0, fn, control=list(fnscale=-1))$par })
theta_n = propose_new(doit) ggplot(doit_eval, aes(x=theta1, y=theta2)) + geom_raster(aes(fill=vv)) + scale_fill_viridis() + geom_point(data=design, pch=1, cex=3, col='white') + geom_point(data=data.frame(as.list(theta_n)), pch=1, cex=3, col='red')
Add 75 points sequentially (only re-estimating sigma2
on the last iteration):
# function to update the doit object quickly if the smoothing parameters sigma2 # don't change; otherwise, call doit_fit again doit_update = function(doit, design_new) with(doit, { design_new = design_new[1, ] theta_new = design_new %>% select(-f) %>% as.matrix theta_up = rbind(theta, theta_new) gg_ = GGfun(theta, theta_new, sigma2) gg2_ = sqrt(gg_) GG_up = rbind(cbind(GG, gg_), cbind(t(gg_), 1)) GG2_up = rbind(cbind(GG2, gg2_), cbind(t(gg2_), 1)) dd_ = 1 / drop(1 - crossprod(gg_, GGinv %*% gg_)) hh_ = GGinv %*% gg_ GGinv_up = rbind(cbind(GGinv + dd_*tcrossprod(hh_), - dd_*hh_), cbind(-dd_ * t(hh_), dd_)) ff_up = c(ff, design_new$f) bb_up = drop(GGinv_up %*% sqrt(ff_up)) ee_up = drop(1/diag(GGinv_up) * GGinv_up %*% sqrt(ff_up)) # write new object doit$theta = theta_up doit$ff = ff_up doit$bb = bb_up doit$GG = GG_up doit$GG2 = GG2_up doit$ee = ee_up doit$GGinv = GGinv_up return(doit) })
n_new = 75 design_new = design doit_new = doit for (jj in 1:n_new) { theta_n = propose_new(doit_new) f_n = f(theta_n) design_add = data.frame(as.list(theta_n), f=f_n) design_new = bind_rows(design_new, design_add) if (jj %% 10 == 0) { sigma2 = doit_estimate_sigma(design_new) doit_new = doit_fit(design_new, sigma2=sigma2) } else { doit_new = doit_update(doit_new, design_add) } }
doit_eval_update = doit_approx(doit_new, theta_eval) ggplot(mapping=aes(x=theta1, y=theta2)) + geom_raster(data=df, aes(fill=f)) + geom_contour(data=doit_eval_update, aes(z=ee)) + geom_point(data=design_new %>% mutate(type=rep(c('old','new'), c(100, n()-100))), mapping=aes(x=theta1, y=theta2, colour=type), cex=2) + scale_fill_viridis()
doit2 = doit_new
doit_marginal = function(doit, k, theta_eval=NULL) with(doit, { if (is.null(theta_eval)) theta_eval = theta[, k] d_ij = tcrossprod(bb) * GG2 sum_d_ij = sum(d_ij) nu_ij = 0.5 * outer(theta[,k], theta[,k], '+') sd_ = sqrt(sigma2[k]/2) ans = sapply(theta_eval, function(tt) { phi_ij = dnorm(tt, nu_ij, sd_) return(sum(d_ij * phi_ij) / sum_d_ij) }) return(data_frame(par=colnames(theta)[k], theta=theta_eval, posterior=ans)) }) doit_marginals = function(doit, theta_eval=NULL) { map_df(1:ncol(doit$theta), ~doit_marginal(doit, ., theta_eval)) }
marg_theta = doit_marginals(doit2) ggplot(marg_theta, aes(x=theta, y=posterior)) + geom_point() + geom_line() + facet_wrap(~par, scales='free')
doit_expectation = function(doit) with(doit, { d_ij = tcrossprod(bb) * GG2 sum_d_ij = sum(d_ij) nu_ij = map(colnames(theta), ~0.5 * outer(theta[,.], theta[,.], '+')) return(map_dbl(nu_ij, ~sum(d_ij * .) / sum(d_ij))) }) doit_expectation(doit2)
doit_variance = function(doit) with(doit, { d_ij = tcrossprod(bb) * GG2 sum_d_ij = sum(d_ij) nu_ij = map(colnames(theta), ~0.5 * outer(theta[,.], theta[,.], '+')) dd = ncol(theta) kk = expand.grid(ii = 1:dd, jj = 1:dd) %>% filter(jj >= ii) %>% mutate(v = map2_dbl(ii, jj, ~ sum(d_ij * nu_ij[[.x]] * nu_ij[[.y]]) / sum_d_ij)) v_theta = matrix(0, dd, dd) for (l in 1:nrow(kk)) { v_theta[kk[l,'ii'], kk[l,'jj']] = v_theta[kk[l,'jj'], kk[l,'ii']] = kk[l,'v'] } rownames(v_theta) = colnames(v_theta) = colnames(theta) return(v_theta) }) v_theta = doit_variance(doit2) print(v_theta)
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.