6.1 引言

根据一组数据拟合模型,需要寻找参数估计值,以优化观测数据与模型预测之间的关系。模型参数估计值代表了我们感兴趣的群体的属性。虽然在每种情况下都有可能找到最佳参数值,但无论使用什么数据,都只是从总体中抽取的一组样本。假设可以从同总体中抽取不同的、独立的同类数据样本,如果对它们进行独立分析,很可能会得出至少略有不同的参数估计值。因此,在根据数据拟合模型时,估计参数的确切值其实并不是最重要的问题,相反,我们想知道的是,如果我们能够获得多个独立样本,这些估计值的可重复性有多大。也就是说,参数只是估计值,我们想知道对这些估计值有多大把握。例如,通常情况下,高度多变的数据通常会导致每个模型参数都可能具有广泛的可信估计值分布,通常情况下,这些估计值的置信区间也会很宽。

在本章中,我们将探讨其他可用的方法,以描述任何建模情况中至少一部分固有的不确定性。虽然可能有许多潜在的不确定性来源会阻碍我们管理自然资源的能力,但这里只能对其中的一部分进行有用的研究。一些不确定性来源会影响所收集数据的可变性,其他来源则会影响可用数据的类型。

6.1.1 不确定性类型

有多种方式来描述不同类型的不确定性,这些不确定性影响渔业模型参数估计以及如何使用这些估计算。这些一般被称为误差来源,通常是指残差误差而不是指已经犯下的错误。不幸的是,“误差”这一术语,如残差误差,有导致混淆的潜力,因此最好使用“不确定性”这一术语。虽然只要你知道这个问题,它对你来说不应该是个问题。

Francis & Shotton (1997) 列出了与自然资源评估和管理相关的六种不同类型的不确定性,而 Kell 等 (2005) 在遵循 Rosenberg 和 Restrepo (1994) 的基础上,将这些缩减为五种。或者,它们都可以归纳为四个标题下 (Haddon 2011),其中一些有子标题,如下所示:

  • 过程不确定性:种群动态率(如生长、年际平均补充量、年际自然死亡率)和其他生物学特性及过程中的潜在自然随机变化。

  • 观测不确定性:抽样误差和测量误差反映了样本旨在代表总体但仅是样本这一事实。数据收集不充分或非代表性会加剧观测不确定性,任何错误、数据的故意误报或未报告都会导致观测不确定性,这在渔业统计领域并不罕见。

  • 模型不确定性:与所选模型结构描述研究系统动力学的能力相关:

    • 不同的结构模型可能会提供不同的答案,并且对于哪个模型是自然界更好的表示存在不确定性。

    • 为给定过程选择残差误差结构是模型不确定性的一种特殊情况,这对参数估计具有重要影响。

    • 估计不确定性是模型结构中参数之间存在相互作用或相关性的一种结果,以至于略微不同的参数集可能导致对数似然值相同(在数值模型拟合中使用的精度范围内)。

  • 实施不确定性:管理行动的效果或范围可能与预期不同。定义不明确的管理选项可能导致实施不确定性。

    • 制度不确定性:管理目标定义不明确导致无法实施的管理。

    • 决策制定与实施之间的时间滞后可能导致更大差异。评估通常在渔业数据收集一年或更长时间后进行,而管理决策的执行往往又需要一年或更长时间。

我们这里特别关注过程、观测和模型的不确定性。每种不确定性都可能影响模型参数估计、模型结果和预测。实施不确定性更多是关于模型或模型的结果在管理自然资源时如何使用。这会对基于库存评估模型结果的不同管理策略的效率产生重要影响,但它并不影响这些即时结果(Dichmont 等,2006)。

模型不确定性可以是定量的,也可以是定性的。因此,使用完全相同的数据和残差结构的模型可以相互比较,并选择最佳拟合的模型。这些模型可以被认为既相关又不同。然而,当模型不相容时,例如,使用相同结构模型但采用不同的残差误差结构时,它们各自可以产生最佳拟合,模型选择必须基于除拟合质量之外的其他因素。这类模型之间不能平滑过渡,而是构成对所研究系统的定性和定量上不同的描述。模型不确定性是模型选择背后的驱动力之一(Burnham and Anderson, 2002)。即使只有一个模型被开发出来,它也往往是从许多可能的模型中隐含选择出来的。在特定情况下使用多种类型的模型(例如,同时使用过剩生产模型和完全年龄结构模型)往往能带来仅使用一种模型会错过的见解。 遗憾的是,随着用于种群评估建模的资源普遍减少,现在使用多个模型已成为一种日益罕见的选择。

模型和实施的不确定性在自然资源管理中都十分重要。我们已经比较了不同模型的输出结果,然而,在这里我们将重点介绍能够帮助我们表征在管理建议方面具有意义的不同模型参数估计值和其他模型输出结果的可接受置信度的方法。

针对任何参数估计或其他模型输出,存在多种策略来表征不确定性。一些方法侧重于数据问题,而另一些方法则侧重于在现有数据条件下,合理参数值的潜在分布。我们将考虑四种不同的方法:

  1. 自助法(bootstraping),关注数据样本中固有的不确定性,并通过检验如果采取略有不同的样本对参数估计的影响来运作。
  2. 渐近误差(asymptotic errors)使用参数估计之间的协方差矩阵来描述这些参数值周围的不确定性,
  3. 似然曲线(likelihood profiles)基于主要参数构建,以获得每个参数更具体的分布,最后,
  4. 贝叶斯边缘后验分布(Bayesian marginal characterize)表征模型参数和输出估计中固有的不确定性。

这些方法都允许对参数和模型输出的不确定性进行表征,并提供确定每个参数均值或期望值周围选定百分位数范围的方法(例如, \(\bar x \pm 90 \% CI\) 在某些情况下可能不对称)。

我们将使用一个简单的剩余生产模型来说明所有这些方法,尽管它们应该具有更广泛的适用性。

6.1.2 示例模型

该示例与第 4 章”模型参数估计“中关于使用对数正态似然拟合动态模型部分所描述的模型相同。我们将在本章的许多示例中使用该模型,其种群动态相对简单,如 公式 6.1 所示。

\[ \begin{split} B_{t=0} &= B_{init} \\ B_{t+1} &= B_t + r B_t \left(1-\dfrac{B_t}{K} \right) - C_t \end{split} \qquad(6.1)\]

其中 \(B_t\) 表示第 \(t\) 年的可利用生物量, \(B_{init}\) 为可利用数据第一年的生物量( \(t=0\) ),考虑了记录开始时的任何初始消耗。 \(r\) 是内在自然增长率(种群增长率项), \(K\) 是承载力或未捕捞的可利用生物量,在别处通常称为 \(B_0\) (不要与 \(B_{init}\) 混淆)。最后, \(C_t\) 是第 \(t\) 年的总捕捞量。为了将这些动态与除捕捞量以外的渔业观测结果联系起来,我们使用一个相对丰度指数( \(I_t\),通常是单位努力捕捞量或 cpue,但也可以是调查指数)。

\[ I_t = \dfrac{C_t}{E_t}=qB_t \quad \text{or} \quad C_t = qE_tB_t \qquad(6.2)\]

其中 \(I_t\) 是第 \(t\) 年的捕捞率(CPUE 或 cpue), \(E_t\) 是第 \(t\) 年的捕捞努力量,而 \(q\) 被称为可捕系数(它也可能随时间变化,但我们假设它是恒定的)。由于 \(q\) 仅将种群生物量与捕捞量进行比例缩放,我们将使用可捕系数的封闭形式估计方法来减少需要估计的参数数量(Polacheck 等,1993)。

\[ q=e^{\frac{1}{n}\sum\log \left(\frac{I_t}{\hat B_t}\right)} =\exp\left(\frac{1}{n}\sum \left(\frac{I_t}{\hat B_t} \right) \right) \qquad(6.3)\]

我们将使用 MQMF abdat 数据集拟合这个模型,然后在接下来的章节中检验该模型拟合的不确定性。我们将通过最小化基于对数正态分布的残差来拟合模型。简化后如下:

\[ -LL(y|\theta,\hat \sigma, I)= \frac{n}{2}(\log(2\pi)+2\log(\hat \sigma)+1) + \sum_{i =1}^n \log(I_t) \qquad(6.4)\]

其中 \(\theta\) 是参数向量 ( \(r\)\(K\)\(B_{init}\) ), \(I_t\) 是在年份 \(t\) 观测到的 cpue 值, \(\hat \sigma^2\) 的最大似然估计定义为:

\[ \hat \sigma^2 = \sum_{i=1}^n \dfrac{\left(\log(I_t)-\log(\hat I_t) \right)^2}{n} \qquad(6.5)\]

注意除以 \(n\) 而不是 \(n-1\)。由于唯一的非恒定值是 \(\log(\hat I_t)\) ,使用最大似然法得到的结果与使用最小二乘残差法得到的结果相同(只要观察到的和预测的 cpue 都经过对数转换)。然而,在分析不确定性时,使用最大似然法比使用平方和法具有优势。严格来说, \(\hat \sigma\) 值也是一个模型参数,但在这里我们特别处理它,只是为了说明与最小二乘方法的等价性。

代码
 #Fit a surplus production model to abdat fisheries data  
library(MQMF)
library(ggplot2)
library(knitr)

data(abdat); logce <- log(abdat$cpue)    
param <- log(c(0.42,9400,3400,0.05))   
label=c("r","K","Binit","sigma") # simpspm returns   
bestmod <- nlm(f=negLL,p=param,funk=simpspm,indat=abdat,logobs=logce)  
outfit(bestmod,title="SP-Model",parnames=label) #backtransforms  
nlm solution:  SP-Model 
minimum     :  -41.37511 
iterations  :  20 
code        :  2 >1 iterates in tolerance, probably solution 
             par      gradient   transpar
r     -0.9429555  6.743051e-06    0.38948
K      9.1191569 -9.223729e-05 9128.50173
Binit  8.1271026  1.059182e-04 3384.97779
sigma -3.1429030 -8.116218e-07    0.04316

模型拟合情况如 图 6.1 所示。Schaefer 模型的简单动力学似乎为这些观察到的鲍鱼捕捞率数据提供了一个合理的描述。实际上,在时间序列中,法律最小尺寸有所变化,渔业内部引入了区域划分,以及其他重要变化,因此这一结果最多只能被视为一种近似,并且只能被视为提供了一种方法论的示例。观察到/预测到的对数正态残差,如果乘以预测值,显然会得到观察值(灰线)。这里展示这一点,因为我们即将在自助法(bootstrap)数据时使用这种简单关系。

最大持续产量(MSY)可以从 Schaefer 模型中简单地估计为 \(\text{MSY} = rK/4\),这意味着这种最佳拟合表明存在 \(\text{MSY} = 888.842t\)。在接下来的各节中,我们将尝试回答的两个问题是:预测 cpue 在 公式 6.1 中观察数据周围的合理分布范围是多少?以及,平均 MSY 估计值周围的 90%置信区间是多少?

代码
 #plot the abdat data and the optimum sp-model fit  Fig 6.1  
predce <- exp(simpspm(bestmod$estimate,abdat))   
optresid <- abdat[,"cpue"]/predce #multiply by predce for obsce  
ymax <- getmax(c(predce,abdat$cpue))  
plot1(abdat$year,(predce*optresid),type="l",maxy=ymax,cex=0.9,  
      ylab="CPUE",xlab="Year",lwd=3,col="grey",lty=1)  
points(abdat$year,abdat$cpue,pch=1,col=1,cex=1.1)  
lines(abdat$year,predce,lwd=2,col=1)  # best fit line  
图 6.1: abdat 数据集的 Schaefer 剩余产量模型 的最佳拟合用在线性空间表示(红色实线)。灰线穿过数据点,以说明与预测线的差异。The optimum fit of the Schaefer surplus production model to the abdat data set plotted in linear-space (solid red-line). The grey line passes through the data points to clarify the difference with the predicted line.

6.2 自助法(Bootstrapping)

从总体中采集的数据被视为(假定)能够代表该总体以及预期样本值的潜在概率密度分布。这是一个非常重要的假设,最早由 Efron(1979)提出。他提出了这样一个问题:当样本包含或就是关于总体所有可用信息时,为什么不能假设样本真的就是总体,以估计检验统计量的抽样分布?因此,给定一个包含 \(n\) 个观测值的原始样本,自助样本将是从原始样本中有放回地抽取的 \(n\) 个观测值的随机样本。自助样本(即对样本数据值进行有放回的随机重抽样)被假定为近似那些通过反复抽样原始抽样总体所可能产生的值的分布。这些自助样本中的每一个都被视为来自原始总体的独立随机样本。这种有放回的重抽样对某些人来说似乎有违直觉,但可以用来拟合标准误差、百分位数置信区间以及进行假设检验。 据报道,“bootstrap”这个名字来源于故事《莫丘森男爵的冒险记》(The Adventures of Baron Munchausen),在这个故事中,男爵通过自己拉自己的靴带而逃脱溺水,从而从一口井中脱身(Efron and Tibshirani, 1993)。

Efron(1979)首次提出自助法作为一种实用方法,Bickel 和 Freedman (1981)则展示了自助法的渐近一致性(随着自助样本数量增加时的收敛行为)。基于这一展示,自助法已被应用于许多标准应用,例如多元回归(Freedman,1981;ter Braak,1992)和分层抽样( Bickel 和 Freedman,1984,他们发现了一个局限性)。Efron 最终将他曾在斯坦福大学为高年级学生讲授的材料整理成一份关于进展的综述(Efron 和 Tibshirani,1993)。

6.2.1 经验概率密度分布

假设给定一个来自总体的样本,非参数的最大似然估计值就是该样本本身。也就是说,如果样本包含 \(n\) 个观测值 \((x_1, x_2, x_3, \dots, x_n)\),那么对于 \(n\) 个观测值中的每个观测值 \(x_i\),其概率密度分布的最大似然非参数估计值就是将概率质量的 \(1/n\) 。必须强调的是,这并不意味着所有值都有相等的似然,而是意味着每个观测值出现的似然性相等,尽管可能有多个观测值具有相同的值(在进行下一步之前,请确保你清楚这个区别!)。如果被抽样的总体变量有一个众数值,那么我们期望,有时会得到与该众数附近相同或相似值的出现频率高于样本分布两端的值。

自助法(Bootstrapping)包括应用蒙特卡洛方法,即从原始样本本身中进行有放回抽样,仿佛它是一个理论统计分布(类似于正态分布、伽马分布和贝塔分布)。有放回抽样与本质上无限大的总体一致。因此,我们将样本视为代表整个总体。

总之,自助法用于估计参数值或模型输出的不确定性。这是通过汇总来自重复样本的自助法参数估计值来实现的,这些重复样本是通过用从原始样本中估计的样本替换真实总体样本而得到的。

6.3 简单自助法示例

为了了解如何在 R 中实现自助法,从简单的例子开始是明智的。澳大利亚北部对虾渔业,在卡奔塔里亚湾和约瑟夫·波拿巴湾之间,沿着大陆右上侧,是一个混合渔业,捕获多种对虾(Dichmont 等,2006;Robins 和 Somers,1994)。我们将使用 1970 年至 1992 年间捕获的虎虾(Penaeus semisulcatusP. esculentus)和基围虾(Metapenaeus endeavouriM. ensis)的例子。这些捕获量之间似乎存在相关性, 图 6.2 ,但数据大量分散。刀额新对虾在价值更高的虎虾渔业中总是作为副产品捕获,这反映在其相对捕获量上。

虾捕捞数据相对较为嘈杂,这在虾捕捞中并不意外。捕捞基围虾与虎虾捕捞的相关性也不应令人惊讶。基围虾通常被视为更有价值的虎虾渔业中的兼捕物,因此可以预期虎虾总捕捞量与捕捞努力虾总捕捞量之间存在某种关系。另一方面,如果你将香蕉虾捕捞量与虎虾捕捞量作图,则不应期望存在这种关系,因为这两个渔业几乎相互独立(大多在同一区域),一个在白天捕捞,另一个在夜间捕捞。在混合渔业中探索这种关系,往往能提出关于物种间或不同渔业间相互作用可能性的假设。

代码
 #regression between catches of NPF prawn species Fig 6.2  
data(npf)  
model <- lm(endeavour ~ tiger,data=npf)  
plot1(npf$tiger,npf$endeavour,type="p",xlab="Tiger Prawn (t)",  
      ylab="Endeavour Prawn (t)",cex=0.9)  
abline(model,col=1,lwd=2)  
correl <- sqrt(summary(model)$r.squared)  
pval <- summary(model)$coefficients[2,4]  
label <- paste0("Correlation ",round(correl,5)," P = ",round(pval,8))  
text(2700,180,label,cex=1.0,font=7,pos=4)  

# ggplot(data = npf, aes(x = tiger, y = endeavour)) +
#   geom_point() +
#   geom_smooth(method = "lm", se = FALSE) +
#   theme_bw() +
#   labs(x = "Tiger Prawn (t)", y = "Endeavour Prawn (t)")
图 6.2: 1970 - 1992 年间澳大利亚北部对虾渔业中基围虾和虎虾产量之间的正相关关系(数据来自 Robins 和 Somers,1994)。The positive correlation between the catches of endeavour and tiger prawns in the Australian Northern Prawn Fishery between 1970 - 1992 (data from Robins and Somers, 1994).

