R 中的设计模式

本文翻译自 Design Patterns in R(By Sebastian Warnholz)。

本文的灵感来源于:

设计模式似乎是一个很大的词,特别是因为它在面向对象编程中的使用。但最终我认为它只不过是软件设计中的可重复策略。

不动点算法

下面,我使用 R 并且以计算正数平方根的不动点算法为例。算法定义如下:

\[x_{n+1} = f(x_n)
\]

用于寻找平方根的不动点函数如下:

\[f(x \mid p) = \frac{p}{x}
\]

要计算的正是 p 的平方根。用 R 代码描述算法:

fp <- function(f,
x,
converged,
...)
{
value <- f(x, ...)
if (converged(x, value)) value
else Recall(f, value, converged, ...)
}

x 是初始值或最后一次迭代得到的值。converged 是有 arguments... 两个参数的函数,... 用于 R 的“函数柯里化”[1]。不动点函数如下:

fpsqrt <- function(x, p) p / x

并且

converged <- function(x, y) all(abs(x - y) < 0.001)

开始计算:

fp(fpsqrt, 2, converged, p = 2)

## Error: evaluation nested too deeply: infinite recursion / options(expressions=)?

第一次运行完全不起作用。在目前的代码实现中很难找出哪里出了问题,但我们会找到的。下面,我将应用不同的模式来修改上述框架以获得解决方案。

包装器模式

这个模式是我从 Stuart Sierra 的演讲中得到的。通过包装器模式我可以向一个函数增加新功能,却不改变原先的函数。我所要做的事就是给函数添加日志,或者记录函数的保留属性,R 中的许多函数并不会这样做。有时候要尝试调用一个函数并每两分钟重试一次,因为连接数据库失败或文件系统没有响应。

一个函数要有单一、明确的用途,日志和写入数据库是两件事。计算下一次迭代以及记录迭代次数也是两件事。这一个功能,以及用于记录是/否的额外参数,不会长期独立存在。

在我的上个例子中,遇到的问题是不动点函数在两个值之间振荡,而不是收敛于平方根。解决这个问题的一个技巧是使用平均阻尼。这就是说我们用 \(\frac{x_{n-1}+x_n}{2}\),而不是 \(x_n\) 来计算 \(x_{n+1}\)。这实际上不是不动点函数逻辑的一部分,所以逻辑不应该被它污染:

averageDamp <- function(fun)
{
function(x, ...) (x + fun(x, ...)) / 2
} fp(averageDamp(fpsqrt), 2, converged, p = 2) ## [1] 1.414214
# and to compare:
sqrt(2) ## [1] 1.414214

OK,看起来能跑通了!我需要一个额外的包装器来打印每一次迭代的值:

printValue <- function(fun)
{
function(x,
...)
{
cat(x, "\n")
fun(x, ...)
}
} fp(printValue(averageDamp(fpsqrt)), 2, converged, p = 2)
## 2
## 1.5
## 1.416667
## 1.414216
## [1] 1.414214

现在的问题是,如果我们添加太多的包装器,计算就会变得复杂。事实上试图找出哪个包装器首先被调用,也许这对你来说并不明显。

包装器模式可以在原函数之前之后(或前后同时)增加新功能。printValue 在原函数之前添加打印功能,averageDamp 在原函数之后作修正。如果看到 averageDamp 的另一种实现,模式将会显得更加清晰:

averageDamp <- function(fun)
{
function(x, ...)
{
value <- fun(x, ...)
(x + value) / 2
}
}

接口模式

柯里化(Currying)

从我的角度来看,这项技术的价值在于你可以更轻松地构建接口(在语言能够充分支持的情况下)。例如,平方根的不动点函数需要两个参数。然而,该算法实际上只知道一个参数。在这种情况下柯里化只是意味着将双参数函数 fpsqrt 变成单参数函数。我们可以通过设置 p = 2 来实现这一点,这是我借助 ... 实现的。

在 R 中,有两个原生选项来模拟柯里化。通常看到的是使用点参数(...)来允许将额外的参数传递给该函数。然而,这给我的框架中的每个实现都带来了额外的负担,因为我需要让我定义的每个包装器函数使用点参数。另一种选择是使用匿名函数将原始版本封装在单参数函数中,如下所示:

