【问题标题】:Need to reassign the value of a variable at the end of my function in Clojure需要在 Clojure 中的函数末尾重新分配变量的值
【发布时间】:2021-02-04 12:10:30
【问题描述】:

我是 Clojure 领域的新手,也是函数式编程的新手。我正在尝试编写一个函数来计算给定词汇表(只是单词列表)和一组概率(每个单词出现的概率)的特定单词列表出现的概率。我正在使用简化的词袋模型,并且假设每个结果都是独立的。

例如,给定:

  • 词汇(相关概率):sleep (0.3)、dog (0.09)、a (0.2)、the (0.05)、cow (0.17)、boat (0.04)、everything (0.15)
  • 句子:(list 'the 'dog 'boat)

我希望它计算 (0.05) * (0.09) * (0.04) = 0.00018

我已经有一个函数可以获取每个单词的概率,它可以按预期工作。我把它贴在这里供参考:

(defn lookup-probability [w outcomes probs]
  (if (not= w (first outcomes)) ;;if the current element is not equal to the word we're looking for...

    (lookup-probability w (rest outcomes) (rest probs)) ;;...keep cycling through the vocabulary

    (first probs) ;;once we find the right word, fetch the corresponding entry in the probability list
  )
)

这是我感到困惑的部分:

(def sentenceprobs '()) ;;STEP 1
(defn compute-BOW-prob [sentence vocabulary probabilities]
  (if (not(empty? sentence))
      (def sentenceprobs (conj sentenceprobs (lookup-probability (first sentence) vocabulary probabilities)) ;;STEP 2
      (compute-BOW-prob (rest sentence) vocabulary probabilities) ;;STEP 3
    )
    (product sentenceprobs) ;;STEP 4 (the product function just multiplies all the elements of a list together)
  )
)

这是我的总体策略:

  1. 首先定义一个空列表“sentenceprobs”,我将在其中存储句子中每个单词的概率
  2. 如果句子非空,则将第一个单词的概率添加到列表“sentenceprobs”
  3. 在句子的其余部分递归调用函数(减去我们刚刚找到概率的单词,ofc)
  4. 一旦句子为空,即我们已经获取了每个单词的概率,返回“sentenceprobs”中所有元素的乘积

如果我只想使用该功能一次,这很好用。但是,如果我想多次调用它,sentenceprobs 仍然包含上一次调用的所有概率。该函数仍将运行,但它只是给了我错误的概率(小得多)。 所以我尝试在函数的最后重置 sentenceprobs 的值以使其“可重用”:

(def sentenceprobs '())
(defn compute-BOW-prob [sentence vocabulary probabilities]
  (if (not(empty? sentence))
      (def sentenceprobs (conj sentenceprobs (lookup-probability (first sentence) vocabulary probabilities))
      (compute-BOW-prob (rest sentence) vocabulary probabilities)
    )
    (product sentenceprobs)
  )
  (def sentenceprobs '()) ;; <---THIS IS WHAT I ADDED
)

当我这样做时,该函数根本不会返回任何内容。从某种意义上说,这是意料之中的,因为函数必须在此列表上返回一个操作,因此将其设为空可能会搞砸。但我认为,由于我在退出 if 语句之前递归并返回一个值,所以这不是问题。我想我错了哈哈。

我在 Internet 上进行了一些探索,似乎这不是 def 在 Clojure 中的工作方式,但我不知道如何解决它。有谁知道我怎样才能做到这一点?非常感谢。

【问题讨论】:

  • 只是一般提示:切勿在 defn 或类似名称中使用 def。仅使用 def 作为顶级元素。 (总有一天你可以做到,但到那时你就会对 Clojure 有足够的了解。所以,现在不要这样做。)

标签: clojure functional-programming nlp


【解决方案1】:

就像您提到的那样,这不是使用def 的方式。您尝试创建一个列表,然后从多个断开连接的函数调用中追加内容。这是命令式语言的方式,而不是函数式语言的方式。在这里,我们宁愿匿名创建一个列表,并将其作为函数的返回值传递。

我尝试运行你的代码,但 compute-BOW-prob 没有编译,所以我不确定你期望它如何工作。

无论如何,这里有一些改进点。

在第一个版本中,我尝试尽可能少地修改您的原始设计(def 不得不去)。 在您的设计中,您尝试仅在基本情况(空句)中退回产品。这对于递归函数来说不是一个好的设计,它们应该总是返回相同类型的值。在comp-bow 中,这是通过返回 1 的基本情况以及在您向上移动调用函数时不断乘以概率来解决的。

(defn comp-bow [sentence vocabulary probabilities]
  (if (not (empty? sentence))
    (* (comp-bow (rest sentence) vocabulary probabilities)
       (lookup-probability (first sentence) vocabulary probabilities))
    1))

不知道你是否熟悉let。这是你在 Clojure 中分配东西的最后一件事,分配只存在于 let 列表中。 此设计与您的设计非常相似,因为它会创建概率列表并最终只执行乘法。 (我使用apply * 而不是你的product。这里我将返回列表的函数和返回产品的函数分开(如上所述)。这里的let 仅用于说明目的。

(defn comp-bow2 [sentence vocabulary probabilities]
  (apply * (comp-bow2-sub sentence vocabulary probabilities)))

(defn comp-bow2-sub [sentence vocabulary probabilities]
  (if (not (empty? sentence))
    (let [sentenceprobs (comp-bow2-sub (rest sentence) vocabulary probabilities)
          word-prob (lookup-probability (first sentence) vocabulary probabilities)]
      (conj sentenceprobs word-prob))))

如果您要进行递归函数调用,您应该了解recur。由于 Clojure 在 JVM 上运行,如果您对许多递归函数调用进行操作,您可能会遇到麻烦。 recur 通过在调用时删除当前堆栈帧来避免这种情况,但要做到这一点,您只能将 recur 调用放在函数的最后,以便在通过调用函数向上移动时不需要函数堆栈帧。 这与我的第一个建议有点相似,但不同之处在于我开始直接将概率乘以 1,以便可以使用 recur

(defn comp-bow3 [sentence vocabulary probabilities]
  (comp-bow3-sub sentence vocabulary probabilities 1))

(defn comp-bow3-sub [sentence vocabulary probabilities product]
  (if (empty? sentence)
    product
    (recur (rest sentence) vocabulary probabilities
           (* product (lookup-probability (first sentence) vocabulary probabilities)))))

你没有必要使用递归来完成这个(尽管它很有趣)。 mvarela 建议的基于reduce 的解决方案可能对大多数人来说更清晰。

【讨论】:

    【解决方案2】:

    只是根据 Alan 的回应建立一点点。在这种情况下,您有一个值列表(一个句子中的单词),并且您想要计算一个聚合(所有这些单词一起发生的概率,根据之前的一些概率计算)。我假设您已经像 Alan 那样构建了您的概率表,就像 Alan 所做的那样(尽管我使用字符串而不是关键字作为键)。

    要执行聚合,我们将使用reduce,它允许您将集合折叠为单个值。它通过使用一个接受累加器和一个值的函数并将其应用于集合中的所有元素来实现这一点。

    代码如下所示:

    (def prob-map
      {"sleep"      0.3
       "dog"        0.09,
       "a"          0.2,
       "the"        0.05,
       "cow"        0.17,
       "boat"       0.04,
       "everything" 0.15})
    
    (defn compute-BOW-prob [probs sentence]
      (reduce (fn [acc word]
                (* acc (get probs word 1)))
              1
              (clojure.string/split sentence #"\s")))
    
    (compute-BOW-prob prob-map "the dog boat")
    ;; => 1.7999999999999998E-4
    

    它本质上与 Alan 的解决方案相同,但它没有用于乘以概率的单独步骤(这也为您节省了一个中间列表,在这种情况下这很可能不是问题,但如果您有非常大的输入)。

    上面的代码将概率图和一个句子作为输入。然后它拆分句子(我只是使用空格作为分隔符,但您可以根据需要添加标点符号和停用词),并使用提供的函数减少列表。该函数采用累加器 (acc) 和列表元素 (word),并将累加器乘以该单词的概率(或 1,如果未找到该单词……您可以采用不同的当然,如何处理这个问题的方法)。函数下方的1acc 将取的初始值。

    希望这有助于澄清您的想法! 一般来说,你根本不需要修改变量,你绝对不应该在函数内部使用def。另外,尽量避免显式使用全局变量,并让你的函数将它们作为参数。

    【讨论】:

      【解决方案3】:

      请参阅this template project。它展示了我喜欢如何组织一个项目(只需克隆该存储库并开始编码!)。尤其要研究the list of documentation,看看Clojure 和命令式语言之间的区别。

      我会通过使用地图来保存概率来解决这个问题。然后,您可以使用mapv(或只是map)将概率从地图中拉出到向量(或列表)中。然后使用(apply * ...) 计算乘积:

      (ns tst.demo.core
        (:use tupelo.test)
        (:require
          [tupelo.core :as t]))
      
      (def prob-map
        {:sleep      0.3
         :dog        0.09,
         :a          0.2,
         :the        0.05,
         :cow        0.17,
         :boat       0.04,
         :everything 0.15})
      
      (defn calc-prob
        [words]
        (let [probs (mapv #(get prob-map %) words)]
          (apply * probs)))
      
      (dotest
        (let [sentence [:the :dog :boat]
              result   (calc-prob sentence)
              expected (t/spyx (* 0.05 0.09 0.04))  ; spyx displays the value
              ]
          (is (t/rel=  result expected :digits 8))))
      

      您可以通过以下方式运行它:

      > lein clean
      > lein test
      

      产生输出:

      --------------------------------------
         Clojure 1.10.2-alpha1    Java 15
      --------------------------------------
      
      Testing tst.demo.core
      (* 0.05 0.09 0.04) => 1.7999999999999998E-4
      
      Ran 2 tests containing 1 assertions.
      0 failures, 0 errors.
      

      【讨论】:

      • 如果函数中的全局变量和局部变量不使用相同的名称probs,这可能会更清楚。对于初学者来说,不同的范围可能并不明显。
      • 好收获;固定。
      • 使用map 后跟apply * 听起来您想改用reduce。通常,如果您想将集合“折叠”到单个值,reduce(或transduce,如果您想使用传感器,但这可能超出了 OP 的范围)是要使用的工具。
      猜你喜欢
      • 2014-12-25
      • 1970-01-01
      • 1970-01-01
      • 2023-04-06
      • 1970-01-01
      • 2022-08-10
      • 1970-01-01
      • 1970-01-01
      • 2012-05-09
      相关资源
      最近更新 更多