复杂函数求导的 R 语言实践

2017 年 12 月 4 日 数萃大数据 康维珊等

概述

求导是求解非线性方程的重要途径之一。简单方程的求导,略懂数学知识的人手动就可以算出。然而,在实际一些算法中,求导的函数往往较为复杂。当使用如 Newton-raphson 求解时候,就需要花费大量时间来计算求导公式或导数初值。R 语言是一门统计学相关的语言,其中包含大量求解数学算法的包以及函数等,尤其方便非相关专业的编程人员的使用。R 语言开发环境下载安装简单,没有复杂的框架,运行直观容易,能够快速进行开发并运行结果。作者以个人经验为谈,旨在让初学者少走弯路,快速掌握 R 语言求导的技巧。

简单函数求导

1. Deriv 方法

deriv 是 R 中最常见的求导方法。其英文全拼“derivative”也易于使人理解。下面的例子是对公式 y=x^2 中的求导,其中须注意表达式表示方法。作为必须连接符“~”不能缺省,目标变量 y 可以省略。如不使用连接符“~”,也可采用 expression(x^2) 来直接声明表达式。deriv 方法求导后会返回 expression 类型的变量。然后通过 eval 函数赋值给 x 来计算出导数结果。举例如下:

  
    
    
    
  1. grad <- deriv(y~x^2,"x") #deriv 方法求取一阶导数

  2. grad #调用 grad 变量

  3. expression({

  4. .value <- x^2

  5. .grad <- array(0, c(length(.value), 1L), list(NULL, c("x")))

  6. .grad[, "x"] <- 2 * x

  7. attr(.value, "gradient") <- .grad

  8. .value

  9. })

  10. x=2 #给参数赋值

  11. eval(grad) #给导数公式赋值

  12. [1] 4

  13. attr(,"gradient")

  14. x

  15. [1,] 4

Deriv 还可返回函数类型的结果,参数即是所求导的变量 x,当然也可以设置多个参数,这样在计算结果时可直接通过函数对参数赋值。例如:

  
    
    
    
  1. grad <- deriv(expression(x^2),"x",func=TRUE) #func=TRUE 令返回值为 function 类型

  2. grad(2) #grad 函数中直接参数赋值计算结果

  3. [1] 4

  4. attr(,"gradient")

  5. x

  6. [1,] 4

  7. grad <- deriv(expression(x^2*y*z),"x",function(x,y,z){}) #用 function(){} 传递多个参数

  8. grad(1,2,3)

  9. [1] 6 #原函数代参结果

  10. attr(,"gradient")

  11. x

  12. [1,] 12 #导函数代参结果

  13. grad <- deriv(expression(x^2*y*z),"x",function.arg = c("x","y","z")) #用 function.arg 属性传递多个参数

  14. grad(1,2,3)

  15. [1] 6

  16. attr(,"gradient")

  17. x

  18. [1,] 12

2. Deriv 方法

Deriv3 是求二阶导的方法,与 Deriv 使用方法相同,返回结果为二阶导数。

  
    
    
    
  1. deriv3(y~x^4,"x")

  2. expression({

  3. .value <- x^4

  4. .grad <- array(0, c(length(.value), 1L), list(NULL, c("x")))

  5. .hessian <- array(0, c(length(.value), 1L, 1L), list(NULL,

  6. c("x"), c("x")))

  7. .grad[, "x"] <- 4 * x^3

  8. .hessian[, "x", "x"] <- 4 * (3 * x^2)

  9. attr(.value, "gradient") <- .grad

  10. attr(.value, "hessian") <- .hessian

  11. .value

  12. })

3. D 方法