fp(averageDamp(function(x) fpsqrt(x, p = 2)), 2, converged)

## [1] 1.414214

如果我依靠这个接口(单参数函数),我可以不是用点参数。然而,这种技术的语法支持在 R 中受到限制,这就是为什么会出现像 purrrrlist 这样的软件包来试图改善这种情况;包 functionalpryr 提供专门的功能用于实现柯里化。

闭包(Closures)

R 中的每个函数都是一个闭包(除了原生函数)。闭包是一个具有与之相关环境的函数。例如,R 包中的函数可以访问包的名称空间,或在面向对象时类中的方法可以访问整个类。但是通常这个术语是在从其他函数中返回函数时使用的(每当你试图对闭包进行子集化时,除了 R 的错误消息外)。如果你对此还不了解,你可以阅读这篇文章《Advanced R》的相关章节。

我的例子中,我使用闭包来为给定的 p 值重新定义平方根的不动点函数。我认为只有通过以下实施才能强调给定 p

fpsqrt <- function(p)
{
function(x) p / x
}

这实际上使算法的调用更加简洁:

fp(averageDamp(fpsqrt(2)), 2, converged)

## [1] 1.414214

缓存模式

在各种情况下,我都想缓存一些结果而不是重新计算它们。这是因为性能方面的考虑,因为无论使用哪个库,计算矩阵逆的时间关于样本大小都不是线性的。如果你有 10000 个观察值,并且要计算在蒙特卡罗模拟中得到的协方差矩阵(\(10000 \times 10000\))的逆,你有得好等了。为了说明这一点,我设想计算一个线性估计量。尽管估计量可以通过解析方法来得到,但我使用了不动点算法。这种情况下,不动点函数中我使用 Newton-Raphson 算法,定义如下:

\[\beta_{n+1} = \beta_n - (f'' (\beta_n))^{-1} f'(\beta_n)
\]

其中,

\[f'(\beta) = X^\top (y - X\beta) \\
f''(\beta) = -X^\top X
\]

是正态分布情况下似然函数在 \(\beta\) 点的一阶和二阶导数。如果你对此没有什么概念,就把注意力完全放在这个模式上。在 R 中,我已经使用闭包应用了接口模式,请考虑以下实现:

nr <- function(X,
y)
{
function(beta) beta - solve(-crossprod(X)) %*% crossprod(X, y - X %*% beta)
} # Some data to test the function:
set.seed(1)
X <- cbind(1, 1:10)
y <- X %*% c(1, 2) + rnorm(10) # Average damping in this case will make the convergence a bit slower:
fp(printValue(averageDamp(nr(X, y))), c(1, 2), converged)
## 1 2
## 0.9155882 2.027366
## 0.8733823 2.041049
## 0.8522794 2.047891
## 0.8417279 2.051311
## 0.8364522 2.053022
## 0.8338143 2.053877
## 0.8324954 2.054304
##           [,1]
## [1,] 0.8318359
## [2,] 2.0545183
# And to have a comparison:
stats::lm.fit(X, y)$coefficients ## x1 x2
## 0.8311764 2.0547321

结果看起来很好。我们应该选择一个不同的容忍度,以便更接近 R 的实现,目前看来这个容忍度选择得非常宽松,但这不是现在的重点。我现在想要做的是使 nr 的返回函数依赖于预先计算的值,以避免它们在每次迭代中重新计算。在这个例子中,我将它与局部函数的定义结合起来:

nr <- function(X, y)
{
# f1 relies on values in its scope:
f1 <- function(beta) Xy - XX %*% beta Xy <- crossprod(X, y)
XX <- crossprod(X)
f2inv <- solve(-XX) function(beta) beta - f2inv %*% f1(beta)
} fp(averageDamp(nr(X, y)), c(1, 2), converged)
##           [,1]
## [1,] 0.8318359
## [2,] 2.0545183

一些评论:

  • 在上面的例子中,像 f1 这样的局部函数定义是唯一依赖自由变量的地方。即 f1 依赖于封闭环境中定义的值 XyXX;这是我在上层函数中不惜一切代价要避免的。
  • 我喜欢这种表示方法,因为我将不动点函数的逻辑保留在 nr;在给定数据的情况下,该函数知道如何计算下一次迭代的所有事。一种不同的方法是定义 nr,使其将 XXXyf2inv 作为参数,这意味着我的代码的其他部分必须知道 nr 中的实现,并且我必须查看不同的地方以了解下一次迭代是如何进行的计算。

