为什么空函数组合在 Haskell 中有效?

Why does empty function composition work in Haskell?

我花了很长时间没有编程 Haskell,并决定通过从事一个相对高级的项目重新投入其中。我正在尝试按照 this guide 从头开始​​编写神经网络程序。我对他解决简单问题(例如创建权重和偏差网络)的一些最深奥的方法摸不着头脑,但说到这个:

feed :: [Float] -> [([Float], [[Float]])] -> [Float]
feed input brain = foldl' (((relu <$>) . ) . zLayer) input brain

我不明白他在做什么。更具体地说,我不明白为什么在这里的函数组合中使用两个.。他使用 (relu <$>) . )。这个 . 后跟一个括号对我来说没有意义。我理解它代表函数组合,在这种情况下,函数 zLayer 接受一层神经元,类型为 ([Float], [[Float]]) 和前一层的输出,类型为 [Float],并且产生一个新的输出,也是 [Float] 类型。我知道他将 relu <$> 函数应用于 zLayer 的结果,这是有道理的。也就是说,你想通过在大脑的一层上应用 zLayer,然后在结果上应用 relu <$>,最后通过它来折叠大脑(它只是一个层列表)作为下一层的input

看似空洞的构图让我很烦恼。我上面描述的,对我来说,应该这样实现:

feed :: [Float] -> [([Float], [[Float]])] -> [Float]
feed inp brain = foldl' (((sigmoid <$>) . computeLayer) inp brain

(我使用的是 sigmoid 函数而不是整流器 (ReLU),computeLayer 只是我对 zLayer 的实现。)对吧?我在那里做的是(据说)提供,作为 foldl' 的函数,这个:

(sigmoid <$> (computeLayer))

当我在我的 .computeLayer 之间添加 .) 时(当然还有一个左括号),它起作用了。没有它们,这是错误:

net.hs:42:42: error:
    • Couldn't match type ‘[Float]’ with ‘Float’
      Expected type: [Float] -> ([Float], [[Float]]) -> Float
        Actual type: [Float] -> ([Float], [[Float]]) -> [Float]
    • In the second argument of ‘(.)’, namely ‘computeLayer’
      In the first argument of ‘foldl'’, namely
        ‘((sigmoid <$>) . computeLayer)’
      In the expression: foldl' ((sigmoid <$>) . computeLayer) inp brain
   |
42 | feed inp brain = foldl' ((sigmoid <$>) . computeLayer) inp brain
   |                                          ^^^^^^^^^^^^

为什么这个看似空洞的函数组合会起作用?

这是目前为止的全部代码,如果有帮助的话:

import System.Random
import Control.Monad
import Data.Functor

foldl' f z []     = z
foldl' f z (x:xs) = let z' = z `f` x
                    in seq z' $ foldl' f z' xs

sigmoid :: Float -> Float
sigmoid x = 1 / (1 + (exp 1) ** (-x))

-- Given a list, gives out a list of lists of length *each element of the list*
makeBiases :: [Int] -> Float -> [[Float]]
makeBiases x b = flip replicate b <$> x

-- Given a list, gives out, for each element X in the list, a list of length x + 1, of
-- x elements in any normal distribution
makeWeights :: [Int] -> Float -> [[[Float]]]
makeWeights xl@(_:xs) el = zipWith (\m n -> replicate n (replicate m el)) xl xs

-- Make initial biases and weights to give a list of tuples that corresponds to biases
-- and weights associated with each node in each layer
makeBrain :: [Int] -> Float -> Float -> [([Float], [[Float]])]
makeBrain (x:xs) b el = zip (makeBiases xs b)  (makeWeights (x:xs) el)

-- Given output of a layer, apply weights and sum for all nodes in a layer. For each list
-- of weights (each node has multiple inputs), there will be one output
sumWeightsL l wvs = sum . zipWith (*) l <$> wvs

-- Given output of a layer, apply weights to get tentative output of each node. Then
-- sum biases of each node to its output
computeLayer :: [Float] -> ([Float], [[Float]]) -> [Float]
computeLayer l (bs, wvs) = zipWith (+) bs (sumWeightsL l wvs)

feed :: [Float] -> [([Float], [[Float]])] -> [Float]
feed inp brain = foldl' ((sigmoid <$>) . computeLayer) inp brain

main = do
  putStrLn "3 inputs, a hidden layer of 4 neurons, and 2 output neurons:"
  print $ feed [0.1, 0.2, 0.3] (makeBrain [3,4,2] 0 0.22)

正如@Bergi 指出的那样,表达式 ((relu <$>) . ) 不是 "empty function composition" 而是一种叫做 "section" 的东西。 (实际上,在这种情况下,它是一个嵌套在另一个部分中的部分。)

你以前肯定见过这个,即使你忘记了它叫什么and/or没有意识到它适用于函数组合运算符(.),只是提醒你...

在Haskell中,对于任何二元运算符(如(+)),你可以写左或右"section":

(1+)  -- short for   \x -> 1+x
(+1)  -- short for   \x -> x+1

这样 map (2*) mylist 之类的东西就可以用来加倍列表的每个元素,而不必写成 map (\x -> 2*x) mylist.

函数组合(.)和fmap运算符(<$>)的工作方式相同,所以:

((sigmoid <$>) . )

是以下简称:

\f -> (sigmoid <$>) . f

这是以下简称:

\f -> (\xs -> sigmoid <$> xs) . f

你可以扩展到:

\f z -> (\xs -> sigmoid <$> xs) (f z)

然后简化为:

\f z -> sigmoid <$> f z    :: (a -> [Float]) -> a -> [Float]

请注意,相比之下,您想在其位置使用的表达式 (sigmoid <$>) 等效于:

\xs -> sigmoid <$> xs    :: [Float] -> [Float]

这显然不一样。

总之,这一切意味着折叠函数:

(((sigmoid <$>) .) . computeLayer)

可以eta扩展和简化如下:

\acc x -> (((sigmoid <$>) .) . computeLayer) acc x
\acc x -> ((sigmoid <$>) .) (computeLayer acc) x
\acc x -> (\f z -> sigmode <$> f z) (computeLayer acc) x
\acc x -> sigmoid <$> (computeLayer acc) x
\acc x -> sigmoid <$> computeLayer acc x

并且您可以快速验证修改后的定义:

feed inp brain = foldl' (\acc x -> sigmoid <$> computeLayer acc x) inp brain

类型检查并在您的程序中给出相同的结果。

归根结底,您的直觉基本没问题。您希望折叠函数是 sigmoidcomputeLayer 函数的组合,但 computeLayer 接受两个参数而不是一个参数这一事实意味着简单组合不起作用。

为了您的娱乐,以下内容也适用:

feed inp brain = foldl' (((.).(.)) (sigmoid <$>) computeLayer) inp brain