尽管回归高度显著( \(P< 1e^{-06}\)),但虾类捕捞量的变异性意味着我们难以确定可以有多大信心相信相关性。通常情况下,在相关系数周围估计置信区间并不直接,幸运的是,使用自助法(bootstrapping),这可以轻松完成。我们可以从原始数据集中取出 23 对数据,进行 5000 次自助法抽样,每次计算相关系数。完成后,我们可以计算平均值和不同的分位数。在这种情况下,我们不是对单个值进行自助法,而是对值对进行自助法;我们必须取值对以保持任何内在的相关性。在 R 中,可以通过首先对每个数据向量的位置进行有放回抽样,以确定每个自助法样本要取哪些对进行重新抽样。

代码
 # 5000 bootstrap estimates of correlation coefficient Fig 6.3  
set.seed(12321)     # better to use a less obvious seed, if at all  
N <- 5000                            # number of bootstrap samples  
result <- numeric(N)          #a vector to store 5000 correlations  
for (i in 1:N) {          #sample index from 1:23 with replacement  
   pick <- sample(1:23,23,replace=TRUE)   #sample is an R function  
   result[i] <- cor(npf$tiger[pick],npf$endeavour[pick])   
}  
rge <- range(result)                  # store the range of results  
CI <- quants(result)     # calculate quantiles; 90%CI = 5% and 95%  
restrim <- result[result > 0] #remove possible -ve values for plot  
parset(cex=1.0)        # set up a plot window and draw a histogram  
bins <- seq(trunc(range(restrim)[1]*10)/10,1.0,0.01)   
outh <- hist(restrim,breaks=bins,main="",col=0,xlab="Correlation")  
abline(v=c(correl,mean(result)),col=c(4,3),lwd=c(3,2),lty=c(1,2))  
abline(v=CI[c(2,4)],col=4,lwd=2) # and 90% confidence intervals  
text(0.48,400,makelabel("Range ",rge,sep="  ",sigdig=4),font=7,pos=4)  
label <- makelabel("90%CI ",CI[c(2,4)],sep="  ",sigdig=4)  
text(0.48,300,label,cex=1.0,font=7,pos=4)  
图 6.3: 5000次自助法估算的基围虾和老虎虾渔获量之间相关性,原始均值用绿色虚线表示,自助法均值和 90% CI 用蓝色实线表示。出于图示目的,已删除了可能的负相关(尽管没有出现)。

虽然相关性值的分布显然向左偏斜,但我们有信心原始数据中的高相关性是现有关系数据的合理表示。

6.4 自助法时间序列数据

尽管前一个例子中的对虾捕捞量跨越了数年,但数据到达的具体年份与其之间的相关性无关。因此,我们可以对数据对进行自助法处理,并继续分析。然而,在自助法处理物种种群动态信息时,必须保持数据的时间序列特性,其中某年的数值(数量或生物量)以某种方式依赖于前一年的数值。例如,在剩余产量模型中,观测数据进入分析的顺序是种群动态的一个关键方面,因此盲目地自助法处理数据对既不合适也不明智。我们采用的解决方案是:我们首先获得最佳模型拟合及其预测的 cpue 时间序列,然后对每个点的个别残差进行自助法处理。在每个循环中,自助法抽样的残差应用于最佳预测值,以生成一个新的自助法“观测”数据序列,然后重新拟合。 你会记得观测值可以从最优预测 cpue 值乘以对数正态残差得到, 图 6.1 。基于原始数据的最优解,特定年份 \(t\) 的对数正态残差为:

\[ \text{resid}_i = \dfrac{I_t}{\hat I_t} = \exp\left(\log(I_t)-\log(\hat I_t) \right) \qquad(6.6)\]

其中 \(I_t\) 指的是第 \(t\) 年观察到的单位捕捞努力量渔获量,而 \(\hat I_t\) 是第 \(t\) 年预测的最优单位捕捞努力量渔获量。一个最优解将意味着最优预测单位捕捞努力量渔获量的时间序列和相关的对数正态残差的时间序列。鉴于我们使用的是乘法对数正态残差,一旦我们对残差进行自助采样(有放回的随机样本),我们需要将最优预测单位捕捞努力量渔获量的时间序列乘以自助残差序列。对数正态残差预计将围绕 1.0 中心,较低值受零约束,较高值无约束;因此可能出现对数正态分布的偏斜。

\[ I_t^* = \hat I_t \times\left(\dfrac{I_t}{\hat I_t} \right)^* = \exp\left(\log(\hat I_t)+\left( \log(I_t)-\log(\hat I_t)\right)^*\right) \qquad(6.7)\]

其中上标 \(*\) 表示自助值,如 \(I_t^*\),或自助样本,如 \((I_t/\hat I_t)^*\)。如果我们使用简单的加性正态随机残差,那么我们会使用右边的方程,但不会进行对数转换和指数化。对于剩余产量模型,对数正态版本可以这样实现:

代码
 # fitting Schaefer model with log-normal residuals with 24 years   
data(abdat); logce <- log(abdat$cpue) # of abalone fisheries data  
param <- log(c(r= 0.42,K=9400,Binit=3400,sigma=0.05)) #log values  
bestmod <- nlm(f=negLL,p=param,funk=simpspm,indat=abdat,logobs=logce)  
optpar <- bestmod$estimate      # these are still log-transformed  
predce <- exp(simpspm(optpar,abdat))      #linear-scale pred cpue  
optres <- abdat[,"cpue"]/predce     # optimum log-normal residual  
optmsy <- exp(optpar[1])*exp(optpar[2])/4  
sampn <- length(optres)        # number of residuals and of years  
代码
result <- cbind(abdat, predce, optres)

knitr::kable(result, digits = 3)
表 6.1: abdat 数据集及相关的最佳预测 cpue(predce)和最佳残差(optres)。The abdat data-set with the associated optimum predicted cpue (predce), and the optimum residuals (optres).
year catch cpue predce optres
1985 1020 1.000 1.135 0.881
1986 743 1.096 1.071 1.023
1987 867 1.130 1.093 1.034
1988 724 1.147 1.076 1.066
1989 586 1.187 1.105 1.075
1990 532 1.202 1.183 1.016
1991 567 1.265 1.288 0.983
1992 609 1.320 1.388 0.951
1993 548 1.428 1.479 0.966
1994 498 1.477 1.593 0.927
1995 480 1.685 1.724 0.978
1996 424 1.920 1.856 1.034
1997 655 2.051 1.998 1.027
1998 494 2.124 2.049 1.037
1999 644 2.215 2.147 1.032
2000 960 2.253 2.180 1.033
2001 938 2.105 2.103 1.001
2002 911 2.082 2.044 1.018
2003 955 2.009 2.003 1.003
2004 936 1.923 1.952 0.985
2005 941 1.870 1.914 0.977
2006 954 1.878 1.878 1.000
2007 1027 1.850 1.840 1.005
2008 980 1.727 1.782 0.969

通常情况下,至少需要进行 1000 次自助采样。请注意,我们将 bootfish 设置为矩阵而不是数据框。如果你移除 as.matrix,使 bootfish 成为数据框,比较执行该操作所需的时间,并看到在计算密集型工作中使用矩阵的优势。

代码
 # 1000 bootstrap Schaefer model fits; takes a few seconds  
start <- Sys.time() # use of as.matrix faster than using data.frame  
bootfish <- as.matrix(abdat)  # and avoid altering original data  
N <- 1000;   years <- abdat[,"year"] # need N x years matrices  
columns <- c("r","K","Binit","sigma")   
results <- matrix(0,nrow=N,ncol=sampn,dimnames=list(1:N,years))  
bootcpue <- matrix(0,nrow=N,ncol=sampn,dimnames=list(1:N,years))  
parboot <- matrix(0,nrow=N,ncol=4,dimnames=list(1:N,columns))  
for (i in 1:N) {  # fit the models and save solutions  
  bootcpue[i,] <- predce * sample(optres, sampn, replace=TRUE)  
  bootfish[,"cpue"] <- bootcpue[i,] #calc and save bootcpue  
  bootmod <- nlm(f=negLL,p=optpar,funk=simpspm,indat=bootfish,  
        logobs=log(bootfish[,"cpue"])) 
  parboot[i,] <- exp(bootmod$estimate)  #now save parameters 
  results[i,] <- exp(simpspm(bootmod$estimate,abdat))  #and predce   
}  
cat("total time = ",Sys.time()-start, "seconds   \n")  
total time =  3.766865 seconds   

在我写这本书时使用的电脑上,bootstrap 运行大约需要 4 秒。这包括从样本中抽取 24 年的 bootstrap 样本,并将过剩生产模型拟合到样本上,每次迭代大约需要 0.004 秒。对于任何需要多次拟合模型或计算似然值的计算密集型方法,这都是值得了解的。了解分析运行所需的时间有助于设计这些分析的规模。过剩生产模型的拟合时间非常短,但如果一个复杂的年龄结构模型拟合需要大约 1 分钟,那么 1000 个复制(按现代标准来说,这是最低要求,而且更多的复制无疑会提供更精确的结果)将需要超过 16 个小时!在可能的情况下,优化代码速度仍然非常重要;当我们在本章后面讨论使用 Rcpp 包近似贝叶斯后验分布时,我们将讨论优化速度的方法。

引导样本通常可以绘制出来,以提供模型拟合质量的直观印象, 图 6.4 。可以将quants() 函数应用于包含最优预测 cpue 引导估计的结果矩阵,从而在图的灰色边界内绘制百分位数置信区间,但在这里结果非常紧密,这样做大多只会使图形显得杂乱。

1988 年和 1989 年的数值具有最大的残差,因此永远不会被超过。

代码
 # bootstrap replicates in grey behind main plot Fig 6.4  
plot1(abdat[,"year"],abdat[,"cpue"],type="n",xlab="Year",  
      ylab="CPUE") # type="n" just lays out an empty plot  
for (i in 1:N)      # ready to add the separate components  
  lines(abdat[,"year"],results[i,],lwd=1,col="grey")  
points(abdat[,"year"],abdat[,"cpue"],pch=16,cex=1.0,col=1)  
lines(abdat[,"year"],predce,lwd=2,col=1)  
图 6.4: 鲍鱼渔业 abdat 数据集最佳预测 cpue 的 1000 个自助估计值。黑点为原始数据,黑线为原始模型拟合的最佳预测 cpue,灰色轨迹为预测 cpue 的 1000 个自助估计值。1000 bootstrap estimates of the optimum predicted cpue from the abdat data set for an abalone fishery. Black points are the original data, the black line is the optimum predicted cpue from the original model fit, and the grey trajectories are the 1000 bootstrap estimates of the predicted cpue.

在 1000 次重复中,每个图中仍存在一些不够清晰的部分,尤其是在后期年份,因此必须小心,不能仅仅绘制灰色轨迹的轮廓(可能使用 chull() 定义),否则预测轨迹空间中相对较窄的区域可能会被忽略。通常,进行 2000-5000 次自助法抽样可能看起来有些过度,但要避免轨迹空间中的实际空白,这样的数量可以是有利的。这类图表有帮助,但主要发现与模型参数和输出相关,例如 MSY。我们可以使用 \(r\)\(K\) 的自助法估计来估计 MSY 的自助法估计,所有这些都可以绘制为直方图,并使用 quants() ,我们可以识别出我们想要的任何百分位数置信区间。 quants()默认提取 0.025、0.05、0.5、0.95 和 0.975 分位数(可以输入其他范围),允许识别出 95%和 90%的中心置信区间以及中位数。

代码
 #histograms of bootstrap parameters and model outputs Fig 6.5  
dohist <- function(invect,nmvar,bins=30,bootres,avpar) { #adhoc  
  hist(invect[,nmvar],breaks=bins,main="",xlab=nmvar,col=0)  
  abline(v=c(exp(avpar),bootres[pick,nmvar]),lwd=c(3,2,3,2),  
         col=c(3,4,4,4))  
}  
msy <- parboot[,"r"]*parboot[,"K"]/4 #calculate bootstrap MSY   
msyB <- quants(msy)        #from optimum bootstrap parameters  
parset(plots=c(2,2),cex=0.9)  
bootres <- apply(parboot,2,quants); pick <- c(2,3,4) #quantiles  
dohist(parboot,nmvar="r",bootres=bootres,avpar=optpar[1])  
dohist(parboot,nmvar="K",bootres=bootres,avpar=optpar[2])  
dohist(parboot,nmvar="Binit",bootres=bootres,avpar=optpar[3])  
hist(msy,breaks=30,main="",xlab="MSY",col=0)  
abline(v=c(optmsy,msyB[pick]),lwd=c(3,2,3,2),col=c(3,4,4,4))  
图 6.5: 前三个模型参数的 1000 个自助估计值和 MSY 的柱状图。在每幅图中,两条细外线定义了中位数周围的 90%置信区间,中间的垂直线表示最佳估计值,但这些估计值通常紧靠中位数下方,Binit 模型除外。The 1000 bootstrap estimates of each of the first three model parameters and MSY as histograms. In each plot the two fine outer lines define the inner 90% confidence bounds around the median, the central vertical line denotes the optimum estimates, but these are generally immediately below the medians, except for the Binit.

再次仅用 1000 个自助法估计,直方图并不能很好地表示所讨论的所有参数和输出值的经验分布。更多的重复将使输出平滑,并稳定分位数或百分位数的置信界限。即便如此,也可以对这类参数和输出值的生成精度有一个概念。

6.4.1 参数相关性

我们也可以通过使用 R 函数 pairs() 将每个参数和模型输出绘制在一起,来检查它们之间的任何相关性。 \(r\)\(K\)\(B_{init}\) 之间的强相关性立即显现出来。与 sigma 值缺乏相关性是残差内部变化应如何表现的一种典型情况。更有趣的是, \(msy\)(它是 \(r\)\(K\) 的函数)与其他参数的相关性有所降低。这种降低反映了 \(r\)\(K\) 之间的负相关性,这种负相关性会抵消彼此变化的影响。因此,对于相当不同的 \(r\)\(K\) 值,我们可以有相似的 \(msy\) 值。在 图 6.6 中使用 rgb() 函数来根据点的密度变化颜色强度,也可以在绘制 1000 条轨迹时使用,以识别图(6.4)中最常见的轨迹。

代码
 #relationships between parameters and MSY  Fig 6.6  
parboot1 <- cbind(parboot,msy)  
 # note rgb use, alpha allows for shading, try 1/15 or 1/10  
pairs(parboot1,pch=16,col=rgb(red=1,green=0,blue=0,alpha = 1/20))  
图 6.6: 用 Schaefer 模型拟合 abdat 数据集时,1000 个自助法估算的最佳参数估算值与得出的 MSY 值之间的关系。全色强度由至少 20 个点得出。更多的自助重复将使这些强度图更加完整。The relationships between the 1000 bootstrap estimates of the optimum parameter estimates and the derived MSY values for the Schaefer model fitted to the abdat data-set. Full colour intensity derived from a minimum of 20 points. More bootstrap replicates would fill out these intensity plots.

6.5 渐近误差

置信区间的概念(通常为 90% 或 95% CI)在 Snedecor 和 Cochran (1967, 1989) 以及许多其他文献中经典定义为:

\[ \bar x \pm t_{\nu} \dfrac{\sigma}{\sqrt{n}} \qquad(6.8)\]

其中 \(\bar x\)\(n\) 个观测样本(实际上,任何参数估计)的平均值, \(\sigma\) 是样本标准差,是样本变异性的度量, \(t_{\nu}\) 是具有 \(\nu = (n−1)\) 个自由度的 \(t\) 分布(另见rt()dt()pt()qt())。如果我们有多个独立样本,则可以估计样本均值组的标准差。 \(\sigma/\sqrt{n}\) 为变量 \(x\) 的标准误差,并且是当只有一个样本时估计样本均值集预期标准差的一种分析方法(生成多个样本的引导法是另一种方法,尽管在这种情况下,直接使用百分位数置信区间会更好)。由于我们处理的是正态分布数据,这种经典置信区间在均值或期望值周围对称分布。这在处理单个样本时是可行的,但我们要解决的问题是如何确定在将多参数模型拟合到数据时,我们可以以多大信心信任参数估计和模型输出。

在这种情况下,可以通过在最优参数集附近估计模型参数的协方差矩阵来产生渐近标准误差,如 公式 6.8 所示。在实践中,假设多参数模型在最优点的最大似然或平方和表面的梯度近似于多元正态分布,并可用于表征不同参数之间的关系。这些关系是生成所需协方差矩阵的基础。该矩阵用于生成每个参数的标准误差,如 公式 6.8 所示,然后这些标准误差可用于估计参数的近似置信区间。它们是近似的,因为这种方法假设在最优点附近的拟合表面是规则的和平滑的,并且表面在最优点附近近似于多元正态(=对称)分布。这意味着所得标准误差将在最优解周围对称,这可能适当也可能不适当。 然而,作为初步近似,渐近标准误差可以提供关于参数估计值周围固有变异的指示。

