【问题标题】:Function Recursion in RR中的函数递归
【发布时间】:2020-11-05 01:27:45
【问题描述】:

我正在 R 中编写一个函数 (NextWordPrediction) 来预测给定一些单词的下一个单词。基本结构如下:

  1. 如果 dat 中存在输入,则 nrow(dat) != 0 返回输入并回答
  2. 如果输入不存在,nrow(dat) == 0 调用递归并尝试输入 1(例如,如果输入是“hello great world”,则尝试“great world”,依此类推,直到 nrow nrow(dat) != 0
  3. 如果在第 2 步之后 nrow(dat) == 0 返回字符串 "Word not in dictionary. We added this to our database!" 并将原始输入添加到数据集

这里是完整的代码:

NextWordPrediction <- function(input) {
        dat <- training %>%
                filter(., N_gram == str_count(input, "\\S+") + 1) %>%
                filter(grepl(paste("^", tolower(str_squish(input)), sep = ""), Word)) %>%
                arrange(., desc(Prop))
        
        if (nrow(dat) != 0) {
                assign("training",
                       training %>%
                               mutate(Frequency = ifelse(Word == input &
                                                                 N_gram == str_count(input, "\\S+"),
                                                         Frequency + 1,
                                                         Frequency)) %>%
                               group_by(., N_gram) %>%
                               mutate(., Prop = Frequency/ sum(Frequency)) %>%
                               data.frame(.),
                       envir = .GlobalEnv)
                
                val <- dat$Word_to_Predict[1]
                ans <- paste(str_squish(input), val)
                
                return(list(ans, head(dat,5)))
                
        } else if (nrow(dat) == 0 & word(input, 1) != "NA") {
                input_1 <-  Reduce(paste, word(input, 2:str_count(input,"\\S+")))
                
                return(NextWordPrediction(input_1)) 
                
        } else if (nrow(dat) == 0 & word(input, 1) == "NA") {
                assign("training",
                       training %>%
                               add_row(., Word = tolower(input), Frequency = 1, N_gram = str_count(input, "\\S+")),
                       envir = .GlobalEnv)
                ans <- paste("Word not in dictionary. We added this to our database!")
                return(ans)
        }
}

我遇到的问题发生在第 2 步和第 3 步之间。如果在递归调用后未找到输入,则添加到数据库的输入是 input-1(“伟大的世界”),我想要原来的输入(“你好伟大的世界”)。这是我第一次尝试实现递归,想了解我代码中的错误。

谢谢:)


更新可重现:

library(dplyr); library(stringr)

training <- data.frame(Word = c("hello", "she was great", "this is", "long time ago in"), Frequency = c(4, 3, 10, 1),
                       N_gram = c(1, 3, 2, 4), Prop = c(4/18, 3/18, 10/18, 1/18), Word_to_Predict = c(NA, "great", "is", "in"))

NextWordPrediction("she was") ## returns "she was" & "great"

NextWordPrediction("hours ago") ## returns "hours ago" & "in"

NextWordPrediction("words not in data") ## returns "Word not in dictionary. We added this to our database!" after trying "not in data", "in data" and adds "words not in data" to dataset

【问题讨论】:

  • 将删除的单词连接 (paste) 到递归调用的返回值。这意味着您需要存储第一个单词,对剩余的单词进行递归,然后在返回之前再次连接 word1。

标签: r function


【解决方案1】:

这是对字符串操作的递归函数的一个不完美且过于复杂的演示。理想情况下,可以采取更多的保护措施,当然还有更快、更有效、更智能的方法来完成这项任务,但是......也许你会明白这一点。

我要将所有es 更改为as,一次一个字。

e_to_a <- function(strings) {
  # unnecessarily complex
  message("# Called  : ", sQuote(strings))
  if (!nzchar(strings)) return(strings)
  word1 <- sub("^([^[:space:]]*)[[:space:]]?.*", "\\1", strings)
  others <- sub("^[^[:space:]]*[[:space:]]?", "", strings)
  message("# - word1 : ", sQuote(word1))
  message("# - others: ", sQuote(others))
  # operate on the first word
  word1 <- gsub("e", "a", word1)
  if (nzchar(others)) {
    others <- e_to_a(others)
    return(paste(word1, others))
  } else {
    return(word1)
  }
}