计数器模式

到目前为止,不动点框架不允许限制迭代次数。这当然是你总想要控制的东西。这次我使用闭包来模拟可变状态。考虑计数器的两种实现:

# Option 1:
counterConst <- function()
{
# like a constructor function
count <- 0
function()
{
count <<- count + 1
count
}
}
counter <- counterConst()
counter()
## [1] 1
counter()
## [1] 2
# Option 2:
counter <- local({
count <- 0
function()
{
count <<- count + 1
count
}
})
counter()
## [1] 1
counter()
## [1] 2

我还记得闭包很难理解,因为在上面的例子中,当 R 中几乎所有东西都是不可变的时候,它们可以用来模拟可变状态。为从 Hadley (R 领域的知名专家)的一个例子中明白这一点,我可能用头撞了几个小时墙。

为了在不动点框架中实现最大迭代次数,我结合了包装器模式计数器模式来修改收敛准则,以便算法在给定次数的迭代后终止:

addMaxIter <- function(converged,
maxIter)
{
count <- 0
function(...)
{
count <<- count + 1
if (count >= maxIter) TRUE
else converged(...)
}
}

这使我们能够探索最初示例中发生的错误:

fp(printValue(fpsqrt(2)), 2, addMaxIter(converged, 4))
## 2
## 1
## 2
## 1
## [1] 2

现在我们可以看到算法的初始版本在 1 到 2 之间振荡。您可能会说,在这种情况下,你还可以将迭代的次数看作算法的逻辑(作为 fp 的责任)。在这种情况下,我会争辩说,代码不再反映之前介绍的算法公式。但是让我们来比较一下不同的实现:

fpImp <- function(f,
x,
convCrit,
maxIter = 100)
{
converged <- function()
{
convCrit(x, value) | count >= maxIter
} count <- 0
value <- NULL repeat { count <- count + 1
value <- f(x) if (converged()) break
else
{
x <- value
next
} } list(result = value, iter = count)
} fpImp(averageDamp(fpsqrt(2)), 2, converged)
## $result
## [1] 1.414214
##
## $iter
## [1] 4

现在让我们为 fp 的返回值添加迭代次数:

addIter <- function(fun)
{
count <- 0
function(x)
{
count <<- count + 1
value <- fun(x)
attr(value, "count") <- count
value
}
} fp(addIter(averageDamp(fpsqrt(2))), 2, converged)
## [1] 1.414214
## attr(,"count")
## [1] 4

也许这个实现应该有一个自己的名字,但是你仍然可以看到包装器计数器模式,它和 averageDamp 函数类似。与 fpImp 相比,围绕最大迭代次数的逻辑已经从算法的具体实现中分离出来。特别是如果我考虑添加更多功能,命令式的实现不可避免地必须应付越来越多的事情。相反,我可以在我的不动点框架中插入新功能。所以我认为对扩展开放对修改封闭,这不仅仅是一件好事,如果你喜欢面向对象的话。

计数器模式当然更一般化。它仅仅反映了模拟可变状态的一种策略。只有极少数情况下,我才真的需要这样做,计数是一个重复出现的主题。


  1. 函数柯里化(Currying)指的是把多个参数放进一个接受许多参数的函数,形成一个新的函数接受余下的参数,并返回结果 ↩︎