Hessian 矩阵描述了最大似然表面在最优值附近的局部曲率或梯度。更正式地说,它由描述不同参数集似然函数的二阶偏导数组成。也就是说,每个参数相对于自身和其他所有参数的变化率的变化率。所有 Hessian 矩阵都是方阵。如果我们只考虑 Schaefer 模型的四个参数中的 \(r\)\(k\)\(B_{init}\),即 公式 6.1 中的三个参数,那么描述这三个参数的 Hessian 矩阵将是:

\[ H(f) = \left\{ \begin{matrix} \frac{\partial^2f}{\partial r^2} & \frac{\partial^2 f}{\partial r \partial K} & \frac{\partial^2 f}{\partial r \partial B_{init}}\\ \frac{\partial^2f}{\partial r \partial K} & \frac{\partial^2f}{\partial K^2} & \frac{\partial^2 f}{\partial K \partial B_{init}} \\ \frac{\partial^2f}{\partial r\partial B_{init}} & \frac{\partial^2f}{\partial K \partial B_{init}} & \frac{\partial^2f}{\partial B_{init}^2} \end{matrix} \right\} \qquad(6.9)\]

如果我们正在计算二阶偏导数的函数 \(f\) 使用对数似然来将模型拟合到数据上,那么协方差矩阵是 Hessian 矩阵的逆。然而,请注意这一点,如果函数 \(f\) 使用最小二乘法进行数据分析,那么形式上协方差矩阵是最佳拟合处的残差方差与 Hessian 矩阵的逆的乘积。残差方差反映了估计参数的数量:

\[ Sx^2 = \dfrac{\sum(x-\hat x)^2}{n-p} \qquad(6.10)\]

这是观测值与预测值之间的平方偏差之和,除以观测次数( \(n\) )减去参数个数( \(p\) )。

因此,要么通过求 Hessian 矩阵的逆来估计方差-协方差矩阵( \(\mathbf{A}\)), \(\mathbf{A} = \mathbf{H}^{-1}\)(在使用最大似然时),要么在使用最小二乘法时,我们将 Hessian 矩阵的逆的元素乘以残差方差:

\[ \mathbf{A}= Sx^2\mathbf{H}^{-1} \qquad(6.11)\]

在这里我们将重点关注最大似然方法,但了解在使用最大似然方法和最小二乘法方法时的程序差异是值得的(建议在使用渐近误差时始终使用最大似然方法)。

\(\theta\) 向量中每个参数的标准误差估计是通过取方差-协方差矩阵的对角元素(方差)的平方根获得的:

\[ \text{StErr}(\theta) = \sqrt{\text{diag}(\mathbf{A})} \qquad(6.12)\]

在 Excel 中,我们使用有限差分法来估计 Hessian 矩阵 (Haddon 2011),但这种方法在参数强相关时并不总是表现良好。幸运的是,在 R 中,许多可用的非线性求解器在拟合模型时提供了自动生成 Hessian 矩阵估计值的选项。

代码
 #Fit Schaefer model and generate the Hessian  
data(abdat)  
param <- log(c(r= 0.42,K=9400,Binit=3400,sigma=0.05))   
 # Note inclusion of the option hessian=TRUE in nlm function  
bestmod <- nlm(f=negLL,p=param,funk=simpspm,indat=abdat,  
               logobs=log(abdat[,"cpue"]),hessian=TRUE)   
outfit(bestmod,backtran = TRUE) #try typing bestmod in console  
nlm solution:   
minimum     :  -41.37511 
iterations  :  20 
code        :  2 >1 iterates in tolerance, probably solution 
         par      gradient   transpar
1 -0.9429555  6.743051e-06    0.38948
2  9.1191569 -9.223729e-05 9128.50173
3  8.1271026  1.059182e-04 3384.97779
4 -3.1429030 -8.116218e-07    0.04316
hessian     : 
             [,1]         [,2]        [,3]       [,4]
[1,] 3542.8630966  2300.305473   447.63247 -0.3509662
[2,] 2300.3054728  4654.008776 -2786.59928 -4.2155105
[3,]  447.6324673 -2786.599276  3183.93947 -2.5662897
[4,]   -0.3509662    -4.215511    -2.56629 47.9905538
代码
 # Now generate the confidence intervals  
vcov <- solve(bestmod$hessian)      # solve inverts matrices  
sterr <- sqrt(diag(vcov)) #diag extracts diagonal from a matrix  
optpar <- bestmod$estimate      #use qt for t-distrib quantiles  
U95 <- optpar + qt(0.975,20)*sterr # 4 parameters hence  
L95 <- optpar - qt(0.975,20)*sterr # (24 - 4) df  
cat("\n               r      K     Binit    sigma \n")   

               r      K     Binit    sigma 
代码
cat("Upper 95% ",round(exp(U95),5),"\n") # backtransform  
Upper 95%  0.45025 10948.12 4063.59 0.05838 
代码
cat("Optimum   ",round(exp(optpar),5),"\n")#\n =linefeed in cat  
Optimum    0.38948 9128.502 3384.978 0.04316 
代码
cat("Lower 95% ",round(exp(L95),5),"\n")  
Lower 95%  0.33691 7611.311 2819.693 0.0319 

6.5.1 模型输出的不确定性

渐近标准误差也可以提供模型输出(如 MSY 估计)的近似置信区间,尽管这需要稍微不同的方法。这种方法基于一个假设,即关于最优解的对数似然曲面可以用多元正态分布来近似。这通常定义为:

\[ \text{Mult-Variate Normal} =N(\mu, \Sigma) \qquad(6.13)\]

其中,在本节讨论的情况下, \(\mu\) 是最优参数估计的向量(均值的向量),而 \(\Sigma\) 是最优参数向量的协方差矩阵。

一旦这些输入被估计,我们可以通过从具有最优参数估计值的均值和由逆 Hessian 估计的协方差矩阵的多变量正态分布中抽样来生成随机参数向量。这些随机参数向量可以像 bootstrap 参数向量一样使用,用来生成参数和模型输出的百分位数置信区间。使用多变量正态分布(有些人写作 multivariate normal),参数之间的相关性会自动被考虑在内。

6.5.2 从多元正态分布中取样

基础 R 没有用于处理多元正态分布的函数,但可以使用包含合适函数的几个 R 包。MASS 包(Venables 和 Ripley, 2002)包含一个合适的随机数生成器,而 mvtnorm 包则拥有更广泛的多元概率密度函数。这里我们将使用 mvtnorm

代码
 # Use multi-variate normal to generate percentile CI    Fig 6.7  
library(mvtnorm) # use RStudio, or install.packages("mvtnorm")  
N <- 1000 # number of multi-variate normal parameter vectors  
years <- abdat[,"year"];  sampn <- length(years)  # 24 years  
mvncpue <- matrix(0,nrow=N,ncol=sampn,dimnames=list(1:N,years))  
columns <- c("r","K","Binit","sigma")  
 # Fill parameter vectors with N vectors from rmvnorm  
mvnpar <- matrix(exp(rmvnorm(N,mean=optpar,sigma=vcov)),  
                 nrow=N,ncol=4,dimnames=list(1:N,columns))  
 # Calculate N cpue trajectories using simpspm  
for (i in 1:N) mvncpue[i,] <- exp(simpspm(log(mvnpar[i,]),abdat))  
msy <- mvnpar[,"r"]*mvnpar[,"K"]/4 #N MSY estimates   
 # plot data and trajectories from the N parameter vectors  
plot1(abdat[,"year"],abdat[,"cpue"],type="p",xlab="Year",  
      ylab="CPUE",cex=0.9)  
for (i in 1:N) lines(abdat[,"year"],mvncpue[i,],col="grey",lwd=1)  
points(abdat[,"year"],abdat[,"cpue"],pch=16,cex=1.0)#orig data  
lines(abdat[,"year"],exp(simpspm(optpar,abdat)),lwd=2,col=1)  
图 6.7: 从最优参数及其相关方差-协方差矩阵定义的多变量正态分布中采样的随机参数向量得出的 1000 条 cpue 预测轨迹。The 1000 predicted cpue trajectories derived from random parameter vectors sampled from the multi-variate Normal distribution defined by the optimum parameters and their related variance-covariance matrix.

和引导法示例一样,即使是 1000 个轨迹的样本也不足以完全填充轨迹空间,导致某些区域比其他区域稀疏。更多的重复实验可能会填补这些空白, 图 6.7 。在引导法 cpue 线中使用 rgb() 也有助于识别稀疏区域。

我们也可以使用 pairs() 绘制隐含参数相关性(如果有),就像我们对自助样本所做的那样, 图 6.8 。然而,在这里,相对于自助过程的结果, 图 6.6 ,结果似乎相对均匀且清晰分布。更平滑的分布反映了这些值都是从一个明确定义的概率密度函数中抽取的,而不是经验分布。有理由认为,自助过程将预期提供对数据特性的更准确表示。然而,哪个结果集更好地代表原始样本来源的总体却不太容易有信心地回答。真正重要的是它们的汇总统计数据是否不同,考虑到 表 6.2 ,我们可以看到,虽然每个参数和模型输出的分布细节略有不同,但似乎没有一个单一的规律表明其中一个比另一个更宽或更窄,且这种模式是一致的。

代码
 #correlations between parameters when using mvtnorm Fig 6.8  

pairs(cbind(mvnpar,msy),pch=16,col=rgb(red=1,0,0,alpha = 1/10)) 
图 6.8: 从估计的多变量正态分布中取样的 1000 个参数估计值(假定围绕最佳参数估计值)与 Schaefer 模型拟合的丰年鱼量值之间的关系。The relationships between the 1000 parameter estimates sampled from the estimated multi-variate Normal distribution assumed to surround the optimum parameter estimates and the derived MSY values for the Schaefer model fitted to the abdat data set.

最后,我们可以通过绘制参数和 MSY 的直方图数组及其预测的置信界限来展示渐近置信区间,如 图 6.9 所示。在这种情况下,我们使用内部 90%界限。均值和中位数比自助法示例更加接近,这进一步反映了使用多元正态分布的对称性。

代码
 #N parameter vectors from the multivariate normal Fig 6.9  
mvnres <- apply(mvnpar,2,quants)  # table of quantiles  
pick <- c(2,3,4)   # select rows for 5%, 50%, and 95%   
meanmsy <- mean(msy)     # optimum bootstrap parameters  
msymvn <- quants(msy)   # msy from mult-variate normal estimates  
  
plothist <- function(x,optp,label,resmvn) {  
  hist(x,breaks=30,main="",xlab=label,col=0)  
  abline(v=c(exp(optp),resmvn),lwd=c(3,2,3,2),col=c(3,4,4,4))   
} # repeated 4 times, so worthwhile writing a short function  
par(mfrow=c(2,2),mai=c(0.45,0.45,0.05,0.05),oma=c(0.0,0,0.0,0.0))   
par(cex=0.85, mgp=c(1.35,0.35,0), font.axis=7,font=7,font.lab=7)  
plothist(mvnpar[,"r"],optpar[1],"r",mvnres[pick,"r"])  
plothist(mvnpar[,"K"],optpar[2],"K",mvnres[pick,"K"])  
plothist(mvnpar[,"Binit"],optpar[3],"Binit",mvnres[pick,"Binit"])  
plothist(msy,meanmsy,"MSY",msymvn[pick])  
图 6.9: 从最优解估算的多变量正态中得出的 r、K、Binit 和推导的 MSY 的 1000 个参数估计直方图。在每幅图中,绿线表示算术平均值,蓝色粗线表示中位数,两条蓝色细线表示中位数周围 90%的置信区间。Histograms of the 1000 parameter estimates for r, K, Binit, and the derived MSY, from the multi-variate normal estimated at the optimum solution. In each plot, the green line denotes the arithmetic mean, the thick blue line the median, and the two fine blue lines the inner 90% confidence bounds around the median.
代码
knitr::kable(bootres, digits = 3)
knitr::kable(mvnres, digits = 3)
表 6.2: 自助百分位数参数置信区间与方差-协方差矩阵渐近估计值参数置信区间的比较。上表是自助法结果,下表是多元正态分布结果。A comparison of the bootstrap percentile confidence bounds on parameters with those derived from the Asymptotic estimate of the variance-covariance matrix. The top table relates to the bootstrapping and the bottom to the values from the multi-variate normal.
r K Binit sigma
2.5% 0.335 7740.636 2893.714 0.025
5% 0.345 8010.341 2970.524 0.026
50% 0.390 9116.193 3387.077 0.039
95% 0.435 10507.708 3889.824 0.050
97.5% 0.445 11003.649 4055.071 0.052
r K Binit sigma
2.5% 0.339 7717.875 2882.706 0.033
5% 0.347 7943.439 2945.264 0.034
50% 0.389 9145.146 3389.279 0.043
95% 0.437 10521.651 3900.034 0.055
97.5% 0.444 10879.571 4031.540 0.058

6.6 似然剖面

“似然剖面”(Likelihood profile)这个名字暗示了生成这些分析所使用的过程。在本章中,我们已经最优地拟合了一个四参数模型,并探讨了如何表征这些参数估计值周围的不确定性。可以想象从这四个参数中选择一个,并将其值固定在远离其最优值的位置。如果随后将所选参数固定,而允许其他参数变化,重新拟合模型,我们可以想象剩余参数会找到一个新的最优解,但负对数似然值会大于当所有四个参数都自由变化时获得的最优值。这个过程是生成似然曲线的基本思想。

目的是使用最大似然方法(最小化负对数似然)来拟合模型,但仅拟合特定参数,同时将其他参数保持在最优值周围的常数值。通过这种方式,可以在给定参数具有一系列固定值的情况下获得新的“最优”拟合。因此,我们可以确定固定参数对模型拟合总似然的影响。一个例子可以帮助使这个过程更加清晰。

再次使用 Schaefer 剩余生产模型对 abdat 渔业数据进行分析,我们得到最优参数 \(r = 0.3895\)\(K = 9128.5\)\(Binit = 3384.978\),以及 \(sigma = 0.04316\),这意味着 \(MSY = 888.842\) 吨。

代码
 #Fit the Schaefer surplus production model to abdat  
data(abdat); logce <- log(abdat$cpue)    # using negLL  
param <- log(c(r= 0.42,K=9400,Binit=3400,sigma=0.05))   
optmod <- nlm(f=negLL,p=param,funk=simpspm,indat=abdat,logobs=logce)  
outfit(optmod,parnames=c("r","K","Binit","sigma"))  
nlm solution:   
minimum     :  -41.37511 
iterations  :  20 
code        :  2 >1 iterates in tolerance, probably solution 
             par      gradient   transpar
r     -0.9429555  6.743051e-06    0.38948
K      9.1191569 -9.223729e-05 9128.50173
Binit  8.1271026  1.059182e-04 3384.97779
sigma -3.1429030 -8.116218e-07    0.04316

如何调整有限数量的模型参数,同时保持其余参数不变的问题,通过修改用于最小化负对数似然的功能得以解决。我们不是使用 negLL() 函数来计算对数正态分布的 cpue 值的负对数似然,而是使用了 MQMF 函数 negLLP() (负对数似然曲线)。这增加了固定某些参数的能力,同时通过仅改变非固定参数来求解最优解。如果我们查看 negLLP() 函数的 R 代码,并将其与 negLL() 函数进行比较,我们可以看到除了参数之外,重要区别在于 logpred 语句之前的三个代码行。请参阅帮助页面(?)以获取更多详细信息,尽管我希望你能立刻看出,如果你忽略 initparnotfixed 参数, negLLP() 应该会给出与 negLL() 相同的结果。

代码
 #the code for MQMF's negLLP function  
negLLP <- function(pars, funk, indat, logobs, initpar=pars,  
                   notfixed=c(1:length(pars)),...) {  
  usepar <- initpar  #copy the original parameters into usepar  
  usepar[notfixed] <- pars[notfixed] #change 'notfixed' values  
  npar <- length(usepar)   
  logpred <- funk(usepar,indat,...) #funk uses the usepar values  
  pick <- which(is.na(logobs))  # proceed as in negLL  
  if (length(pick) > 0) {  
    LL <- -sum(dnorm(logobs[-pick],logpred[-pick],exp(pars[npar]),  
                     log=T))  
  } else {  
    LL <- -sum(dnorm(logobs,logpred,exp(pars[npar]),log=T))  
  }  
  return(LL)  
} # end of negLLP  

例如,为了确定 \(r\) 参数的估计精度,我们可以强制它取 0.3 到 0.45 之间的常数值,同时将其他参数拟合以获得最佳拟合效果,然后绘制总似然与给定 \(r\) 值的函数关系图。与 R 中所有模型拟合一样,我们需要两个函数:一个用于生成所需的预测值,另一个作为包装器将观测值和预测值结合起来供最小化器使用,在此情况下为 nlm()。为了继续进行,我们使用 negLLP() 来允许某些参数保持固定值。 nlm() 通过迭代修改输入参数,朝着改善模型拟合的方向进行调整,然后将这些参数反馈到生成预测值的输入模型函数中,与观测值进行比较。因此,我们需要将模型函数安排为不断返回我们希望固定为初始设定值的特定参数值。 negLLP() 中的代码是完成这一操作的一种方法。 所需的更改包括在 initpar 中有一个独立的原始 pars 集合,该集合必须包含指定的固定参数,以及一个 notfixed 参数,用于识别从 initpar 中哪些值将被 pars 中的值覆盖,这些值将随后被 nlm 修改。