D 方法与 deriv 的最大不同之处在于其返回类型为“call”类型,这也意味着它更容易嵌入在其他方法中被引用。其次它能够求取二阶偏导。再者 D 方法的嵌套使用可以求取任意高阶导数。最后 D 的结果要比 deriv 的结果直观一些,deriv 常会把返回的表达式转换成复合表达式的形式,不如 D 方法显示结果直接。 D 方法的调用方式基本和 deriv 相同,在此不再赘述。二阶偏导可通过 D 方法嵌套的方式先对 x 求导再对 y 求导来获得。

  
    
    
    
  1. #D 方法求二阶偏导

  2. dxy = D(D(expression(x^2*y^2),"x"),"y")

  3. dxy2 * x * (2 * y)

带参函数求导

假设带参函数如下:

其中 

对上面的 y 函数进行求导,当 x 为奇数时求解第一部分,偶数时求解第二部分,所以 R 中实现方式如下。

  
    
    
    
  1. library(Deriv)

  2. options(digits=16)

  3. param = c(2.0,3.0,6.0,8.0,7.0) #参数向量

  4. dx_o = D(expression(log(x)),"x") #x 为奇数时求导公式

  5. dx_e = D(expression(log(x^2)),"x") #x 为偶数时求导公式

  6. dx_e

  7. 2 * x/x^2

  8. dx_e = Simplify(dx_e) #简化公式

  9. dx_e

  10. 2/x

  11. grad = 0.0

  12. for(i in 1:length(param)){ #参数循环

  13. if(param[i]%%2){ #参数奇偶判断

  14. grad = grad + eval(dx_e,list(x=param[i])) #参数代入相应公式进行累加求和

  15. }else{

  16. grad = grad + eval(dx_o,list(x=param[i]))

  17. }

  18. }

  19. grad # 打印一阶导数公式计算结果

  20. [1] 1.744047619047619

从本例中可以看出,求和在求导中最为容易,把整体函数可以拆解成单独的个体来看待,最终再合并即可。上面的 R code 引入了 eval 赋值的另外一种用法-list。当公式中包含多个参数,使用 list 方法赋值最为方便,例如 eval(expression(a+b+c),list(a=1,b=2,c=3))。 除此之外,还使用到 Simplify 方法来简化表达式。像 dx_e 实际输出结果为 2*x/x^2,因为 D 方法或 Deriv 方法只作求导,不做简化处理,因此得到的结果可读性不强。所以 Simplify 方法对表达式进行简化处理,从而使公式更加明朗,但它并不是 R 自带方法,因此需要引入 library(Deriv) 才能使用。在引入 library 之前,需要先通过命令 install.packages(“Deriv”) 来安装该算法包。

复杂函数求导

1. 函数拼接

对上面的嵌套函数,如果全部展开来书写原函数,其一消耗时间,其二无法确保正确性,所以磨刀不误砍柴工,下面是针对公式 A 的 R code 实现。

  
    
    
    
  1. #公式 A 嵌套拼接

  2. epsilo <- function(p){ #拼接 epsilo

  3. expr = "log(y)-Alpha0"

  4. for(i in 1:p){

  5. expr = paste(expr,"-Alpha",i,"*X",i,sep="")

  6. }

  7. return(expr)

  8. }

  9. A <- function(epsilo){ #拼接公式 A

  10. expr = paste("exp(",epsilo,")/delta",sep="")

  11. return(expr)

  12. } )

  13. p=2

  14. D(parse(text=A(epsilo(p))),"Alpha0") #将公式 A 的字符串形式转换为表达式

  15. -(exp(log(y) - Alpha0 - Alpha1 * X1 - Alpha2 * X2)/delta)

从上面可以看出,面对较长或嵌套的公式,我们可以简化成字符串拼接的方式,即 paste 方法来堆砌表达式,然后使用 parse(text=字符串) 将字符串转换成表达式类型,此方法在运算中非常实用。这样再复杂的函数都可以先拼接成字符串再转换成要求导的表达式,后面再做求导操作就非常容易了。

2. 函数转换

上面的公式不是纯粹的求和公式,因此需要对它进行转换才能达到可操作的目的。大家都知道导数具有下面等式性质:


