使用梯度下降优化函数

Optimize a function using gradient descent

生长度日是植物物候学中的一个概念,特定作物每天需要积累一定量的热量单位才能从一个阶段移动到另一个阶段。

我有一个给定站点 10 年的每日分辨率的热单位数据,如下所示:

  set.seed(1)
  avg_temp <- data.frame(year_ref = rep(2001:2010, each = 365),
                         doy = rep(1:365, times = 10),
                         thermal.units = sample(0:40, 3650, replace=TRUE))

我在这个网站上也种了一种作物,如果在第 152 天种植,则需要 110 天才能成熟

  planting_date <- 152 
  observed_days_to_mature <- 110
  

我也有一些初步的随机猜测,关于这种作物在从种植开始到完全成熟的每个阶段通常可能积累多少热单位。例如在下面的例子中,第 1 阶段需要从种植开始积累 50 个热量单位,第 2 阶段需要 120 个热量单位,因为 种植,第 3 阶段需要 190 个热量单位,因为种植等等。

  gdd_data <- data.frame(stage_id = 1:4,
                         gdd_required = c(50, 120, 190, 250))
  

所以根据 gdd 要求,我可以计算每年这种作物需要多少天才能成熟。

  library(dplyr)
  library(data.table)

  days_to_mature_func <- function(gdd_data_df, avg_temp_df, planting_date_d){
    
    gdd.vec <- gdd_data_df$gdd_required 
    
    year_vec <- sort(unique(avg_temp_df$year_ref))
    
    temp_ls <- list()
    
    for(y in seq_along(year_vec)){
      
      year_id <- year_vec[y]
      
      weather_sub <- avg_temp_df %>% 
                     dplyr::filter(year_ref == year_id & 
                                   doy >= planting_date_d)  
      
      stage_vec <- unlist(lapply(1:length(gdd.vec), function(x)  planting_date_d - 1 + which.max(cumsum(weather_sub$thermal.units) >= gdd.vec[x])))
      
      stage_vec[length(stage_vec)] <- ifelse(stage_vec[length(stage_vec)] <= stage_vec[length(stage_vec) - 1], NA, stage_vec[length(stage_vec)])
      
      gdd_doy <- as.data.frame(t(as.data.frame(stage_vec)))
      
      names(gdd_doy) <- paste0('stage_doy', 1:length(stage_vec))
      gdd_doy$year_ref <- year_id
      temp_ls[[y]] <- gdd_doy
  }
    days_to_mature_mod <- rbindlist(temp_ls)
    return(days_to_mature_mod)
  }

  days_to_mature_mod <- days_to_mature_func(gdd_data, avg_temp, planting_date)
  days_to_mature_mod

  stage_doy1 stage_doy2 stage_doy3 stage_doy4 year_ref
  1:        154        160        164        167     2001
  2:        154        157        159        163     2002
  3:        154        157        160        162     2003
  4:        155        157        163        165     2004
  5:        154        156        160        164     2005
  6:        154        161        164        168     2006
  7:        154        156        159        161     2007
  8:        155        158        161        164     2008
  9:        154        156        160        163     2009
  10:       154        158        160        163     2010

由于作物需要 110 天才能成熟,我将误差定义为:

  error_mod <- mean(days_to_mature_mod$stage_doy4 - observed_days_to_mature)^2
  

我的问题是如何优化 gdd_data 中的 gdd_required 以产生最小的错误。

我实施的一种方法是遍历一系列因素,减少 gdd_required 每一步并计算误差。误差最低的因素是我应用的最终因素 gdd_required 数据。我正在阅读有关梯度下降算法的文章,该算法可能会使此过程更快,但不幸的是,我还没有足够的技术专长来实现这一目标。

来自评论: 我确实有一个不明确的条件 - 我正在优化的函数中的 x 是有序的,即 x[1] < x[2] < x[3] < x[4] 因为它们是累积的.

根据您的示例,您可以定义一个函数,该函数采用任意 gdd_required 和 returns 拟合:

optim_function <- function(x){
  gdd_data <- data.frame(stage_id = 1:4, gdd_required = x)
  days_to_mature_mod <- days_to_mature_func(gdd_data, avg_temp, planting_date)
  error_mod <- mean(days_to_mature_mod$stage_doy4 - observed_days_to_mature)^2
}

函数 optim 允许您找到达到最小值的参数,从您使用的初始设置开始,例如

optim(c(50, 120, 190, 250), optim_function)
#$par
#[1] 266.35738 199.59795 -28.35870  30.21135
# 
#$value
#[1] 1866.24
# 
#$counts
#function gradient 
#      91       NA 
#
#$convergence
#[1] 0
#
#$message
#NULL

因此,使用参数 266.35738、199.59795、-28.35870、30.21135 找到了 1866 年左右的最佳拟合。

帮助页面提供了一些关于进行约束优化的指示,如果它们在特定范围内很重要的话。

鉴于您认为参数应该严格递增的评论,您可以使用 cumsum(exp()) 将任意值转换为递增的值,这样您的代码将变为

optim_function_plus <- function(x){
  gdd_data <- data.frame(stage_id = 1:4, gdd_required = cumsum(exp(x)))
  days_to_mature_mod <- days_to_mature_func(gdd_data, avg_temp, planting_date)
  error_mod <- mean(days_to_mature_mod$stage_doy4 - observed_days_to_mature)^2
}

opt <- optim(log(c(50, 70, 70, 60)), optim_function_plus)
opt
# $par
# [1] 1.578174 2.057647 2.392850 3.241456
# 
# $value
# [1] 1953.64
# 
# $counts
# function gradient 
# 57       NA 
# 
# $convergence
# [1] 0
# 
# $message
# NULL

要使参数恢复到您感兴趣的比例,您需要执行以下操作:

cumsum(exp(opt$par))
# [1]  4.846097 12.673626 23.618263 49.189184