最佳实践是检查 negLLP() 产生的结果与使用 negLL() 相同,尽管代码检查让我们确信它会如此(我不再惊讶于自己会犯错),通过允许所有参数变化(默认设置)。

代码
 #does negLLP give same answers as negLL when no parameters fixed?  
param <- log(c(r= 0.42,K=9400,Binit=3400,sigma=0.05))   
bestmod <- nlm(f=negLLP,p=param,funk=simpspm,indat=abdat,logobs=logce)  
outfit(bestmod,parnames=c("r","K","Binit","sigma"))  
nlm solution:   
minimum     :  -41.37511 
iterations  :  20 
code        :  2 >1 iterates in tolerance, probably solution 
             par      gradient   transpar
r     -0.9429555  6.743051e-06    0.38948
K      9.1191569 -9.223729e-05 9128.50173
Binit  8.1271026  1.059182e-04 3384.97779
sigma -3.1429030 -8.116218e-07    0.04316

令人高兴的是,正如预期的那样,我们得到了相同的解,因此现在我们可以继续研究固定 \(r\) 的值并重新拟合模型的影响。通过后见之明(这比现实情况要好得多),我们选择了介于 0.325 和 0.45 之间的 \(r\) 值。我们可以设置一个循环来依次应用这些值并拟合相应的模型,将解保存在循环进行的过程中。下面我们列出了前几个结果, 表 6.3 ,以说明随着固定值 \(r\) 越来越远离其最优值,负对数似然如何增加。

代码
 #Likelihood profile for r values 0.325 to 0.45  
rval <- seq(0.325,0.45,0.001)  # set up the test sequence  
ntrial <- length(rval)        # create storage for the results  
columns <- c("r","K","Binit","sigma","-veLL")  
result <- matrix(0,nrow=ntrial,ncol=length(columns),  
                 dimnames=list(rval,columns))# close to optimum  
bestest <- c(r= 0.32,K=11000,Binit=4000,sigma=0.05)   
for (i in 1:ntrial) {  #i <- 1  
  param <- log(c(rval[i],bestest[2:4])) #recycle bestest values  
  parinit <- param  #to improve the stability of nlm as r changes         
  bestmodP <- nlm(f=negLLP,p=param,funk=simpspm,initpar=parinit,   
                  indat=abdat,logobs=log(abdat$cpue),notfixed=c(2:4),  
                  typsize=magnitude(param),iterlim=1000)
  bestest <- exp(bestmodP$estimate)       
  result[i,] <- c(bestest,bestmodP$minimum)  # store each result  
}  
minLL <- min(result[,"-veLL"]) #minimum across r values used.  
代码
knitr::kable(result[1:12,], digits = 3)
表 6.3: nlm 解决方案 126 行中的前 12 条记录用于绘制 r 的似然曲线。The first 12 records from the 126 rows of the nlm solutions that are used to make the likelihood profile on r. The strong correlation between r, K, and Binit is, once again, apparent.
r K Binit sigma -veLL
0.325 0.325 11449.17 4240.797 0.048 -38.618
0.326 0.326 11403.51 4223.866 0.048 -38.696
0.327 0.327 11358.24 4207.082 0.048 -38.772
0.328 0.328 11313.35 4190.442 0.048 -38.848
0.329 0.329 11268.83 4173.945 0.048 -38.922
0.33 0.330 11224.69 4157.589 0.048 -38.996
0.331 0.331 11180.91 4141.373 0.048 -39.070
0.332 0.332 11137.49 4125.293 0.047 -39.142
0.333 0.333 11094.43 4109.350 0.047 -39.213
0.334 0.334 11051.72 4093.540 0.047 -39.284
0.335 0.335 11009.11 4077.752 0.047 -39.354
0.336 0.336 10967.34 4062.316 0.047 -39.422

6.6.1 基于似然比的置信区间

Venzon 和 Moolgavkar(1988)描述了一种获取他们称之为“近似似然比置信区间”的方法,该方法基于对通常用于似然比检验方法的重新排列。该方法依赖于这样一个事实:随着样本量的增大,似然比检验渐近地趋近于 \(\chi^2\) 分布,因此这种方法只是近似的。毫不奇怪,似然比检验基于两个似然值的比率,或者如果处理的是对数似然值,则是从一个中减去另一个:

\[ \dfrac{L(\theta)_{max}}{L(\theta)} = e^{LL(\theta)_{max}-LL(\theta)} \qquad(6.14)\]

其中 \(L(\theta)\)\(\theta\) 参数的似然值, \(max\) 下标表示最大似然(假设所有其他参数也进行了最佳拟合), \(LL(\theta)\) 是等效的对数似然值。对于单个参数的实际置信区间,假设其他参数保持最优(如在似然轮廓中),预期的对数似然值由以下公式给出(Venzon 和 Moolgavkar,1988):

\[ \begin{split} & 2\times [LL(\theta)_{max}-LL(\theta)]\leq \chi_{1, 1-\alpha}^2 \\ & LL(\theta) = LL(\theta)_{max}-\dfrac{\chi^2_{1,1-\alpha}}{2} \end{split} \qquad(6.15)\]