所以,公式 Y 求导可以根据上面的等式进行转换。公式 Y 转换如下: 


然后,转换后的式子明显变繁为简,仅需重点求取 A关于x的偏导即可。

  
    
    
    
  1. #函数转换求导数公式

  2. A <- function(p){ #拼接 A 式子

  3. alpha = c(p)

  4. expr = "log(y) - Alpha0"

  5. for(j in 1:p){

  6. expr = paste(expr," - X",j,"* Alpha",j,sep = "")

  7. }

  8. return(expr)

  9. }

  10. p=2 #指定 parameter 个数

  11. dA = deparse(D(parse(text=A(p)),"Alpha1")) #A 式对变量 Alpha1 求导,再将表达式转换成字符串

  12. dY = NULL

  13. dY = paste(dA,"* log(1-(",A(p),"))-(",A(p),")*1/(1-(" ,A(p),"))*", dA) #拼接 Y 的导数式子

  14. Simplify(parse(text=dY)) #简化导数公式

  15. expression(X1 * ((log(y) - (Alpha0 + Alpha1 * X1 + Alpha2 * X2))/(1 +

  16. Alpha0 + Alpha1 * X1 + Alpha2 * X2 - log(y)) - log(1 + Alpha0 +

  17. Alpha1 * X1 + Alpha2 * X2 - log(y))))

在如上代码中,deparse 用于将表达式转换成字符串再行进行拼接以完成 Y 的求导公式。所以,parse 方法与 deparse 是相对的,前者将字符串转换成表达式,后者将表达式转换成字符串。如此,复杂的表达式便可以使用子表达式转换成字符串做任意拼接。

3. 特殊函数

R 语言可以对很多特殊函数直接求导,从而使得求导更为方便。下面以 Gamma 函数与 Bessel 函数为示例说明。 (1) Gamma 函数 R 语言不仅可以直接调用 gamma 函数,而且同样可以对 gamma 函数进行求导。

  
    
    
    
  1. #gamma 函数求导数公式

  2. D(expression((gamma(x))^2),"x")

  3. 2 * (gamma(x) * digamma(x) * (gamma(x)))

(2) BesselK 函数 R 语言不能对 BesselK 函数直接进行求导,但是能够对 cosh 函数求导。BesselK 实际需要调用 cosh 子函数,因此,将 BesselK 转换成 cosh 相关表达式后就能使用 R 求导了。例如: 

  
    
    
    
  1. #BesselK 函数部分求导数公式

  2. Func = expression(exp(-z*cosh(t))*cosh(v*t))

  3. D(Func,"z")

  4. exp(-z * cosh(t)) * (sinh(v * t) * t)

因此,BesselK(v,z) 求导后的公式为: 

非线性方程求解

求导在求解非线性方程中有很普遍的应用,这里以 R 的 newtonraphson 方法中自带的例子举例。

  
    
    
    
  1. # newtonraphson 方法求解非线性方程

  2. ftn4 <- function(x) {

  3. # returns function value and its derivative at x

  4. fx <- log(x) - exp(-x)

  5. dfx <- 1/x + exp(-x) #手动输入导数公式

  6. return(c(fx, dfx))

  7. }

  8. library(spuRs)

  9. newtonraphson(ftn4, 2, 1e-6)

  10. At iteration 1 value of x is: 1.12202

  11. At iteration 2 value of x is: 1.294997

  12. At iteration 3 value of x is: 1.309709

  13. At iteration 4 value of x is: 1.3098

  14. Algorithm converged

  15. [1] 1.3098

