packages feed

duckling-0.2.0.0: exe/Duckling/Ranking/Train.hs

-- Copyright (c) 2016-present, Facebook, Inc.
-- All rights reserved.
--
-- This source code is licensed under the BSD-style license found in the
-- LICENSE file in the root directory of this source tree.


{-# LANGUAGE RecordWildCards #-}

module Duckling.Ranking.Train
  ( makeClassifiers
  ) where

import Data.HashSet (HashSet)
import Prelude
import qualified Data.HashMap.Strict as HashMap
import qualified Data.HashSet as HashSet
import qualified Data.List as List

import Duckling.Engine
import Duckling.Ranking.Extraction
import Duckling.Ranking.Types
import Duckling.Resolve
import Duckling.Testing.Types
import Duckling.Types

-- -----------------------------------------------------------------
-- Probabilistic layer
-- Naive Bayes classifier with Laplace smoothing
-- Train one classifier per rule, based on the test corpus.

makeClassifiers :: [Rule] -> Corpus -> Classifiers
makeClassifiers rules corpus = HashMap.map train $ makeDataset rules corpus

-- | Train a classifier for a single rule
train :: [Datum] -> Classifier
train datums = Classifier {okData = okClass, koData = koClass}
  where
    total = List.length datums
    (ok, ko) = List.partition snd datums
    merge :: [BagOfFeatures] -> BagOfFeatures -> BagOfFeatures
    merge xs m = List.foldl' (HashMap.unionWith (+)) m xs
    okCounts = merge (map fst ok) HashMap.empty
    koCounts = merge (map fst ko) HashMap.empty
    vocSize = HashMap.size $ HashMap.union okCounts koCounts
    okClass = makeClass okCounts total (List.length ok) vocSize
    koClass = makeClass koCounts total (List.length ko) vocSize

-- | Compute prior and likelihoods log-probabilities for one class.
makeClass :: BagOfFeatures -> Int -> Int -> Int -> ClassData
makeClass feats total classTotal vocSize = ClassData
  { prior = prior
  , unseen = unseen
  , likelihoods = likelihoods
  , n = classTotal
  }
  where
    prior = log $ fromIntegral classTotal / fromIntegral total
    denum = vocSize + sum (HashMap.elems feats)
    unseen = log $ 1.0 / (fromIntegral denum + 1.0)
    likelihoods = HashMap.map (\x ->
      log $ (fromIntegral x + 1.0) / fromIntegral denum
      ) feats

-- | Augment the dataset with one example.
-- | Add all the nodes contributing to the resolutions of the input sentence.
-- | Classes:
-- | 1) True (node contributed to a token passing test predicate)
-- | 2) False (node didn't contribute to any passing tokens)
makeDataset1 :: [Rule] -> Context -> Options -> Dataset -> Example -> Dataset
makeDataset1 rules context options dataset (sentence, predicate) = dataset'
  where
    tokens = parseAndResolve rules sentence context options
    (ok, ko) = List.partition (predicate context) tokens
    subnodes :: Node -> HashSet Node
    subnodes node@(Node{..}) = case children of
      [] -> HashSet.empty
      cs -> HashSet.unions $ HashSet.singleton node:map subnodes cs
    nodesOK = HashSet.unions $ map (subnodes . node) ok
    nodesKO = HashSet.difference
      (HashSet.unions $ map (subnodes . node) ko) nodesOK
    updateDataset :: Class -> HashSet Node -> Dataset -> Dataset
    updateDataset klass nodes dataset =
      HashSet.foldl' (\dataset node@Node {..} ->
        case rule of
          Just rule -> HashMap.insertWith (++) rule
            [(extractFeatures node, klass)] dataset
          Nothing -> dataset
        ) dataset nodes
    dataset' = updateDataset False nodesKO $ updateDataset True nodesOK dataset

-- | Build a dataset (rule name -> datums)
makeDataset :: [Rule] -> Corpus -> Dataset
makeDataset rules (context, options, examples) =
   List.foldl' (makeDataset1 rules context options) HashMap.empty examples