【翻译】R 中的设计模式的更多相关文章

  1. 【原创翻译】认识MVC设计模式:web应用开发的基础(实际编码篇)

    原文地址:http://www.larryullman.com/2009/10/15/understanding-mvc-part-3/ 全系列INDEX [原创翻译]认识MVC设计模式:web应用开 ...

  2. webstorm快捷键 webstorm keymap内置快捷键英文翻译、中英对照说明

    20160114参考网络上的快捷键,整理自己常用的: 查找/代替shift+shift 快速搜索所有文件,简便ctrl+shift+N 通过文件名快速查找工程内的文件(必记)ctrl+shift+al ...

  3. 在 R 中估计 GARCH 参数存在的问题(基于 rugarch 包)

    目录 在 R 中估计 GARCH 参数存在的问题(基于 rugarch 包) 导论 rugarch 简介 指定一个 \(\text{GARCH}(1, 1)\) 模型 模拟一个 GARCH 过程 拟合 ...

  4. 在 R 中估计 GARCH 参数存在的问题

    目录 在 R 中估计 GARCH 参数存在的问题 GARCH 模型基础 估计 GARCH 参数 fGarch 参数估计的行为 结论 译后记 在 R 中估计 GARCH 参数存在的问题 本文翻译自< ...

  5. R中遇到的部分问题

    在Rstdio使用的是3.5.1的64位R版本中遇到问题:The Perl script 'WriteXLS.pl' failed to run successfully. 首先使用 Sys.whic ...

  6. [Head First设计模式]山西面馆中的设计模式——观察者模式

    系列文章 [Head First设计模式]山西面馆中的设计模式——装饰者模式 引言 不知不自觉又将设计模式融入生活了,吃个饭也不得安生,也发现生活中的很多场景,都可以用设计模式来模拟.原来设计模式就在 ...

  7. [Head First设计模式]山西面馆中的设计模式——建造者模式

    系列文章 [Head First设计模式]山西面馆中的设计模式——装饰者模式 [Head First设计模式]山西面馆中的设计模式——观察者模式 引言 将学习融入生活中,是件很happy的事情,不会感 ...

  8. [Head First设计模式]饺子馆(冬至)中的设计模式——工厂模式

    系列文章 [Head First设计模式]山西面馆中的设计模式——装饰者模式 [Head First设计模式]山西面馆中的设计模式——观察者模式 [Head First设计模式]山西面馆中的设计模式— ...

  9. [Head First设计模式]抢票中的设计模式——代理模式

    系列文章 [Head First设计模式]山西面馆中的设计模式——装饰者模式 [Head First设计模式]山西面馆中的设计模式——观察者模式 [Head First设计模式]山西面馆中的设计模式— ...

随机推荐

  1. Java类的封装

    java中四种不同的限定词限定的成员(成员变量或成员变量方法),访问权限由大到小依次为: public(公共的) 可以被所有的类访问 protected(受保护的) 可以被这个类本身访问 可以被它的子 ...

  2. 5.String StringBuffer StringBuilder

    String,StringBuffer和StringBuilder三者的讲解 对于StringBuffer和StringBuilder是对Stirng的一个优化. 之前已经说过了,String对象一旦 ...

  3. 树莓派2 raspyberry Pi2 交叉编译app

    Pi 使用的是ARMV7架构的BCM2836, 下载交叉编译器 arm-linux-gnueabihf-gcc 即可. 本地环境: Ubuntu14 + x86_64 1. 下载编译器地址: 1). ...

  4. Ubuntu-18.04 下使用Nginx搭建高可用,高并发的asp.net core集群

    一.实现前的准备 以下是实现简单负载均衡的思路,图中的服务器均为虚拟机 三台Linux服务器,一台用作Nginx负载均衡(192.168.254.139),另外两台用作Asp.Net Core应用程序 ...

  5. VC++下使用ADO操作数据库

    VC++下使用ADO操作数据库主要要用到 _ConnectionPtr,_CommandPtr,_RecordsetPtr三个ADO对象指针,我查找了相关资料,发现网上源码很多,但是都相对凌乱,于是自 ...

  6. OC实用转换model的工具

    OC实用转换model的工具 说明 这是本人写的一个专门用来将json数据直接转换生成Model文件的工具,目的是为了让你从写Model文件的繁琐过程中解脱出来,提升效率以及减少出错的几率,工具的特点 ...

  7. AC自动机, 字符串匹配算法

    package utils import java.util.HashMapimport java.util.LinkedListimport util.control.Breaks._import ...

  8. IIS 7 反向代理 URL重写 转发动态请求

    一.反向代理是什么 有一篇文章说的挺好的 Nginx 反向代理.负载均衡.页面缓存.URL重写及读写分离详解 http://www.server110.com/nginx/201402/5534.ht ...

  9. 装office系统软件

    在电脑里搜索 SW_DVD5_Office_Professional_Plus_2010w_SP1_64Bit_ChnSimp_CORE_MLF_X17-76742 如果已经安装过了,有错误,直接进行 ...

  10. js 获取后缀参数

    function getQueryString(name) { var reg = new RegExp("(^|&)" + name + "=([^&] ...