在行动:

e_to_a("hello great world")
# # Called  : 'hello great world'
# # - word1 : 'hello'
# # - others: 'great world'
# # Called  : 'great world'
# # - word1 : 'great'
# # - others: 'world'
# # Called  : 'world'
# # - word1 : 'world'
# # - others: ''
# [1] "hallo graat world"

关键是当你进行递归调用时,你当前正在做什么

return(NextWordPrediction(input_1)) 

将返回只是递归部分,忽略第一个单词。这和我做的类似

  if (nzchar(others)) {
    others <- e_to_a(others)
    # return(paste(word1, others))
    return(others)
  } else {
    return(word1)
  }

我希望你能把它应用到你的函数中。


底线,由于您的问题不可重现,我猜您的修复是这样的:

        } else if (nrow(dat) == 0 & word(input, 1) != "NA") {
                input_vec <- str_split(input, "\\s+")
                input_firstword <- input_vec[1]
                input_otherwords <- paste(input_vec[-1], collapse = " ")
                return(paste(input_firstword, NextWordPrediction(input_otherwords)))
                
        } else if (nrow(dat) == 0 & word(input, 1) == "NA") {

【讨论】:

  • 我不确定我是否理解到足以将其应用于我的函数
  • 查看我的编辑。如果这不起作用,那么我建议您使您的问题更具可重复性,包括 small、示例数据 (training),并列出您正在使用的所有非基础包。不过,我的前提是:您有意丢弃第一个单词并返回其余单词,您需要以某种方式保留第一个单词。
  • 已更新为可重现。我尝试了您的编辑并实现了您的前提,但我无法做到。我现在明白问题所在,但我无法制定解决方案。
【解决方案2】:

意识流答案。它没有解决任何问题,但它突出了一些可以或必须更改代码的区域。预先:== NA 失败;您总是在递归中丢弃第一个单词; NA(意思是“可以是任何东西”的对象)被强制转换为文字字符串 "NA"

  1. 从新的training 开始,我将debug(NextWordPrediction) 并逐行跟踪。它到达input_1 &lt;- ...,我注意到的第一件事是:

    • 第一次,input_1"great world"
    • 下次是"world";
    • 下次是"na world"失败

    这是一个典型的失败,主要体现在两个方面:

    • 代码假设有多个单词,即使str_count(input,"\\S+")在这里返回1;和
    • 假设2:... 一直在增加并且不会超过某个计数是一个常见的错误,但不幸的是2:1 返回c(2L, 1L) ...也许您应该在任意计数之前检查向量的长度过去。

    认为你试图通过你之前对word(input,1) != NA 的测试来防范这种情况(这也是一个错误),但唯一会发生的情况是input 是0 长度向量 (character(0)),不是空字符串 ""。使用当前代码你不会得到它,我认为你的意图是让它减少到 ""

    我要把你的word(input, 2:str_count(...))改成

    input_1 <- sub("^\\S*\\s?", "", input)
    
  2. 你有word(input, 1) != "NA"(和==),要么将R的本机对象误认为是字符串,要么你认为应该检查文字字符串"NA";诚然,英语不像真正的单词那样使用那么多,有些语言会。我不确定您是否打算将其用作 NA 文字,或者是否由于某种原因您的函数会将 NA 转换为 "NA" 并且您想防止这种情况发生。

    最后一个假设是解决症状,而不是问题。永远不要让你的函数返回"NA"(这发生在几个地方),你需要提防它。对我来说,看到一个 word "NA" 并将其与 R 原生 NA 区分开来是完全合理的。数据缺失对于区分很重要。

  3. 假设您的意思是 != NA 而不是 ...word(input, 1) != NA 将永远无法工作。让我们来看一些例子:

    word("hello", 1)
    # [1] "hello"
    word("", 1)
    # [1] ""
    word(c(), 1)
    # Warning in rep(string, length.out = n) :
    #   'x' is NULL so the result will be NULL
    # Error in mapply(function(word, loc) word[loc, "start"], words, start) : 
    #   zero-length inputs cannot be mixed with those of non-zero length
    word(character(0), 1)
    # [1] NA
    

    好的,所以它可以返回一个NA,当输入向量是一个长度为0的字符向量,但是...

    word(character(0), 1) == NA
    # [1] NA
    word(character(0), 1) == NA_character_
    # [1] NA
    

    没错,你不能这样检查NA-ness。 (你知道NA有六种以上吗?它们不一样,identical(NA, NA_real_)。)

    使用is.na(.):

    is.na(word(character(0), 1))
    # [1] TRUE
    

    (假设我们可以在正常操作中看到它。)

    我要将 if 条件更改为:

    } else if (nrow(dat) == 0 && nzchar(input) && !is.na(word(input, 1))) {
    
  4. 我们越来越近了。现在我可以进入函数的第三个调用,其中input 最终是"",我们进入第一个条件块,将新内容分配给training。不幸的是,dat$Word_to_Predict[1]NA,所以你的 ans" NA",这似乎不合逻辑。当然,您的默认 training 数据集明确表示,虽然我不知道您的意思是在这里发生什么,但我建议将 NA 的 R 对象字符串化为 " NA" 似乎是错误的。

    不过,我对此流程没有基本解决方法:您想将找到的 val 与前一个 input 字符串连接起来,但是......如果 Word_to_PredictNA(不是正常的字符串),然后......你做什么?为了继续前进,我将不考虑将"NA" 连接到一个字符串上……尽管我相信从语言的角度来看它会产生“错误”的结果。 (我只会将"NA" 解释为"(I don't have a great value for this spot)" 或类似的:-)

  5. 总是 pasteing 一个压缩的 inputval,但是......如果 input"",那么 paste 仍然在它们之间添加一个空格,这似乎没有必要。您以后总是可以通过反复挤压琴弦来“修补”这个问题,但是......再次出现症状/问题。我建议改为使用

    ans <- str_squish(paste(input, val))
    
  6. 还有我的原话……

    当您以"she was" 开头时,它会在第一次调用时找到一些东西,我们通过paste inputval 来获得答案。但是,当您必须进行递归时,您会在句子的其余部分再次调用该函数并完美丢弃第一个单词。例如:

    NextWordPrediction("hello great world")
    #1> `input` is "hello great world", second `if` block, `input_1` is "great world"
    #2> `input` is "great world", second `if` block, `input_1` is "world"
    #3> `input` is "world", second `if` block, `input_1` is `""`
    #4> `input` is "", first `if` block, `val` is `NA`, and `ans` is "NA"
    #3> blindly returns list("NA", head(dat)) (discarding "world")
    #2> blindly returns list("NA", head(dat)) (discarding "great")
    #1> blindly returns list("NA", head(dat)) (discarding "hello")
    

    您现在看到问题了吗?您需要捕获结果,而不是 return(NextWordPrediction(input_rest)),在前面加上您从 input 中剥离的单词,然后继续将更新的返回值传递到链上。我建议

    input_1 <- gsub("\\s\\S*", "", input)
    input_rest <- sub("^\\S*\\s?", "", input)
    
    out <- NextWordPrediction(input_rest)
    out[[1]] <- str_squish(paste(input_1, out[[1]]))
    return(out)
    

在这一切之后,我现在明白了

NextWordPrediction("hello great world")
# [[1]]
# [1] "hello great world NA"
# [[2]]
#    Word Frequency N_gram Prop Word_to_Predict
# 1 hello         4      1    1            <NA>

根据您最初的training,这是正确的。

不幸的是,这破坏了其他东西。


  1. "words not in data" 最终总是匹配某些东西(training 中没有的东西也一样),因为它会简化为一个空字符串 "",而您的 grepl(paste("^", tolower(str_squish(input)), sep = ""), Word) 的第一个逻辑将始终与 @ 的 input 匹配987654404@.

    我们可以在您的第一次过滤中通过一个简单的附加条件来解决此问题:

      filter(nzchar(input) & grepl(paste("^", tolower(str_squish(input)), sep = ""), Word)) %>%
    
  2. 最后,当你到达最后一个if块时,当你需要向training添加数据时,如果这是函数的第一个/外部调用,那么input真正反映了整个句子,即是你想要的。但是,如果您已经进行了一次或多次递归调用,那么 input 只是链中的一个词,而不是全部。而且由于上面的一些假设,在这个阶段input"",所以......任何添加都是无用的。

    有两种处理方法:

    1. 跟踪这是外部(第一个)调用还是某个内部调用。当你递归调用时,检查返回值......如果为空并且这是一个内部调用,则返回空;如果为空且这是第一个/外部调用,附加到training;或
    2. 始终将整个字符串与当前的input 一起传递。这将推翻我在上面第 6 条中的建议,因此您的第二个 if 块只会调用 NextWordPrediction(input_rest, input_1)(使用我的变量)而不是 str_squish 在它之后。挤压/粘贴将在第一个 if 块中处理,您需要在其中添加 preceding 的值(如果有)。
    NextWordPrediction <- function(input, preceding = "") {
    

旁注,本身没有错,但仍然不好。

  • &amp; (single) in an if 条件工作 但不好的做法:&amp; 执行向量逻辑,这意味着它可以返回长度不是 1 的向量; if 条件的长度必须正好为 1,而不是 0 或 2 或更多。在此处使用&amp;&amp;
  • Reduce(paste, ...) 是不必要的。使用paste(...)

【讨论】:

  • 谢谢!我相信您的回答让我能够以更简单的方式解决我的问题!
  • @kevinn-12,在某些时候,请考虑accepting 我的两个答案之一。有时保持问题“开放”是一种策略,希望得到更多答案,但在某些时候,结束一个问题被认为是“好的”。这样做不仅为回答者提供了一些积分,而且还为有类似问题的读者提供了一些关闭。虽然您只能接受一个答案,但您可以选择对您认为有帮助的人投票。
【解决方案3】:

感谢@r2evans,在了解了我的函数中递归的含义后,我意识到通过递归的解决方案过于复杂,因此以下代码符合我的所有条件并按预期工作:

NextWordPrediction <- function(input) {
        dat <- training %>%
                filter(., N_gram == str_count(input, "\\S+") + 1) %>%
                filter(grepl(paste("^", tolower(str_squish(input)), sep = ""), Word)) %>%
                arrange(., desc(Prop))
        
        if (nrow(dat) != 0) {
                assign("training",
                       training %>%
                               mutate(Frequency = ifelse(Word == input &
                                                                 N_gram == str_count(input, "\\S+"),
                                                         Frequency + 1,
                                                         Frequency)) %>%
                               group_by(., N_gram) %>%
                               mutate(., Prop = Frequency/ sum(Frequency)) %>%
                               data.frame(.),
                       envir = .GlobalEnv)
                
                val <- dat$Word_to_Predict[1]
                ans <- paste(str_squish(input), val)
                
                return(list(ans, head(dat,5)))
                
        } else {
                for (i in 2:str_count(input, "\\S+")) {
                        input_1 <-  word(input, start = i, end =  str_count(input,"\\S+"))
                        
                        dat <- training %>%
                                filter(., N_gram == str_count(input_1, "\\S+") + 1) %>%
                                filter(grepl(paste("^", tolower(str_squish(input_1)), sep = ""), Word)) %>%
                                arrange(., desc(Prop))
                        if (nrow(dat) != 0) {
                                val <- dat$Word_to_Predict[1]
                                ans <- paste(str_squish(input), val)
                                
                                return(list(ans, head(dat,5)))
                                
                        } else if (nrow(dat) == 0 & i == str_count(input, "\\S+")) {
                                assign("training",
                                       training %>%
                                               add_row(., Word = tolower(input), Frequency = + 1, N_gram = str_count(input, "\\S+"), 
                                                       Word_to_Predict = word(input, -1)) %>%
                                               group_by(., N_gram) %>%
                                               mutate(., Prop = Frequency/ sum(Frequency)) %>%
                                               data.frame(.),
                                       envir = .GlobalEnv)
                                
                                ans <- paste("Word not in dictionary. We added this to our database!")
                                
                                return(ans)
                        }
                }
        }
}

它遍历 input-1 直到在数据帧中找到一个值,当这种情况发生时返回答案,否则我们将原始输入添加到数据帧。

【讨论】:

    猜你喜欢
    • 2013-04-21
    • 2013-08-12
    • 1970-01-01
    • 1970-01-01
    • 2015-09-12
    • 1970-01-01
    • 2016-05-02
    • 1970-01-01
    • 2016-01-23
    相关资源
    最近更新 更多