其中 \(\chi^2_{1, 1-\alpha}\)\(\chi^2\) 分布的第 \(1-\alpha\) 分位数,自由度为1(例如,对于 95% 置信区间, \(\alpha= 0.95\)\(1-\alpha = 0.05\),以及 \(\chi^2_{1, 1-\alpha}= 3.84\)

对于单个参数 \(\theta_i\),其95%的近似置信区间的求解可表述为:找到所有满足以下条件的 \(\theta_i\) 值,该参数对应的对数似然与全局最优对数似然之差的 2 倍小于或等于 3.84( \(\chi^2_{1, 1-\alpha}= 3.84\)),或者( 公式 6.15 的最后一行),也可以通过搜索满足对数似然值等于最大对数似然减去所需 \(\chi^2\) 值一半的 \(\theta_i\)(即,当自由度为 1 时,计算 \(LL(\theta)_{max} -1.92\))。若构建双参数似然曲面时,则需在 \(\chi^2\) 值设定为 5.99(自由度为2)的条件下搜索 \(\theta_i\) ,此时应从最大似然值中减去 2.995(对于更高的自由度以此类推)。

我们可以绘制 \(r\) 参数的似然剖面,并包含这些近似 95% 置信区间。检查函数 plotprofile() 中的代码,以了解从分析结果计算置信区间的步骤。

代码
 #likelihood profile on r from the Schaefer model Fig 6.10  
plotprofile(result,var="r",lwd=2)  # review the code   
图 6.10: Schaefer 剩余产量模型对 abdat 数据集拟合得到的 r 参数的似然曲线。水平实线是最小值和最小值减去 1.92(95% 水平,自由度为 1,见正文)。外部的垂直线是围绕中心均值 0.389 的近似 95% 置信区间。

我们可以对 Schaefer 模型中的其他参数重复此分析。例如,我们可以使用几乎相同的代码以类似的方式对 \(K\) 参数进行类似分析。请注意, \(K\) 参数的剖面图中呈现出更明显的不对称性。虽然 \(r\) 参数的似然剖面也存在轻微不对称(从均值估计中减去 95%置信区间),但对于 \(K\) 参数,这种不对称性肉眼可见。 \(K\) 的最优参数估计为 9128.5,但剖面数据中的最大似然值指向 9130。这仅仅是似然剖面步长的反映。当前它设置为 10,如果设置为 1,我们可以得到更接近的近似值,当然,分析运行时间会延长 10 倍。

代码
 #Likelihood profile for K values 7200 to 12000  
Kval <- seq(7200,12000,10)  
ntrial <- length(Kval)  
columns <- c("r","K","Binit","sigma","-veLL")  
resultK <- matrix(0,nrow=ntrial,ncol=length(columns),  
                 dimnames=list(Kval,columns))  
bestest <- c(r= 0.45,K=7500,Binit=2800,sigma=0.05)   
for (i in 1:ntrial) { 
  param <- log(c(bestest[1],Kval[i],bestest[c(3,4)]))   
  parinit <- param  
  bestmodP <- nlm(f=negLLP,p=param,funk=simpspm,initpar=parinit,  
                indat=abdat,logobs=log(abdat$cpue),  
                notfixed=c(1,3,4),iterlim=1000)  
  bestest <- exp(bestmodP$estimate)  
  resultK[i,] <- c(bestest,bestmodP$minimum)  
}  
minLLK <- min(resultK[,"-veLL"])  
 #kable(head(result,12),digits=c(4,3,3,4,5))  # if wanted.  
代码
 #likelihood profile on K from the Schaefer model Fig 6.11  
plotprofile(resultK,var="K",lwd=2)  
图 6.11: Schaefer 剩余产量模型对 abdat 数据集的 K 参数的似然曲线,与 r 参数的处理方式相同。红线是最小值和最小值加 1.92(卡方分布 1 自由度的 95% 水平,见正文)。垂直粗线是围绕 9128.5 均值的近似 95% 置信区间。

6.6.2 负对数似然或似然

我们已经绘制了负对数似然图,例如 图 6.11 ,在以对数空间操作时这些图是合适的,但很少有人对对数空间有清晰的理解。另一种方法,可能更容易理解百分位数置信区间背后的原理,是将负对数似然转换回简单的似然值。当然,在对数值 -41 转换回原始值时表示一个非常小的数,但我们可以在确保所有似然值之和为 1.0 的过程中重新调整这些值。为了将负对数似然转换回原始值,我们需要先取其相反数,然后进行指数运算。

\[ L= \exp(-(-veLL) \qquad(6.16)\]

因此,对于应用于 \(K\) 参数的似然曲线,我们可以看到当 \(K\) 的值接近最优值时,线性空间的似然值开始增加,如果我们绘制这些似然值,分布的形状可以更直观地理解。尾部保持在零以上,但远离 95%置信区间:

代码
 #translate -velog-likelihoods into likelihoods  
likes <- exp(-resultK[,"-veLL"])/sum(exp(-resultK[,"-veLL"]),  
                                     na.rm=TRUE)  
resK <- cbind(resultK,likes,cumlike=cumsum(likes))  
代码
knitr::kable(resK[1:8,], digits = 5)
表 6.4: 用于制作 K 的似然轮廓的 nlm 解的前 8 条记录,共481行。包括反转换后的负对数似然值(缩放到总和为 1.0)及其运行累积和。
r K Binit sigma -veLL likes cumlike
7200 0.47314 7200 2689.875 0.05158 -37.09799 7e-05 0.00007
7210 0.47257 7210 2693.444 0.05147 -37.14518 7e-05 0.00014
7220 0.47201 7220 2697.023 0.05137 -37.19213 8e-05 0.00022
7230 0.47145 7230 2700.602 0.05127 -37.23881 8e-05 0.00030
7240 0.47089 7240 2704.182 0.05118 -37.28524 8e-05 0.00038
7250 0.47033 7250 2707.762 0.05108 -37.33141 9e-05 0.00047
7260 0.46977 7260 2711.341 0.05098 -37.37732 9e-05 0.00056
7270 0.46922 7270 2714.933 0.05088 -37.42298 1e-04 0.00065
代码
 #K parameter likelihood profile  Fig 6.12     
 
oldp <- plot1(resK[,"K"],resK[,"likes"],xlab="K value",     
              ylab="Likelihood",lwd=2)     
lower <- which.closest(0.025,resK[,"cumlike"])     
mid <- which(resK[,"likes"] == max(resK[,"likes"]))     
upper <- which.closest(0.975,resK[,"cumlike"])     
abline(v=c(resK[c(lower,mid,upper),"K"]),col=1,lwd=c(1,2,1))     
label <- makelabel("",resK[c(lower,mid,upper),"K"],sep="  ")     
text(9500,0.005,label,cex=1.2,pos=4)    
par(oldp)  # return par to old settings; this line not in book   
图 6.12: Schaefer 剩余产量模型对 abdat 数据集拟合的 K 参数似然曲线。在这种情况下,负对数似然值已被反转为似然值并缩放到总和为 1.0。垂直线是围绕均值的近似 95% 置信界限。最上面的三个数字是界限和估计的最优值。

6.6.3 模型输出中的分位数似然剖面

通常,对种群评估模型兴趣的关联在于那些并非直接作为参数估计的模型输出。如我们所见,在参数估计周围生成置信区间相对直接,但如何为模型输出(如 MSY(对于 Schaefer 模型 \(MSY = rK/4\)))提供类似的关于不确定性的估计呢?例如,一个评估模型可能估计种群生物量,或最大持续产量,或某种其他可视为模型间接输出的性能指标。通过在负对数似然中添加一个惩罚项来产生此类模型输出的似然曲线,该惩罚项试图将似然约束到最优目标(参见 公式 6.17 )。通过这种方式,偏离最优值的对数似然的影响可以被表征。

\[ -veLL = -veLL-w\left(\dfrac{\text{output}- \text{target}}{\text{target}} \right)^2 \qquad(6.17)\]

其中 \(-veLL\) 是负对数似然,output 是目标变量(在接下来的示例中,这是 MSY),target 是该变量的最优值(来自整体最优解的 MSY),而 \(w\) 是一个权重因子。权重因子应尽可能大,以生成最窄的似然曲线,同时仍能收敛到解。下面,我们描述一个专门用于处理模型输出周围似然曲线的函数 negLLO() (这不在 MQMF 中)。它只是一个修改版的 negLL() 函数,允许引入 公式 6.17 中描述的权重因子。我们可以通过检查 887.729 吨最优 MSY 值周围的似然曲线来举例

说明这一点,检查范围可以是 740 至 1050 吨,权重为 900。

代码
 #examine effect on -veLL of MSY values from 740 - 1050t  
 #need a different negLLP() function, negLLO(): O for output.  
 #now optvar=888.831 (rK/4), the optimum MSY, varval ranges 740-1050   
 #and wght is the weighting to give to the penalty  
negLLO <- function(pars,funk,indat,logobs,wght,optvar,varval) {  
  logpred <- funk(pars,indat)  
  LL <- -sum(dnorm(logobs,logpred,exp(tail(pars,1)),log=T)) +  
             wght*((varval - optvar)/optvar)^2  #compare with negLL  
  return(LL)  
} # end of negLLO  
msyP <- seq(740,1020,2.5);   
optmsy <- exp(optmod$estimate[1])*exp(optmod$estimate[2])/4  
ntrial <- length(msyP)  
wait <- 400  
columns <- c("r","K","Binit","sigma","-veLL","MSY","pen",  
             "TrialMSY")  
resultO <- matrix(0,nrow=ntrial,ncol=length(columns),  
                  dimnames=list(msyP,columns))  
bestest <- c(r= 0.47,K=7300,Binit=2700,sigma=0.05)   
for (i in 1:ntrial) {  # i <- 1  
  param <- log(bestest)   
  bestmodO <- nlm(f=negLLO,p=param,funk=simpspm,indat=abdat,  
                  logobs=log(abdat$cpue),wght=wait,  
                  optvar=optmsy,varval=msyP[i],iterlim=1000)  
  bestest <- exp(bestmodO$estimate)  
  ans <- c(bestest,bestmodO$minimum,bestest[1]*bestest[2]/4,  
           wait *((msyP[i] - optmsy)/optmsy)^2,msyP[i])  
  resultO[i,] <- ans  
}  
minLLO <- min(resultO[,"-veLL"])  
代码
 #tabulate first and last few records of profile on MSY     
 
kable(head(resultO[,1:7],4),digits=c(3,3,3,4,2,3,2))     
kable(tail(resultO[,1:7],4),digits=c(3,3,3,4,2,3,2))     
表 6.5: 用于制作 MSY 似然曲线的 nlm 解中前 113 行的前 7 条和最后 7 条记录(可以更多)。行名是试验的 MSY 值,pen 是惩罚值。
r K Binit sigma -veLL MSY pen
740 0.389 9130.914 3385.871 0.0432 -30.16 888.883 11.22
742.5 0.389 9130.911 3385.872 0.0432 -30.53 888.883 10.84
745 0.389 9130.911 3385.872 0.0432 -30.90 888.883 10.47
747.5 0.389 9130.911 3385.872 0.0432 -31.26 888.883 10.11
r K Binit sigma -veLL MSY pen
1012.5 0.389 9130.911 3385.872 0.0432 -33.63 888.883 7.74
1015 0.389 9130.911 3385.872 0.0432 -33.32 888.883 8.06
1017.5 0.389 9130.911 3385.872 0.0432 -32.99 888.883 8.38
1020 0.389 9130.911 3385.872 0.0432 -32.66 888.883 8.71
代码
 #likelihood profile on MSY from the Schaefer model Fig 6.13     
 
plotprofile(resultO,var="TrialMSY",lwd=2)  
图 6.13: 用 Schaefer 剩余生产模型拟合 abdat 数据集得到的 MSY 的似然曲线。水平红色线是最小值和最小值加 1.92(卡方分布 1 自由度的 95% 水平,见正文)。垂直线是围绕 887.729 吨均值的近似 95% 置信区间。

不幸的是,确定我们称之为 negLLO() 的惩罚项中的最佳权重,只能通过经验(试错法)来完成。我建议使用 900 的权重,因为我已经发现在这个水平上 95%置信区间(CI)趋于稳定。但那需要我从 100 尝试到 700,以 100 为步长,然后再以 50 为步长来发现这一点。你应该尝试使用 500、700、800、900 和 950 的权重值,以观察它们收敛到稳定值的过程。关于允许稳定解的最大权重的建议仍然是一点模糊的指导。这使得这种方法在可重复应用方面可能最为棘手。如果你尝试使用 wght = 400 来分析上述内容,那么 95% CI 将变为 825 - 950,而不是 847.5 - 927.5。在这种情况下,没有确定性解,因此它变成了尝试不同的权重并寻找一个可重复且一致的解的问题。

使用此似然轮廓方法获得的 95%置信区间(CI)与使用自助法(856.7 - 927.5)和使用渐近误差(849.7 - 927.4)获得的置信区间相当。每种方法根据所选样本量可能会产生略有不同的结果。然而,这些方法之间的一致性表明它们各自都能合理地描述该模型与这些数据组合中固有的变异。

6.7 贝叶斯后验分布

用模型参数拟合观测数据可以类比为在模型和给定数据集所隐含的多维似然表面上寻找最大似然的位置。如果似然表面在最优附近非常陡峭,那么与这些参数相关的不确定性就会相对较小。相反,如果似然表面在最优附近相对平坦,这意味着可以从相当不同的参数值中获得相似的似然,因此这些参数及其相关模型输出的不确定性预计会相对较高。如果存在强参数相关性,也可以提出类似的论点。我们忽略了在更复杂的模型中处理高度多维参数空间的复杂性,因为陡峭性和表面的几何概念仍然适用于多维似然。

如我们所见,如果我们假设在最优值附近似然面是多元正态分布的,那么我们可以使用渐近标准误差来定义参数估计值的置信区间。然而,对于渔业中的许多变量和模型输出,这可能是一个非常强的假设。例如,Schaefer \(K\) 参数的估计似然面相对偏斜,如 图 6.12 所示。理想情况下,我们应该使用能够独立于任何预先设定的概率密度函数来表征最优解附近似然面的方法。如果我们能够做到这一点,我们就可以使用百分位数方法的等效方法来提供参数和模型输出置信区间的估计。

通过形式化的似然剖面分析,我们可以在参数空间中进行搜索,以构建二维似然曲面。然而,对于超过两个参数的形式似然剖面分析,或许使用这样的网格搜索会非常笨拙,并且随着参数数量的进一步增加,会越来越不切实际。我们需要的是一种能够同时整合多个维度以生成类似多维似然剖面的方法。事实上,有几种方法可以实现这一点,包括抽样重要性采样(SIR;参见 McAllister 和 Ianelli,1997;以及 McAllister 等,1994),以及马尔可夫链蒙特卡罗(MCMC;参见 Gelman 等,2013)。在接下来的示例中,我们将实现 MCMC 方法。有许多替代算法可以用于执行 MCMC,但我们将专注于一种相对灵活的方法,称为 Gibbs-within-Metropolis 采样器(有时也称为 Metropolis-Hastings-within-Gibbs)。Metropolis 算法(Metropolis 等,1953 )最初是从二维积分开始的,后来由 Hastings(1970)推广,因此称为 Metropolis-Hastings。 在文献中,你会找到关于马尔可夫链蒙特卡洛和蒙特卡洛马尔可夫链的提及。前者是渔业标准参考(Gelman 等,2013)中使用的,尽管我个人认为用蒙特卡洛方法生成马尔可夫链的想法更直观明显。尽管存在这种潜在的混淆,我建议在写作时坚持使用 MCMC,如果必要的话,可以忽略我的直觉,使用马尔可夫链蒙特卡洛。这些事情并不重要到需要花时间担心,除了有时这些差异确实有意义(这种混淆仍然是一个问题,但只要你知道这些陷阱,就可以避免它们!)。

显然,MCMC(马尔可夫链蒙特卡洛方法)使用马尔可夫链来遍历多维似然曲面。马尔可夫链描述了一个过程,其中每个状态都是根据前一个状态以概率方式确定的。随机游走可以构成马尔可夫链的一种形式,其最终状态将是对随机分布的描述。然而,这里的目的是生成一个马尔可夫链,其最终平衡状态,即所谓的平稳分布,能够提供贝叶斯统计中目标或后验分布的描述。

马尔可夫链以某些参数值组合、可用的观测数据和所使用的模型为起点,这些共同定义了似然空间中的一个位置。根据参数值的不同,似然显然可以是小也可以是大(以模型的最大似然为上限)。MCMC 过程涉及根据每个新候选参数集相对于前一集的似然,遵循一套规则在参数空间中迭代地逐步前进,以确定哪些步骤将成为马尔可夫链的一部分。每次迭代中做出的决策是哪些参数向量将被接受,哪些被拒绝?该过程每一步都涉及生成一个新的候选参数值集,这是通过随机方式完成的(因此称为马尔可夫链蒙特卡罗),在 Gibbs-within-Metropolis 中,一次一个参数(Casella 和 George,1992;Millar 和 Meyer,2000)。这些新的候选参数集,结合可用的数据和模型,定义了新的似然。 这个新的参数组合是否被接受为马尔可夫链的下一步,取决于似然值的变化程度。在所有似然值增加的情况下,这一步会被接受,这似乎是合理的。现在来到关键部分,当似然值减少时,如果新似然值与旧似然值的比率大于某个均匀随机数(介于 0 和 1 之间;见下方方程式),这一步仍然可以被接受。

存在另一个与参数相关性和蒙特卡洛过程可能导致连续参数集自相关相关约束的问题。如果参数集之间存在显著的序列相关性,这可能会对参数空间中变化的全面程度产生有偏见的结论。所采用的解决方案是对结果链进行稀疏化处理,以便最终马尔可夫链中只包含每个 \(n^{th}\) 点。关键在于选择这种稀疏化率,使得 \(n^{th}\) 参数向量之间的序列相关性足够小,以至于不再影响总体方差。在实践示例中,我们将探讨这种稀疏化率。

6.7.1 生成马尔可夫链

如果给定一组数据 \(x\),可以定义特定模型参数集 \(\theta_t\) 的似然性,并且已知参数集的贝叶斯先验概率 \(L(\theta_t)\),那么就可以生成一个马尔可夫链:

\[ L_t= L(\theta_t|x)\times(L(\theta_t) \qquad(6.18)\]

我们可以通过随机增加 \(\theta^C=\theta_t + \Delta \theta_t\) 中的至少一个参数来生成一个新的试验或候选参数集 \(\theta^C\),这将改变隐含的似然性:

\[ L^C = L(\theta^C |x) \times L(\theta_t) \qquad(6.19)\]

如果 \(L^C\)\(L_t\) 的比值大于 1,那么从 \(\theta_t\)\(\theta^C\) 的跃迁就会被接受到马尔可夫链中( \(\theta_{t+1} = \theta^C\) )。

\[ r = \dfrac{L^C}{L_t}= \dfrac{L(\theta^C|x) \times L(\theta_t)}{L(\theta_t |x) \times L(\theta_t)} > 1.0 \qquad(6.20)\]

或者,如果比率 ( \(r\) ) 小于 1,那么只有当比率 \(r\) 大于一个新选择的均匀随机数时,跳跃才会被接受。如果未被接受,则 \(\theta_{t+1}\) 恢复为 \(\theta_t\),并开始一个新的候选周期:

\[ \theta_{t+1} = \left\{ \begin{matrix} \theta^C & \text{if }[L^C/ L_t >U(0,1)] \\ \theta_t & \text{otherwise} \end{matrix} \right. \qquad(6.21)\]

事实上,因为 0 和 1 之间的最大均匀随机数是 1,所以 公式 6.20 并非严格必要。我们可以直接估计比率并使用 公式 6.21 (这就是函数 do_MCMC() 的实现方式)。如果候选参数向量被拒绝,它会恢复为原始值,并使用新的候选参数集重新开始周期。随着马尔可夫链的发展,它应该在参数空间中描绘出一个多维体积。经过足够多的迭代后,它应该收敛到平稳分布。在对这些理论细节进行一些扩展后,我们将通过一些实例来阐述所有这些概念。

6.7.2 起始点

开始马尔可夫链只需选择一个参数向量。一个解决方案是选择一个接近但并不完全等同于最大似然最优解的向量。Gelman 和 Rubin(1992)建议从一个类似于预期分布的超分散分布中抽取样本,但这假设你已经有了一个关于应该从哪个超分散分布开始的概念,而选择这一点仍然有点像是一种艺术形式(Racine-Poon,1992)。正如 Racine-Poon(1992)进一步指出的:“然而,目标分布通常是多峰的,特别是在高维问题中,我对自动化这个过程不太自信。”但是,所使用的起始点可能会对结果产生影响,因此 Gelman 和 Rubin(1992)建议从非常不同的起始点开始多个链(而不是仅仅一个很长的链;Geyer,1992),然后确保它们都收敛到相同的最终分布。 所有这些建议都源于 20 世纪 90 年代初,当时计算机的速度远慢于现在,因此,在多条链或多条非常长的链上进行的讨论(例如,参见《统计科学》第 7 卷第 4 期的大部分内容)已不再是问题,也没有真正理由不运行多条非常长的链(尽管使用最高效的软件仍然是有道理的,因为 MCMCs 通常需要大量时间投入)。

6.7.3 预烧期

与从后验分布的多维模式可能相距较远的点开始马尔可夫链的一个重要点是,生成的第一个点序列预计会以随机方式向更高似然区域移动。然而,这些早期点可能会给本应是最终可接受参数向量云添加笨拙和不适当的尾部。因此,标准做法是简单地删除前 \(m\) 个点,其中 \(m\) 被称为燃烧期。Gelman 等(2013 )建议将每个链的前半部分(50%)作为燃烧期删除,但在他们讨论的是只有几百步长的链,所以在渔业中处理更高维的问题时,我们不必如此激进。几百个早期点甚至更少通常就足够了,特别是如果起始点离最大似然估计不远的话。但初步探索可能生成的链类型应该能够为每个问题找到一个合理的燃烧期。

6.7.4 收敛至稳定分布

20 世纪 90 年代关于应该有多少条链以及它们应该有多长的讨论,是由需要证明生成的马尔可夫链已经收敛到一个稳定解(在处理贝叶斯统计时,是指平稳分布或后验分布)的需求驱动的。Gelman 和 Rubin(1992)以及 Geyer(1992)都提出了用于确定收敛证据的经验方法。这些方法通常围绕比较链内的方差与链间的方差,或者马尔可夫链的一个部分的方差与同一链的另一个部分的方差。一种直观的方法是将多个链或链的部分的重要参数的边缘分布叠在一起绘制。如果它们匹配得足够好,就可以假设已经达到了收敛,尽管似乎没有人能确切定义什么程度可以被视为足够。由于这些方法都是经验性的,因此它们不能保证收敛是完整的,但迄今为止,还没有发现更多的分析方法。 在这里,我们将重点关注多个链的简单比较统计量(均值、中位数、分位数和方差)以及边缘分布图,但存在多种此类诊断方法(Geweke, 1989; Gelman and Rubin, 1992; Geyer, 1992)。R 包 coda 也实现了许多诊断工具,并可以使用简单的语句(如 post = coda::mcmc(posterior) )导入我们即将生成的简单矩阵输出。然而,在这里,我们将继续使用显式的 R 进行制表和绘图,以便读者能够轻松地了解其机制。是否使用像 coda 这样已经非常出色的包,还是自己编写定制的绘图和制表函数,是一个需要你自己决定的问题,但前提是你必须知道如何编写自己的函数。

6.7.5 跳跃分布

每个新的候选参数向量是如何生成的取决于所谓的跳跃分布。有许多选项可用作跳跃分布,但通常情况下,对于集合 \(\theta\) 中的每个参数依次生成一个标准正态随机偏差 \(N(0,1)\),并将其缩放 \(\alpha_i\) 以适应参数 \(i\) 的尺度,然后进行增量,即 公式 6.22 。如果有关参数相关性的信息,则有可能在每一步使用多元正态分布来生成一个完整的新候选参数向量;使用标准多元正态分布仍然是一种可能,这样就会忽略参数相关性。然而,在应用 Metropolis-Hastings 方法(Gibbs-内部-Metropolis)之前依次增量每个参数,在直观上似乎更容易理解(尽管并不总是最高效的)。

\[ \theta^C_i = \theta_{t,i} +N(0,1) \times \alpha_i \qquad(6.22)\]

\(\alpha_i\) 作为过程循环遍历 \(\theta\) 中的每个参数进行缩放非常重要,因为如果参数空间中的跳跃太大,那么跳跃的成功率可能会非常低;但如果跳跃太小,成功率可能会过高,并且可能需要巨大的迭代次数和时间来充分探索多维似然曲面并收敛到平稳分布。这个过程中存在试错元素,没有固定的或简单的规则来确定使用什么缩放因子。新候选值接受率是性能效率的指标。一个简单的经验法则可能是将正态随机偏差缩放到大约原始参数值的 0.5% 到 1.0%。在调整参数集的增量时,0.2 到 0.4(20-40%)的接受率可能是一个合理的目标。缩放值 \(\alpha_i\) 通常会在不同参数之间有所不同,在开发新的分析时可能需要一些详细的搜索来找到可接受的值。可以将这种自适应搜索构建到运行 MCMC 的代码中。 通常,进行 MCMC 分析时,人们会使用专门用于执行此类分析的软件。例如,Gelman 等(2013)在附录中有一个题为《R 和 Bugs 中的计算示例》的部分,但现在建议使用名为 Stan 的软件而不是 Bugs(参见 https://mc-stan.org/,那里有丰富的文档)。然而,在这里,为了确保说明保持透明,没有黑箱,我们将完全在 R 环境中进行,同时强调此类分析需要根据每个案例进行一定程度的定制。

如果使用多元正态分布一次性增加所有参数,那么缩放因子将比单独增加参数时更小,这种减少与同时变化的参数数量有关。

6.7.6 MCMC的应用示例

我们再次使用 abdat 数据,以 Schaefer 剩余生产模型拟合为例,来说明这些方法。对于更复杂的模型(更多参数),进行分析所需的时间可能会大大增加,但基本原理仍然适用。

代码
 #activate and plot the fisheries data in abdat  Fig 6.14  
data(abdat)   # type abdat in the console to see contents  
plotspmdat(abdat) #use helper function to plot fishery stats vs year
图 6.14: abdat 数据集中 cpue 和捕捞量时间序列。

6.7.7 马尔可夫链蒙特卡罗(MCMC)

描述 Gibbs-within-Metropolis 的方程式看起来相对直接,甚至简单。然而,它们的实现涉及许多更多细节。正如我们在模型参数估计章节中所见,在计算贝叶斯统计时,我们需要一种计算参数集似然的方法,但我们还需要一种计算其先验概率的方法(即使我们将其归因于分析的非信息性先验)。要实现 MCMC,还需要其他一些先决条件:

  • 计算负对数似然和每个候选参数集的先验概率所需的函数

  • 我们打算以哪个参数集开始 MCMC 过程,以及我们应该运行多长时间的 MCMC 过程,才开始存储接受的参数向量(即多长的 burn-in)?

  • 在过程中,我们应该多久考虑接受一个结果(thinning rate)?

  • 我们打算生成多少个独立的马尔可夫链,以及我们打算生成的链应该有多长,我们才停止 MCMC 过程?

  • 在特定情况下,我们如何选择一个合适的权重集(即 \(\alpha_i\) )?

依次回答这些要求和问题:

用于计算负对数似然值的函数仍然是 negLL() ,尽管我们在探索参数空间时,为了避免 \(r\) 低于零,或许应该使用 negLL1() ,它会在向量中的第一个参数接近零时施加惩罚(查看 negLL1() 的帮助和代码)。在这里,我们将始终假设每个参数的非信息性先验,因此我们在 MQMF 中包含了一个函数 calcprior() ,它仅将每个链长度的倒数对数相加。也就是说,它对所有可能的参数值赋予相等的似然。因此,单个似然值为 \(1/N\) ,其中 \(N\) 是每个链的预定长度,包括燃烧长度。我们使用它们的对数值,因为我们处理的是对数似然值,以避免因处理极小数而产生的舍入误差。如果您有一个分析问题,其中希望对所有或部分参数使用信息性先验,您只需重写 calcprior() ,这将覆盖 MQMF 中的版本,所需结果将随之而来。

通常的做法是设置一个所谓的”燃烧期”,即从 MCMC 过程的起始点运行若干次迭代而不存储结果。这种舍弃早期结果的做法是为了确保马尔可夫链开始探索模型的似然曲面,而不是在极低似然空间中游荡。当然,这取决于你用来开始马尔可夫链的起始值。你可以用最大似然解来启动 MCMC,但这通常被认为可能使结果产生偏差。然而,如果你包含几百步的燃烧期,那么被接受的起始点就会偏离最优解。尽管如此,开始马尔可夫链候选参数向量时,最好使其与最优解有一定距离,并且对于多个链,每个链从不同的点开始。

观察 Gibbs-within-Metropolis 的方程式,可能会让人产生这样的印象:每一步通过选择候选参数向量并对其进行检验的过程,都可以导致马尔可夫链的增量。如果不存在参数相关性或连续抽样的自相关性,这种情况可能成立。然而,为了避免马尔可夫链中连续步骤之间的自相关性,通常在经过一定数量的迭代后,才考虑将某一步纳入链中。这种链抽稀设计旨在消除任何自相关性水平。我们已经知道 Schaefer 参数高度相关,并将以此知识为基础,探索需要多少程度的链抽稀才能消除马尔可夫链中的自相关性。一个潜在的混淆是,在使用 Gibbs-within-Metropolis 时,我们需要为每个参数进行抽稀步骤。因此,如果希望抽稀步长为 4,那么 do_MCMC() 需要将 \(4 \times 4 = 16\) 作为 thinstep 参数。

生成马尔可夫链的目标是使其收敛于平稳分布(后验分布),这将全面表征模型及其数据的不确定性。但如何确定这种收敛是否已经实现。一种方法是生成多个马尔可夫链,从不同的起点开始。如果它们都收敛,使得每个参数的边缘分布在重复链中非常相似,这将证明已经收敛。这可以通过图形可视化。然而,除了使用图形指标外,还应使用诊断统计量来指示是否已收敛到平稳分布。有许多此类统计量可用,但在这里我们仅提及一些简单的策略。与任何非线性求解器一样,从广泛的初始值开始 MCMC 过程是个好主意。任何用于识别 MCMC 是否已达到目标平稳分布的诊断测试,实际上都会考虑不同马尔可夫序列的收敛性。

当然,在进行任何比较之前,有必要丢弃所谓的燃烧阶段。燃烧阶段是指马尔可夫链在开始表征后验分布之前可能仅遍历稀疏似然空间。Gelman 等(2013 )建议丢弃每个序列的前半部分,但实际选择的分数应由检查确定。最简单的诊断统计包括比较不同序列或同一序列不同部分的中位数和方差。如果来自不同序列(或序列的子集)的中值没有显著差异,那么可以认为序列已经收敛。同样,如果序列内(或序列子集)的方差与序列间没有显著差异,那么可以识别出收敛。使用单个序列可能看起来很方便,但如果收敛速度相对较慢且未知,那么依赖单个序列可能会提供错误的结果。 Gelman 和 Rubin(1992a)的标题清晰地指出了问题:“来自 Gibbs 采样的单个序列会带来虚假的安全感。”只需说,使用多个起始点来生成多个序列,并配合一系列诊断统计和图形,可以确保从任何 MCMC 模拟中得出的结论不是虚假的(Gelman 等,2013)。这些方法之所以被称为计算密集型,是有充分理由的。确定每个链生成序列的长度也是一个只有经验答案的问题。需要运行链直到收敛发生。这可能很快发生,也可能需要很长时间。模型中的参数越多,通常需要的时间就越长。高度不确定或不平衡的模型甚至可能无法收敛,这会是一种低效识别此类问题的方法。

为每个参数生成的增量所赋予的权重是另一个只能真正通过经验确定的事情。通过制定标准,这个过程可以自动化,并且通常在漫长的预热阶段开发,但在这里我们将采用试错法,并力求接受率在 20-40%之间。

6.7.8 MCMC的第一个示例

我们将通过第一个示例来说明上述的一些思想。为此,我们将从接近最大似然解的位置开始生成一个包含 10000 步的马尔可夫链,但有一个 50 次迭代的预热阶段(以进入信息量大的似然空间),以及一个 4 步的链稀疏率(这将不会避免连续点之间的自相关性,因为每 4 步进行一次稀疏,而参数为 4 意味着没有稀疏,但稍后再详细说明)。凭借后见之明(因为我多次运行了这个程序),我设置了 \(\alpha_i\) 的值,以使试验的接受率在 20%到 40%之间。最后,我们还将运行三个包含 100 次迭代的短链,没有预热阶段,并且从不同的起始点开始,以说明预热阶段的影响以及使用不同起始点的作用。通过使用 set.seed() 函数,我们也将确保获得可重复的结果(这通常不是一个明智的选择)。

大部分工作由 MQMF 函数 do_MCMC() 完成。你应该查看这个函数的帮助文档和代码,追踪描述 Gibbs-within-Metropolis 的方程在何处运行,以及先验概率是如何被包含的。你也应该能够看到接受率的计算方法。

代码
 # Conduct MCMC analysis to illustrate burn-in. Fig 6.15  
data(abdat);  logce <- log(abdat$cpue)  
fish <- as.matrix(abdat) # faster to use a matrix than a data.frame!  
begin <- Sys.time()       # enable time taken to be calculated  
chains <- 1                # 1 chain per run; normally do more   
burnin <- 0                # no burn-in for first three chains  
N <- 100                        # Number of MCMC steps to keep  
step <- 4       # equals one step per parameter so no thinning  
priorcalc <- calcprior # define the prior probability function  
scales <- c(0.065,0.055,0.065,0.425) #found by trial and error  
set.seed(128900) #gives repeatable results in book; usually omitted  
inpar <- log(c(r= 0.4,K=11000,Binit=3600,sigma=0.05))  
result1 <- do_MCMC(chains,burnin,N,step,inpar,negLL,calcpred=simpspm,  
                   calcdat=fish,obsdat=logce,priorcalc,scales)  
inpar <- log(c(r= 0.35,K=8500,Binit=3400,sigma=0.05))  
result2 <- do_MCMC(chains,burnin,N,step,inpar,negLL,calcpred=simpspm,  
                   calcdat=fish,obsdat=logce,priorcalc,scales)  
inpar <- log(c(r= 0.45,K=9500,Binit=3200,sigma=0.05))  
result3 <- do_MCMC(chains,burnin,N,step,inpar,negLL,calcpred=simpspm,  
                   calcdat=fish,obsdat=logce,priorcalc,scales)  
burnin <- 50 # strictly a low thinning rate of 4; not enough
step <- 16   # 16 thinstep rate = 4 parameters x 4 = 16  
N <- 10000   # 16 x 10000 = 160,000 steps + 50 burnin
inpar <- log(c(r= 0.4,K=9400,Binit=3400,sigma=0.05))  
result4 <- do_MCMC(chains,burnin,N,step,inpar,negLL,calcpred=simpspm,  
                   calcdat=fish,obsdat=logce,priorcalc,scales)  
post1 <- result1[[1]][[1]]  
post2 <- result2[[1]][[1]]  
post3 <- result3[[1]][[1]]  
postY <- result4[[1]][[1]]  
cat("time   = ",Sys.time() - begin,"\n")  
time   =  10.10596 
代码
cat("Accept = ",result4[[2]],"\n")  
Accept =  0.3471241 0.3437158 0.354289 0.3826251 

现在我们可以将这 10000 长的链绘制为一组点,并在这些点之上叠加三个较短的链,这些链没有使用预热阶段。这突出了预热阶段的重要性,同时也说明了不同的链如何独立地开始探索似然空间(在这里仅以二维表示)。如果这三个链更长,我们预计它们会在由灰色点占据的空间中穿越更广阔的区域。

代码
 #first example and start of 3 initial chains for MCMC Fig6.15  
parset(cex=0.85)     
P <- 75  # the first 75 steps only start to explore parameter space
plot(postY[,"K"],postY[,"r"],type="p",cex=0.2,xlim=c(7000,13000),  
   ylim=c(0.28,0.47),col=8,xlab="K",ylab="r",panel.first=grid())  
lines(post2[1:P,"K"],post2[1:P,"r"],lwd=1,col=1)  
points(post2[1:P,"K"],post2[1:P,"r"],pch=15,cex=1.0)  
lines(post1[1:P,"K"],post1[1:P,"r"],lwd=1,col=1)  
points(post1[1:P,"K"],post1[1:P,"r"],pch=1,cex=1.2,col=1)  
lines(post3[1:P,"K"],post3[1:P,"r"],lwd=1,col=1)  
points(post3[1:P,"K"],post3[1:P,"r"],pch=2,cex=1.2,col=1)  
图 6.15: 从不同的起点(三角形、正方形、圆形)出发的三个独立 MCMC 链中的前 75 个点。这些短链没有设置预烧,因此记录从起点开始。灰色圆点是第四条链中的 10000 个点,其中有 50 个点的 “预烧”和 4 个点的 “稀疏率”,这给出了所有链都应趋近的静态分布的大致概念。

我们也可以通过使用 pairs() 函数和 rgb() 函数将每个参数与其他参数绘制出来,并使用颜色填充来检查参数的相关性细节,这使我们能够可视化点向 \(K\) 较大值和 \(r\) 较小值方向逐渐变软的趋势。要看到效果变化,将 alpha 参数(即 1/50)改为 1/1,使用 50 作为除数似乎在这个情况下(有 10000 个点)是一个合理的折中方案,用于展示密度的梯度。

代码
 #pairs plot of parameters from the first MCMC Fig 6.16  
posterior <- result4[[1]][[1]]  
msy <-posterior[,1]*posterior[,2]/4     
pairs(cbind(posterior[,1:4],msy),pch=16,col=rgb(1,0,0,1/50),font=7)  
图 6.16: Schaefer 模型参数后验分布的 10000 个样本与 MSY 之间的关系。通常情况下,我们会使用比 4 更长的稀疏化步长来描述后验结果。图中的全部颜色至少来自 50 个点。

构成后验分布的接受参数向量可以单独绘制,以参数编号为横轴,提供每个参数的轨迹。理想情况下,应获得通常所说的“毛毛虫”形状,这在 图 6.17\(\sigma\) 参数的轨迹中尤为明显。其他轨迹上下波动,暗示每个轨迹中存在一定程度的自相关。 \(r\)\(K\) 参数轨迹中明显的互补变化模式也支持这一观点。边缘分布提供了对每个参数经验分布形状的初步检验。根据数据情况,可能存在多个峰值或较平的顶部。然而,不规则的形状则表明缺乏收敛。

代码
 #plot the traces from the first MCMC example Fig 6.17  
posterior <- result4[[1]][[1]]  
par(mfrow=c(4,2),mai=c(0.4,0.4,0.05,0.05),oma=c(0.0,0,0.0,0.0))  
par(cex=0.8, mgp=c(1.35,0.35,0), font.axis=7,font=7,font.lab=7)  
label <- colnames(posterior)  
N <- dim(posterior)[1]  
for (i in 1:4) {  
  ymax <- getmax(posterior[,i]); ymin <- getmin(posterior[,i])  
  plot(1:N,posterior[,i],type="l",lwd=1,ylim=c(ymin,ymax),  
       panel.first=grid(),ylab=label[i],xlab="Step")  
  plot(density(posterior[,i]),lwd=2,col=2,panel.first=grid(),main="")  
}  
图 6.17: 四个 Schaefer 模型参数的轨迹以及每个参数的隐含边际分布。如果稀疏化步长增加到 128、256 或更长,迹线内明显的自相关性就会得到改善。

我们可以使用 R 函数 acf() 来检查马尔可夫链中连续步骤之间的自相关程度。该函数将向量中的值与其自身、滞后 1、滞后 2 等依次进行相关性分析。我们期望滞后 0 时的相关性为 1,但理想情况下,如果我们要避免低估总变异,马尔可夫链中连续项之间的相关性应该迅速降至无意义水平。将每个参数的自相关图与其轨迹和边缘分布进行比较。 \(sigma\) 与其它参数相比,其序列相关性相对较低,这一视觉差异应该很明显。

代码
 #Use acf to examine auto-correlation with thinstep = 16   Fig 6.18  
posterior <- result4[[1]][[1]]  
label <- colnames(posterior)[1:4]  
parset(plots=c(2,2),cex=0.85)  
for (i in 1:4) auto <- acf(posterior[,i],type="correlation",lwd=2,  
                           plot=TRUE,ylab=label[i],lag.max=20)  
图 6.18: 四个 Schaefer 模型参数的轨迹显示的自相关性。这是在对四个参数中的每个参数进行 16 = 4 的稀疏化处理后得到的结果。显然,要消除出现的强相关性,需要大幅提高步长。

如果我们使用一个远大于初始值的薄化率运行 MCMC,希望能够观察到序列相关性的减少。这里我们将薄化率增加了 128 倍(从 4x4=16 增加到 4x128=512),并绘制了结果。尽管我们将链的长度减少到 1000,但总步数是(512 x 1000)+(512 x 100)= 563200,因此我们可以预期这次运行会比初始的 MCMC 运行稍长一些。了解运行时间总是个好主意。这些例子最多只需几分钟就能运行完成,而大多数用于严肃模型的 MCMC 运行需要数小时甚至数天。

代码
 #setup MCMC with thinstep of 128 per parameter  Fig 6.19  
begin=gettime()  
scales <- c(0.06,0.05,0.06,0.4)  
inpar <- log(c(r= 0.4,K=9400,Binit=3400,sigma=0.05))  
result <- do_MCMC(chains=1,burnin=100,N=1000,thinstep=512,inpar,  
                  negLL,calcpred=simpspm,calcdat=fish,  
                  obsdat=logce,calcprior,scales,schaefer=TRUE)  
posterior <- result[[1]][[1]]  
label <- colnames(posterior)[1:4]  
parset(plots=c(2,2),cex=0.85)  
for (i in 1:4) auto <- acf(posterior[,i],type="correlation",lwd=2,  
                           plot=TRUE,ylab=label[i],lag.max=20)  
图 6.19: 当抽稀步长为 512 = 4 x 128 时,四个 Schaefer 模型参数的轨迹中显示的自相关情况,这是第一个自相关图中所用抽稀步长的 128 倍。
代码
cat(gettime() - begin)  
35.0029

当抽稀步长从 4 增加到 128(4 * 128 = 512)时,四个参数轨迹中显示的自相关明显减少( 图 6.19 )。但即便 32 倍的增幅也不足以将滞后 2、3 和 4 内的相关性降至无意义。尽管如此,使用该抽稀步长生成的轨迹明显显示出差异(改进),相对于图 6.17 中的轨迹。然而,我们注意到,在当前使用的台式计算机上,563200 步(1100 长度链)大约耗时 40 秒,这台计算机开始对交互式工作有些慢。如果我们使用 512 的抽稀率进行 10000 次迭代,那将耗时近 7 分钟,这开始变得有些繁琐漫长。更复杂的模型可能需要更长的时间,有时甚至需要数天!因此,在我们探索更大抽稀步长的有效性之前,我们将首先找到显著加快每个循环的方法。 通过持续试验发现,将 i 增加到 1024( \(4 \times 256\))仍然存在显著的滞后 1 和有时滞后 2 的自相关,而将 thinstep 增加四倍到 2048(4 * 3 * 128)才能消除所有变量的自相关。

在寻找更快捷的方法之前,我们将完成对标准说明性图的分析,这些图可能在执行模型框架内不确定性或变异的 MCMC 检查时使用。

6.7.9 边际分布

一种可视化后验分布的方法是检查每个参数接受值的频率分布。在直方图上绘制 density() 函数的轮廓也可以改善我们对 MCMC 找到的分布的观察。在下面的情况下,即使每个参数的抽稀率为 128,1000 个复制样本似乎也不足以平滑每个分布。然而,确定复制样本数量是否足够实际上是在问后验分布是否收敛于平稳分布。与其依赖直觉和各种图形的外观,不如使用各种标准的诊断方法。大多数使用抽稀率为 16 绘制的与 MCMC 输出相关的图形似乎表明可能平滑的解。然而,自相关太大,输出可能存在偏差。最好使用可量化的诊断方法。

代码
 # plot marginal distributions from the MCMC  Fig 6.20  
dohist <- function(x,xlab) { # to save a little space  
  return(hist(x,main="",breaks=50,col=0,xlab=xlab,ylab="",  
               panel.first=grid()))   
}  
 # ensure we have the optimum solution available  
param <- log(c(r= 0.42,K=9400,Binit=3400,sigma=0.05))   
bestmod <- nlm(f=negLL,p=param,funk=simpspm,indat=abdat,  
               logobs=log(abdat$cpue))  
optval <- exp(bestmod$estimate)  
posterior <- result[[1]][[1]] #example above N=1000, thin=512  
par(mfrow=c(5,1),mai=c(0.4,0.3,0.025,0.05),oma=c(0,1,0,0))   
par(cex=0.85, mgp=c(1.35,0.35,0), font.axis=7,font=7,font.lab=7)   
np <- length(param)  
for (i in 1:np) { #store invisible output from hist for later use  
  outH <- dohist(posterior[,i],xlab=colnames(posterior)[i])  
  abline(v=optval[i],lwd=3,col=4)  
  tmp <- density(posterior[,i])  
  scaler <- sum(outH$counts)*(outH$mids[2]-outH$mids[1])  
  tmp$y <- tmp$y * scaler  
  lines(tmp,lwd=2,col=2)  
}  
msy <- posterior[,"r"]*posterior[,"K"]/4  
mout <- dohist(msy,xlab="MSY")  
tmp <- density(msy)  
tmp$y <- tmp$y * (sum(mout$counts)*(mout$mids[2]-mout$mids[1]))  
lines(tmp,lwd=2,col=2)  
abline(v=(optval[1]*optval[2]/4),lwd=3,col=4)  
mtext("Frequency",side=2,outer=T,line=0.0,font=7,cex=1.0)  
图 6.20: 稀疏率为 128 时 1000 个点的边际后验分布。在每种情况下,垂直蓝线都是最大似然最优估计值。可能需要更多的重复来平滑分布。后验模式不一定与最大似然估计值相同。

6.8 Rcpp的应用

对于本章我们已使用的简单模型,运行 20 万个复制(薄化步长为 8 意味着这将涉及 160 万次似然计算)所需的时间并不算过于繁重。然而,对于更复杂的模型,或试图消除高水平的序列或自相关时,运行 MCMC 所需的时间可能会变得令人厌烦。如果你有一个运行起来感觉花费很长时间的流程,那么很可能值得对代码的这部分进行性能分析。在 R 中,这很容易通过使用 Rprof() 函数来完成。一旦启动,该函数会不时中断执行(默认为 0.02 秒),并确定中断时正在运行哪个函数。这些情况都会被记录下来,一旦软件运行完成,就可以应用 summaryRprof() 函数来发现哪些函数运行时间最长(self.time)。然后可以尝试加快这些慢速部分。total.time 包括函数本身花费的时间以及它调用的任何其他函数的时间。

代码
 #profile the running of do_MCMC  using the now well known abdat   
data(abdat); logce <- log(abdat$cpue); fish <- as.matrix(abdat)    
param <- log(c(r=0.39,K=9200,Binit=3400,sigma=0.05))  
Rprof(append=TRUE)  # note the use of negLL1()  
result <- do_MCMC(chains=1,burnin=100,N=20000,thinstep=16,inpar=param,  
                 infunk=negLL1,calcpred=simpspm,calcdat=fish,  
                 obsdat=logce,priorcalc=calcprior,  
                 scales=c(0.07,0.06,0.07,0.45))  
Rprof(NULL)  
outprof <- summaryRprof()  
代码
kable(head(outprof$by.self,12)) 
表 6.6: 将 Rprof() 函数应用于 do_MCMC() 函数调用后的输出结果,仅查看输出列表的 by.self 部分(按每个函数的运行时间排序),检查 outprof 的结构。总采样时间在 outprof$sampling.time 中。self.pct 的总和为 99.99,因此这些值是需要关注的。
self.time self.pct total.time total.pct
“funk” 85.78 60.47 114.70 80.85
“mean” 10.52 7.42 13.68 9.64
“max” 9.06 6.39 9.06 6.39
“do_MCMC” 7.00 4.93 141.72 99.90
“infunk” 6.12 4.31 130.66 92.10
“which” 5.66 3.99 7.76 5.47
“dnorm” 4.34 3.06 4.34 3.06
“isTRUE” 2.42 1.71 2.90 2.04
“priorcalc” 2.28 1.61 3.10 2.19
“penalty0” 2.14 1.51 2.14 1.51
“mean.default” 1.88 1.33 3.16 2.23
“numeric” 1.06 0.75 1.06 0.75

显然,几乎所有时间(total.time)都花在了 do_MCMC() 函数中,但在该函数内部,大约 80%的时间用于 funk()= calcpred() = simpspm()。在函数 mean() 中也花费了合理的时间,等等。[.data.frame] 和都与将结果索引放入 do_MCMC() 函数中的矩阵有关。如果它们出现在你的 Rprof 列表中,你可以通过将输入数据的 data.frame 转换为矩阵来部分提高速度。你应该比较使用每种数据形式的计算速度。R 软件确实非常出色,但即使近年来版本速度有所提升,以及使用现代计算机,也没有人会声称它在 MCMC 等计算密集型方法上迅速。显然,如果我们能够加快 simpspm() 函数(即 funk)的速度,那么在运行 MCMC 时可能会获得一些显著的速度提升。也许最好的方法是用 Stan(参见https://mc-stan.org/),但在尽可能保持在基础 R 范围内的前提下,我们将考察一种替代方案。

提高执行速度的一个非常有效的方法是将 R 代码与另一种可以编译成可执行代码而非 R 解释代码的计算机语言相结合。将 C++代码包含到 R 代码中最简单的方法可能是使用 Rcpp 包(Eddelbuettel & Francois, 2011; Eddelbuettel, 2013; Eddelbuettel & Balmuta, 2017)。显然,要使用这种方法,需要同时拥有 C++编译器和 Rcpp 包,这两者都可以从 CRAN 仓库下载(在 RStudio 中完成最为容易)。如果读者正在使用 Linux 或 Mac 计算机,那么他们已经拥有 GNU C++编译器(Rcpp 所使用的编译器)。在 Windows 系统上,安装 GNU C++编译器最简单的方法是访问 CRAN 主页,点击”Download R for Windows”链接,然后点击 Rtools 链接。务必将安装目录添加到路径中。这还提供了许多用于编写 R 包的工具。Rcpp 提供了一些将 C++代码包含进来的方法,其中最简单的方法可能是使用 cppFunction() 在每个会话开始时编译代码。 然而,更好的方法是使用函数 Rcpp::sourceCpp() 从磁盘加载 C++文件,就像你可能使用 source() 加载 R 代码文件一样。所以有多种选择,如果你打算采用这种加速代码的策略,这些选择都值得探索。

6.8.1 处理向量和矩阵

在下面的代码块中,可以看到 cppFunction() 需要将 C++代码输入为一段长文本字符串。使用 C++的一个复杂之处在于,在 R 中,向量、矩阵和数组的索引从 1:N,…,而在 C++中,相同数量的单元格索引将从 0:(N-1),….习惯的力量在我们来回切换 R 和 C++时可能会让我们犯傻(或者也许只是我)。例如,在开发下面的 C++代码时,我最初设置了 biom[0] = ep[3]。因此,我记住了在 biom[0]部分使用 0 而不是 1,但在设置生物量时间序列的初始生物量水平时,我又迅速忘记了索引问题,将参数向量中的 sigma 值设置为初始生物量 Binit 而不是。在 R 中,pars 变量在索引 1 中包含 \(r\) ,在索引 2 中包含 \(K\),在 3 中包含 \(B_{init}\),在 4 中包含 \(sigma\),但在 C++中,索引是 0、1、2 和 3。如果你觉得最后几句让你感到困惑,那就把它当作一件好事,因为希望如果你选择这条加速代码的路线,你会记得非常小心地在向量和矩阵中索引变量。 正如你很可能发现的那样,如果你在 C++中使用指向数组外部的索引(例如,在包含 0、1、2 和 3 的向量中,索引 4 不存在,但它会指向内存中的某个位置!),这通常会导致 R 崩溃并需要重启。在 R 中开发 C++代码时,你会很快学会在运行任何东西之前保存所有内容,我建议你也这样做。

当然,用 C++编程和使用 Rcpp 都有其复杂性,但本章或本书的目的并不是回顾这些主题。尽管如此,希望这个简单的例子能够说明在使用计算机密集型方法时,使用这些方法具有相当显著的优点,并成功鼓励你在适当的场合学习和使用这些方法。Eddelbuettel(2013)和 Wickham(2019)对 Rcpp 的优点提供了优秀的介绍。

6.8.2 simpspm() 的替代方法

如果要使用 simpspmC() 函数代替 simpspm(),则需要在运行任何代码之前运行以下代码块中的代码。

代码
library(Rcpp)  
 #Send a text string containing the C++ code to cppFunction this will   
 #take a few seconds to compile, then the function simpspmC will   
 #continue to be available during the rest of your R session. The   
 #code in this chunk could be included into its own R file, and then  
 #the R source() function can be used to include the C++ into a   
 #session. indat must have catch in col2 (col1 in C++), and cpue in  
 #col3 (col2 in C++). Note the use of ; at the end of each line.   
 #Like simpspm(), this returns only the log(predicted cpue).  
cppFunction('NumericVector simpspmC(NumericVector pars,  
             NumericMatrix indat, LogicalVector schaefer) {  
    int nyrs = indat.nrow();  
    NumericVector predce(nyrs);  
    NumericVector biom(nyrs+1);  
    double Bt, qval;  
    double sumq = 0.0;  
    double p = 0.00000001;  
    if (schaefer(0) == TRUE) {  
      p = 1.0;  
    }  
    NumericVector ep = exp(pars);  
    biom[0] = ep[2];  
    for (int i = 0; i < nyrs; i++) {  
      Bt = biom[i];  
      biom[(i+1)]=Bt+(ep[0]/p)*Bt*(1-pow((Bt/ep[1]),p))-  
                      indat(i,1);  
      if (biom[(i+1)] < 40.0) biom[(i+1)] = 40.0;  
      sumq += log(indat(i,2)/biom[i]);  
    }  
    qval = exp(sumq/nyrs);  
    for (int i = 0; i < nyrs; i++) {  
      predce[i] = log(biom[i] * qval);  
    }  
    return predce;  
}')  

一旦运行了 cppFunction() 代码,我们就可以在任何使用过 simpspm() 函数的地方使用 simpspmC() 函数。一个小麻烦是,simpspmC() 希望输入数据 abdat 是矩阵,而 abdat 以 data.frame 开始(实际上是列表,试试 class(abdat))。输入 data.frame 而不是矩阵会导致 C++ 函数失效,因此,为了解决这个问题,在下面的代码中,你会看到我们使用了 as.matrix() 函数,以确保向 simpspmC() 发送正确的对象类别,幸运的是,使用矩阵比使用 data.frame 更快,因此我们也将其发送给了 simpspm()。我们还加入了 microbenchmark 软件包,以便准确比较两个不同函数的运行速度。显然,要使用该软件包,必须安装该软件包(如果不想安装,也可以省略)。在我的 Windows 10 2018 XPS 13 上进行的比较中,根据时间中位数,simpspmC() 所花的时间通常只有 simpspm() 的 20%,如 表 6.8 所示。第一次使用 simpspmC() 函数时,有时启动速度非常慢,这会影响平均值,但中位数受到的干扰较小。

代码
 #Ensure results obtained from simpspm and simpspmC are same  
library(microbenchmark)  
data(abdat)  
fishC <- as.matrix(abdat) # Use a matrix rather than a data.frame  
inpar <- log(c(r= 0.389,K=9200,Binit=3300,sigma=0.05))  
spmR <- exp(simpspm(inpar,fishC)) # demonstrate equivalence  
 #need to declare all arguments in simpspmC, no default values  
spmC <- exp(simpspmC(inpar,fishC,schaefer=TRUE))  
out <- microbenchmark( # verything identical calling function  
  simpspm(inpar,fishC,schaefer=TRUE),   
  simpspmC(inpar,fishC,schaefer=TRUE),  
  times=1000  
)  
out2 <- summary(out)[,2:8]  
out2 <- rbind(out2,out2[2,]/out2[1,])  
rownames(out2) <- c("simpspm","simpspmC","TimeRatio")  
代码
kable(halftable(cbind(spmR,spmC)),row.names=TRUE,digits=c(4,4,4,4,4,4))  
表 6.7: simpspm() 和 simpspmC() 的预测结果并排展示,以证明代码从参数 c(r=0.389, K=9200, Binit=3300, sigma=0.05) 生成了相同的答案。
spmR spmC spmR spmC
1 1.1251 1.1251 1.9956 1.9956
2 1.0580 1.0580 2.0547 2.0547
3 1.0774 1.0774 2.1619 2.1619
4 1.0570 1.0570 2.2037 2.2037
5 1.0827 1.0827 2.1314 2.1314
6 1.1587 1.1587 2.0773 2.0773
7 1.2616 1.2616 2.0396 2.0396
8 1.3616 1.3616 1.9915 1.9915
9 1.4538 1.4538 1.9552 1.9552
10 1.5703 1.5703 1.9208 1.9208
11 1.7056 1.7056 1.8852 1.8852
12 1.8446 1.8446 1.8276 1.8276

现在我们可以汇总微基准测试的输出结果

代码
kable(out2,row.names=TRUE,digits=c(3,3,3,3,3,3,3,0))
表 6.8: simpspm 和 simpspmC 函数的微基准比较输出结果。微秒值分别是最小值、25 分位数、均值和中位数、75 分位数、最大值以及比较中的评估次数。TimeRatio 是第二行除以第一行,因此就均值而言,simpspmC 所需时间约为 simpspm 的 7%。实际值会在不同运行中有所变化,但变化不会太大,尽管不同计算机和不同版本的 R 之间可能会有差异(最好使用最新版本,通常最快)。
min lq mean median uq max neval
simpspm 45.800 48.600 57.752 52.000 61.200 2309.900 1000
simpspmC 2.600 2.800 4.041 3.200 3.400 749.900 1000
TimeRatio 0.057 0.058 0.070 0.062 0.056 0.325 1

simpspmC 的最大值有时会大于 simpspm,这是因为第一次调用时有时会花费更长的时间。如果出现这种情况,请尝试再次运行比较并注意最大值的变化。然而,真正感兴趣的比较是使用 simpspmC() 而不是 simpspm() 运行 MCMC 平均快多少。我们可以不用 microbenchmark 来做这个比较,因为每次运行所花费的时间现在是可以察觉的。相反,我们使用 MQMF 函数 gettime() ,它提供从每天开始以来的时间(以秒为单位)。

代码
 #How much does using simpspmC in do_MCMC speed the run time?  
 #Assumes Rcpp code has run, eg source("Rcpp_functions.R")  
set.seed(167423) #Can use getseed() to generate a suitable seed  
beginR <- gettime()  #to enable estimate of time taken  
setscale <- c(0.07,0.06,0.07,0.45)  
reps <- 2000  #Not enough but sufficient for demonstration  
param <- log(c(r=0.39,K=9200,Binit=3400,sigma=0.05))  
resultR <- do_MCMC(chains=1,burnin=100,N=reps,thinstep=128,  
                  inpar=param,infunk=negLL1,calcpred=simpspm,  
                  calcdat=fishC,obsdat=log(abdat$cpue),schaefer=TRUE,  
                  priorcalc=calcprior,scales=setscale)  
timeR <- gettime() - beginR   
cat("time = ",timeR,"\n")  
time =  16.81959 
代码
cat("acceptance rate = ",resultR$arate," \n")  
acceptance rate =  0.319021 0.3187083 0.3282664 0.368747  
代码
postR <- resultR[[1]][[1]]  
set.seed(167423)     # Use the same pseudo-random numbers and the   
beginC <- gettime()  # same starting point to make the comparsion  
param <- log(c(r=0.39,K=9200,Binit=3400,sigma=0.05))  
resultC <- do_MCMC(chains=1,burnin=100,N=reps,thinstep=128,  
                 inpar=param,infunk=negLL1,calcpred=simpspmC,  
                 calcdat=fishC,obsdat=log(abdat$cpue),schaefer=TRUE,  
                 priorcalc=calcprior,scales=setscale)  
timeC <- gettime() - beginC  
cat("time = ",timeC,"\n")  # note the same acceptance rates  
time =  3.311526 
代码
cat("acceptance rate = ",resultC$arate," \n")  
acceptance rate =  0.319021 0.3187083 0.3282664 0.368747  
代码
postC <- resultC[[1]][[1]]  
cat("Time Ratio = ",timeC/timeR)  
Time Ratio =  0.196885

尽管每次运行所需的确切时间会有所不同,因为你的电脑会同时运行其他进程,但通常使用 simpspmC() 所需的时间仅是使用 simpspm() 所需时间的 1/5 或 20%。在处理分钟时,这可能看起来并不重要,但一旦 MCMC 运行需要 20-40 小时,那么节省 16-32 小时可能会被认为更有价值。当然,还有其他潜在的方法可以加速这个过程,对于真正计算密集型的分析,这通常是值得的。

使用这两个函数中的任意一个所得到的结果与运行不同的链是等效的,尽管只有当 set.seed() 函数使用不同的值,或者,更好的是,根本不使用它时才成立。如果你使用了相同的种子,那么得到的结果边缘分布将是相同的。使用不同的种子,但只有 2000 次迭代,可能会出现一些偏差。现在我们有了更快速的方法,我们可以在保持较大的抽样间隔的同时探索更多的迭代次数。

代码
 #compare marginal distributions of the 2 chains  Fig 6.21  
par(mfrow=c(1,1),mai=c(0.45,0.45,0.05,0.05),oma=c(0.0,0,0.0,0.0))   
par(cex=0.85, mgp=c(1.35,0.35,0), font.axis=7,font=7,font.lab=7)   
maxy <- getmax(c(density(postR[,"K"])$y,density(postC[,"K"])$y))  
plot(density(postR[,"K"]),lwd=2,col=1,xlab="K",ylab="Density",  
     main="",ylim=c(0,maxy),panel.first=grid())  
lines(density(postC[,"K"]),lwd=3,col=5,lty=2)  
图 6.21: 比较由 simpspm 函数(实黑线)和 simpspmC 函数(虚蓝线)生成的链的 K 参数密度分布,每条链具有相同的起始位置和相同的随机种子,它们相互重叠。使用不同的种子或不同的起始位置重复这些示例,以观察效果。

6.8.3 多个独立链

在进行 MCMC 分析时,最佳做法是运行多个链,但在实际操作中,生成链的总数往往需要在可用时间与至少三个或更多链之间进行权衡。重要的是提供足够的证据来支持分析师关于分析已达到收敛的说法。这里我们将只使用三个链,尽管实际上,对于这样一个简单的模型,运行更多的链会更有说服力。为了提高速度,我们现在只使用 simpspmC(),因为每条细化前的链长为 \(10100 \times 256 = 2585600(2585600 \times 3 = 7756800,770\) 万次迭代)。

代码
 #run multiple = 3 chains  
setscale <- c(0.07,0.06,0.07,0.45)  # I only use a seed for   
set.seed(9393074) # reproducibility within this book  
reps <- 10000   # reset the timer  
beginC <- gettime()  # remember a thinstep=256 is insufficient  
resultC <- do_MCMC(chains=3,burnin=100,N=reps,thinstep=256,  
                   inpar=param,infunk=negLL1,calcpred=simpspmC,  
                   calcdat=fishC,obsdat=log(fishC[,"cpue"]),  
                   priorcalc=calcprior,scales=setscale,schaefer=TRUE)  
cat("time = ",gettime() - beginC," secs  \n")  
time =  93.89898  secs  
代码
 #3 chain run using simpspmC, 10000 reps, thinstep=256 Fig 6.22  
par(mfrow=c(2,2),mai=c(0.4,0.45,0.05,0.05),oma=c(0.0,0,0.0,0.0))   
par(cex=0.85, mgp=c(1.35,0.35,0), font.axis=7,font=7,font.lab=7)   
label <- c("r","K","Binit","sigma")  
for (i in 1:4) {  
   plot(density(resultC$result[[2]][,i]),lwd=2,col=1,  
        xlab=label[i],ylab="Density",main="",panel.first=grid())    
   lines(density(resultC$result[[1]][,i]),lwd=2,col=2)  
   lines(density(resultC$result[[3]][,i]),lwd=2,col=3)  
}  
图 6.22: 在 64(4x64=256)的稀疏率下使用 10000 次重复和 simpspmC 函数计算的四种 Schaefer 参数的边际密度分布在三个链之间的差异。在线宽大于平均值的地方,会出现明显的细微差别。

我们还可以生成不同链的汇总统计数据。事实上,有许多不同的诊断统计和图表可以使用。

代码
 #generate summary stats from the 3 MCMC chains  
av <- matrix(0,nrow=3,ncol=4,dimnames=list(1:3,label))  
sig2 <- av  # do the variance  
relsig <- av # relative to mean of all chains  
for (i in 1:3) {   
  tmp <- resultC$result[[i]]  
  av[i,] <- apply(tmp[,1:4],2,mean)  
  sig2[i,] <- apply(tmp[,1:4],2,var)  
}  
cat("Average \n")  
Average 
代码
av  
          r        K    Binit      sigma
1 0.3821707 9495.580 3522.163 0.04805695
2 0.3809524 9530.307 3537.186 0.04811021
3 0.3822318 9487.911 3522.021 0.04810015
代码
cat("\nVariance per chain \n")  

Variance per chain 
代码
sig2  
             r         K    Binit        sigma
1 0.0009018616 1060498.2 151208.8 6.264484e-05
2 0.0008855405  998083.0 142153.1 6.177037e-05
3 0.0009080043  978855.6 138585.3 6.288734e-05
代码
cat("\n")  
代码
for (i in 1:4) relsig[,i] <- sig2[,i]/mean(sig2[,i])  
cat("Variance Relative to Mean Variance of Chains \n")  
Variance Relative to Mean Variance of Chains 
代码
relsig                                          
          r         K     Binit     sigma
1 1.0037762 1.0474275 1.0501896 1.0033741
2 0.9856108 0.9857815 0.9872949 0.9893677
3 1.0106130 0.9667911 0.9625155 1.0072582

如果我们对不同分布的量值进行比较,就能更清楚地了解差异的程度。百分比差异是指我们直接比较 2.5%和 97.5% 分位数(分布的中心 95%)的值以及第二和第三边际分布的中值,只有一个比较点(Binit 的 97.5% 上限点)大于 1%。

代码
 #compare quantile from the 2 most widely separate MCMC chains  
tmp <- resultC$result[[2]] # the 10000 values of each parameter  
cat("Chain 2 \n")  
Chain 2 
代码
msy1 <- tmp[,"r"]*tmp[,"K"]/4  
ch1 <- apply(cbind(tmp[,1:4],msy1),2,quants)  
round(ch1,4)  
           r         K    Binit  sigma     msy1
2.5%  0.3206  7926.328 2942.254 0.0356 853.1769
5%    0.3317  8140.361 3016.340 0.0371 859.6908
50%   0.3812  9401.467 3489.550 0.0472 896.5765
95%   0.4287 11338.736 4214.664 0.0624 955.1773
97.5% 0.4386 11864.430 4425.248 0.0662 970.7137
代码
tmp <- resultC$result[[3]]  
cat("Chain 3 \n")  
Chain 3 
代码
msy2 <- tmp[,"r"]*tmp[,"K"]/4  
ch2 <-  apply(cbind(tmp[,1:4],msy2),2,quants)  
round(ch2,4)  
           r         K    Binit  sigma     msy2
2.5%  0.3225  7855.611 2920.531 0.0355 853.0855
5%    0.3324  8090.493 3001.489 0.0371 859.3665
50%   0.3826  9370.715 3475.401 0.0471 895.8488
95%   0.4316 11248.955 4188.052 0.0626 952.1486
97.5% 0.4416 11750.426 4376.639 0.0665 966.2832
代码
cat("Percent difference ")  
Percent difference 
代码
cat("\n2.5%  ",round(100*(ch1[1,] - ch2[1,])/ch1[1,],4),"\n")  

2.5%   -0.6006 0.8922 0.7383 0.4636 0.0107 
代码
cat("50%   ",round(100*(ch1[3,] - ch2[3,])/ch1[3,],4),"\n")  
50%    -0.3871 0.3271 0.4055 0.2278 0.0812 
代码
cat("97.5% ",round(100*(ch1[5,] - ch2[5,])/ch1[5,],4),"\n")  
97.5%  -0.6817 0.9609 1.0985 -0.5278 0.4564 

6.8.4 所需重复实验以避免序列相关

我们在前面已经看到,如果稀疏率过低,每个参数的迹线或序列内的自相关性就会很高。显然,增加稀疏化步数会降低自相关性。但不太清楚的是,需要多大的稀疏率才能使这种相关性变得不明显。

现在我们有了一种更快的方法来探讨这个问题,我们可以寻找所需的稀疏率规模。我们从之前的试验中得知,每个参数的稀疏率为 128 时,滞后 2 到 4 步之间仍然存在显著的相关性,因此,我们应该研究稀疏率为 1024(\(4 \times 256\))和 2048(\(4 \times 512\))时的效果。为了更严格地进行比较,我们平衡了稀疏率和迭代次数,因此我们使用 \(2000 \times 1024\)\(1000 \times 2048\),两者都 \(= 2048000\)。问题在于消除自相关是否能更好地掌握不同参数的全部变化。但是,为了使试验具有可比性,未稀疏链的长度必须相同,这样才能对似然曲面进行相同程度的探索。因此,在较小的稀疏率下,我们需要更多的迭代次数,同时还需要考虑烧入期(一个烧入 100 次,另一个烧入 50 次)。图 6.23 中,稀疏率为 1024 时,滞后期为 1 时仍有显著相关性,而稀疏率为 2028 时,相关性消失。即使使用 simpspmC(),该例程也需要 60 秒左右。

消除序列内相关性的重要性在于,如果序列内相关性很高,就会干扰向静态分布的收敛(因为序列点是相关的,而不是跟踪似然曲面的全部范围),结果可能无法捕捉到模型和所研究的可用数据固有的全部变化范围。

代码
 #compare two higher thinning rates per parameter in MCMC  
param <- log(c(r=0.39,K=9200,Binit=3400,sigma=0.05))  
setscale <- c(0.07,0.06,0.07,0.45)  
result1 <- do_MCMC(chains=1,burnin=100,N=2000,thinstep=1024,  
                   inpar=param,infunk=negLL1,calcpred=simpspmC,  
                   calcdat=fishC,obsdat=log(abdat$cpue),  
                   priorcalc=calcprior,scales=setscale,schaefer=TRUE)  
result2 <- do_MCMC(chains=1,burnin=50,N=1000,thinstep=2048,  
                   inpar=param,infunk=negLL1,calcpred=simpspmC,  
                   calcdat=fishC,obsdat=log(abdat$cpue),  
                   priorcalc=calcprior,scales=setscale,schaefer=TRUE)  
代码
 #autocorrelation of 2 different thinning rate chains Fig6.23  
posterior1 <- result1$result[[1]]  
posterior2 <- result2$result[[1]]  
label <- colnames(posterior1)[1:4]  
par(mfrow=c(4,2),mai=c(0.25,0.45,0.05,0.05),oma=c(1.0,0,1.0,0.0))   
par(cex=0.85, mgp=c(1.35,0.35,0), font.axis=7,font=7,font.lab=7)    
for (i in 1:4) {  
  auto <- acf(posterior1[,i],type="correlation",plot=TRUE,  
              ylab=label[i],lag.max=20,xlab="",ylim=c(0,0.3),lwd=2)  
  if (i == 1) mtext(1024,side=3,line=-0.1,outer=FALSE,cex=1.2)  
  auto <- acf(posterior2[,i],type="correlation",plot=TRUE,  
              ylab=label[i],lag.max=20,xlab="",ylim=c(0,0.3),lwd=2)  
  if (i == 1) mtext(2048,side=3,line=-0.1,outer=FALSE,cex=1.2)  
}  
mtext("Lag",side=1,line=-0.1,outer=TRUE,cex=1.2)  
图 6.23: 两条链在 Schaefer 模型四个参数上的自相关性,综合稀疏率分别为 1024 和 2048。请注意,Y 轴上的最大值缩小了,使两者之间的差异更加明显。

我们可以用比较上述三个复制链的相同方法来比较具有不同稀疏率的两个链。也就是说,我们可以绘制它们的边际分布图,并比较它们的量值分布。由于稀疏化后的最终复制数量有限,它们的边际分布惊人地相似(图 6.23)。然而,在比较它们的量值分布时,观察到的分布中心 95% 之间的差异往往比上文 “多重独立链”一节中比较的三条链要大。这些差异似乎并没有遵循任何特定的方向,不过,随着下限和上限向同一方向移动,似乎存在一些偏差,但需要更多的重复才能澄清这一点。

代码
 #visual comparison of 2 chains marginal densities  Fig 6.24  
parset(plots=c(2,2),cex=0.85)   
label <- c("r","K","Binit","sigma")  
for (i in 1:4) {  
   plot(density(result1$result[[1]][,i]),lwd=4,col=1,xlab=label[i],  
        ylab="Density",main="",panel.first=grid())    
   lines(density(result2$result[[1]][,i]),lwd=2,col=5,lty=2)  
}  
图 6.24: 在 2048(虚线)和 1024(黑色实线)的稀疏率下,使用 1000 和 2000 个重复序列计算的 K 参数边际密度分布在两个链之间的变化。The variation between two chains in the marginal density distributions for the K parameter using 1000 and 2000 replicates at thinning rates of 2048 (dashed line) and 1024 (solid black line).

独立的 MCMC 链总会存在一定程度的差异,这就是相似性标准概念变得重要的原因。在这两条链中,虽然存在明显的差异,但中位数的实际差异都小于 1%,而在 10 个外部量级中,有 8 个的实际差异都小于 1%。我们理应相信薄化率更高的链。

代码
 #tablulate a summary of the two different thinning rates.  
cat("1024 thinning rate \n")  
1024 thinning rate 
代码
posterior <- result1$result[[1]]  
msy <-posterior[,1]*posterior[,2]/4   
tmp1 <- apply(cbind(posterior[,1:4],msy),2,quants)  
rge <- apply(cbind(posterior[,1:4],msy),2,range)  
tmp1 <- rbind(tmp1,rge[2,] - rge[1,])  
rownames(tmp1)[6] <- "Range"  
print(round(tmp1,4))  
           r         K    Binit  sigma      msy
2.5%  0.3221  7918.242 2943.076 0.0352 853.5243
5%    0.3329  8139.645 3016.189 0.0367 858.8872
50%   0.3801  9429.118 3499.826 0.0470 895.7376
95%   0.4289 11235.643 4172.932 0.0627 953.9948
97.5% 0.4392 11807.732 4380.758 0.0663 973.2185
Range 0.2213  7621.901 2859.858 0.0612 238.5436
代码
posterior2 <- result2$result[[1]]  
msy2 <-posterior2[,1]*posterior2[,2]/4    
cat("2048 thinning rate \n")  
2048 thinning rate 
代码
tmp2 <- apply(cbind(posterior2[,1:4],msy2),2,quants)  
rge2 <- apply(cbind(posterior2[,1:4],msy2),2,range)  
tmp2 <- rbind(tmp2,rge2[2,] - rge2[1,])  
rownames(tmp2)[6] <- "Range"  
print(round(tmp2,4))  
           r         K    Binit  sigma     msy2
2.5%  0.3216  7852.002 2920.198 0.0351 855.8295
5%    0.3329  8063.878 3000.767 0.0368 859.8039
50%   0.3820  9400.708 3482.155 0.0468 896.6774
95%   0.4313 11235.368 4184.577 0.0628 959.2919
97.5% 0.4434 11638.489 4456.164 0.0676 975.4358
Range 0.2189  8156.444 3161.232 0.0546 257.1803
代码
cat("Inner 95% ranges and Differences between total ranges \n")   
Inner 95% ranges and Differences between total ranges 
代码
cat("95% 1 ",round((tmp1[5,] - tmp1[1,]),4),"\n")  
95% 1  0.1172 3889.49 1437.682 0.0311 119.6942 
代码
cat("95% 2 ",round((tmp2[5,] - tmp2[1,]),4),"\n")  
95% 2  0.1218 3786.487 1535.966 0.0325 119.6064 
代码
cat("Diff  ",round((tmp2[6,] - tmp1[6,]),4),"\n")  
Diff   -0.0024 534.5429 301.3746 -0.0066 18.6367 

6.9 结束语

本章的目的不是鼓励人们编写自己的自助、渐近误差、似然分布或 MCMC 函数,而是让他们探索这些方法,获得直觉,以便在使用这些方法时能够清楚地认识到它们的优势,同样重要的是,认识到它们的局限性。在实施 MCMC 分析时尤其如此,人们最好使用 Stan 或 Template Model Builder(Kristensen 等,2016)或 AD Model Builder(Fournier 等,1998;Fournier 等,2012)等工具。尽管如此,我们还是通过使用 MCMC 详细介绍了贝叶斯统计的应用,因为这确实是捕捉任何特定建模分析中固有的所有不确定性的最佳方法。尽管如此,在许多渔业模型中,许多参数,如自然死亡率、繁殖陡度和一些选择性参数,都被设定为常数,在贝叶斯背景下,这意味着信息量极大的先验。这种说法有点矫揉造作,因为如果不对这些参数进行估计,我们就不需要考虑任何先验概率,但原则上这就是它的含义。解决此类问题的通常方法是研究此类参数的似然曲线,或进行敏感性分析,研究使用不同常数的影响。甚至可以使用标准化的似然曲线作为先验概率。

诚然,使用 MCMC 可以更全面地描述建模情景中的变化,但这种分析无法捕捉模型的不确定性,在这种情况下,结构不同的模型可能会对所评估的种群动态提供不同的看法。这就是通常讨论的模型平均概念。不过,这也提出了一个问题,即哪个模型被认为是最现实的,以及在它们可能完全不相称的情况下,每个模型的权重是多少。不过,在研究任何建模结果时,都需要考虑模型的不确定性。正如 Punt 和 Hilborn(1997)所说:“最常见的方法是选择一个单一的结构模型,并只考虑其参数的不确定性。另一种更站得住脚的方法是考虑一系列真正不同的结构模型。然而,除了计算量更大之外,还很难’约束’所考虑的模型范围。与此相关的一个问题是,如何确定有多少模型参数应被视为不确定参数”。

不确定性的特征描述非常重要,因为它提供了在提供管理建议时可以有多大信心的一些概念。人们可能会迷失在计算细节中,而忘记了主要目标是为某种自然资源提供站得住脚的管理建议。要做到这一点,并没有单一的方法,因此,如果情况导致 MCMC 始终无法收敛,仍有可能采用其他方法,并对评估结果和种群的状况进行长期描述。如果了解这些方法,并知道如何使用和解释这些方法所能发现的模型及其数据,显然会有所帮助。但最终,了解渔业历史以及除渔获量外的其他影响因素也会有所帮助。