在 newtonraphson 方法中,输入包含原函数、导函数与初值。其中导函数需要从原函数求导而来。所以如采用本文中的求导方法,以上代码可更改如下:

  
    
    
    
  1. # D 方法求导作为 newtonraphson 输入

  2. ftn4 <- function(x) {

  3. # returns function value and its derivative at x

  4. fx <- expression(log(x) - exp(-x))

  5. dfx <- D(fx,"x") #D 方法求导获取导数公式

  6. return(c(eval(fx), eval(dfx)))

  7. }

  8. newtonraphson(ftn4, 2, 1e-6)

  9. At iteration 1 value of x is: 1.12202

  10. At iteration 2 value of x is: 1.294997

  11. At iteration 3 value of x is: 1.309709

  12. At iteration 4 value of x is: 1.3098

  13. Algorithm converged

  14. [1] 1.3098

本例中,使用 D 方法求导来代替手动输入导数公式。这样,仅保证原函数输入正确即可,导函数使用 D 方法或 Deriv 方法求出,从而省去计算导函数并验证导函数是否正确的步骤。在实际应用过程中,原函数已经相当复杂,如若计算导函数并键入公式,耗时费力且结果未必正确,因此使用 R 的求导方法可以大大简化该繁琐步骤。

结束语

求导在数学算法应用中相当普遍,例如线性回归模型,Cox 回归模型,参数化回归模型中等等。本文旨在利用 R 中的求导方式使得该流程简单化,省去费力耗时的工作,从而达到事半功倍的效果。本文是在作者实际使用摸索的基础上摘取一些可以帮助初学者的例子,希望可以使需要的人受益。

欢迎大家关注微信公众号:数萃大数据

课程公告

深度学习培训班【上海站】

时间:2017年12月23-24日

地点:上海创梦云实训创新中心

更多详情,请扫描下面二维码

登录查看更多
1

相关内容

最新《自动微分手册》77页pdf
专知会员服务
103+阅读 · 2020年6月6日
机器学习速查手册,135页pdf
专知会员服务
342+阅读 · 2020年3月15日
【新书】Python中的经典计算机科学问题,224页PDF
专知会员服务
56+阅读 · 2019年12月31日
sklearn 与分类算法
人工智能头条
7+阅读 · 2019年3月12日
从零开始深度学习:利用numpy手写一个感知机
数萃大数据
10+阅读 · 2018年6月10日
深度学习线性代数简明教程
论智
11+阅读 · 2018年5月30日
入门 | 一文介绍机器学习中基本的数学符号
机器之心
28+阅读 · 2018年4月9日
【干货】理解深度学习中的矩阵运算
专知
12+阅读 · 2018年2月12日
机器之心最干的文章:机器学习中的矩阵、向量求导
深度学习世界
12+阅读 · 2018年2月7日
专栏 | fastText原理及实践
机器之心
3+阅读 · 2018年1月26日
算法优化|梯度下降和随机梯度下降 — 从0开始
全球人工智能
8+阅读 · 2017年12月25日
Arxiv
18+阅读 · 2019年1月16日
Arxiv
3+阅读 · 2018年12月18日
Arxiv
5+阅读 · 2018年5月28日
Arxiv
8+阅读 · 2018年5月15日
Arxiv
5+阅读 · 2018年4月22日
VIP会员
相关资讯
sklearn 与分类算法
人工智能头条
7+阅读 · 2019年3月12日
从零开始深度学习:利用numpy手写一个感知机
数萃大数据
10+阅读 · 2018年6月10日
深度学习线性代数简明教程
论智
11+阅读 · 2018年5月30日
入门 | 一文介绍机器学习中基本的数学符号
机器之心
28+阅读 · 2018年4月9日
【干货】理解深度学习中的矩阵运算
专知
12+阅读 · 2018年2月12日
机器之心最干的文章:机器学习中的矩阵、向量求导
深度学习世界
12+阅读 · 2018年2月7日
专栏 | fastText原理及实践
机器之心
3+阅读 · 2018年1月26日
算法优化|梯度下降和随机梯度下降 — 从0开始
全球人工智能
8+阅读 · 2017年12月25日
Top
微信扫码咨询专知VIP会员