packages feed

punkt (empty) → 0.1.0

raw patch · 6 files changed

+467/−0 lines, 6 filesdep +arraydep +basedep +mtlsetup-changed

Dependencies added: array, base, mtl, punkt, regex-tdfa, regex-tdfa-text, tasty, tasty-hunit, tasty-quickcheck, text, unordered-containers

Files

+ LICENSE view
@@ -0,0 +1,22 @@+The MIT License (MIT)++Copyright (c) 2014 http://github.com/bryant++Permission is hereby granted, free of charge, to any person obtaining a copy+of this software and associated documentation files (the "Software"), to deal+in the Software without restriction, including without limitation the rights+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell+copies of the Software, and to permit persons to whom the Software is+furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in+all copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN+THE SOFTWARE.+
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ lib/NLP/Punkt.hs view
@@ -0,0 +1,294 @@+{-# LANGUAGE OverloadedStrings #-}++module NLP.Punkt where++import qualified Data.Text as Text+import Data.Text (Text)+import Data.Maybe (catMaybes, fromMaybe)+import Data.HashMap.Strict (HashMap)+import Data.Char (isLower, isAlpha, isSpace)+import qualified Data.HashMap.Strict as Map+import qualified Data.List as List+import Control.Applicative ((<$>), (<*>), (<|>))+import qualified Control.Monad.Reader as Reader++import NLP.Punkt.Match (re_split, re_split_pos, intrasep, word_seps)++data OrthoFreq = OrthoFreq {+    freq_lower :: Int,+    freq_upper :: Int,+    freq_first_lower :: Int,+    freq_internal_upper :: Int,+    freq_after_ender :: Int+    }+    deriving Show++data PunktData = PunktData {+    type_count :: HashMap Text Int,  -- abbreviation counter+    ortho_count :: HashMap Text OrthoFreq,+    collocations :: HashMap (Text, Text) Int,+    total_enders :: Int,+    total_toks :: Int+    }+    deriving Show++data Entity a = Word a Bool | Punct a | ParaStart | Ellipsis | Dash+    deriving (Eq, Show)++data Token = Token {+    offset :: Int,+    toklen :: Int,+    entity :: Entity Text,+    sentend :: Bool,+    abbrev :: Bool+    }+    deriving Show++type Punkt = Reader.Reader PunktData++norm :: Text -> Text+norm = Text.toLower++is_initial :: Token -> Bool+is_initial (Token {entity=Word w True}) =+    Text.length w == 1 && isAlpha (Text.head w)+is_initial _ = False++is_word :: Token -> Bool+is_word tok = case entity tok of { Word _ _ -> True; _ -> False; }++-- dunning log likelihood modified by kiss/strunk+strunk_log :: Double -> Double -> Double -> Double -> Double+strunk_log a b ab n = -2 * (null - alt)+    where+    null = ab * log p1 + (a - ab) * log (1 - p1)+    alt = ab * log p2 + (a - ab) * log (1 - p2)+    (p1, p2) = (b / n, 0.99)++-- vanilla dunning log likelihood+dunning_log :: Double -> Double -> Double -> Double -> Double+dunning_log a b ab n | b == 0 || ab == 0 = 0+                     | otherwise = -2 * (s1 + s2 - s3 - s4)+    where+    (p0, p1, p2) = (b / n, ab / a, (b - ab) / (n - a))+    s1 = ab * log p0 + (a - ab) * log (1 - p0)+    s2 = (b - ab) * log p0 + (n - a - b + ab) * log (1 - p0)+    s3 = if a == ab then 0 else ab * log p1 + (a - ab) * log (1 - p1)+    s4 = if b == ab then 0 else+        (b - ab) * log p2 + (n - a - b + ab) * log (1 - p2)++ask_type_count = Reader.liftM type_count Reader.ask+ask_total_toks = Reader.liftM (fromIntegral . total_toks) Reader.ask+ask_total_enders = Reader.liftM (fromIntegral . total_enders) Reader.ask++ask_ortho :: Text -> Punkt OrthoFreq+ask_ortho w_ = return . Map.lookupDefault (OrthoFreq 0 0 0 0 0) (norm w_)+               =<< fmap ortho_count Reader.ask++ask_colloc :: Text -> Text -> Punkt Double+ask_colloc w0_ w1_ =+    return . fromIntegral . Map.lookupDefault 0 (norm w0_, norm w1_)+    =<< collocations <$> Reader.ask++-- c(w, ~.)+freq :: Text -> Punkt Double+freq w_ = ask_type_count >>= return . fromIntegral . Map.lookupDefault 0 w+    where w = norm w_++-- c(w, .)+freq_snoc_dot :: Text -> Punkt Double+freq_snoc_dot w_ = freq wdot where wdot = w_ `Text.snoc` '.'+-- potential slowdown if ghc doesn't know that norm "." == "."++-- c(w) == c(w, .) + c(w, ~.)+freq_type :: Text -> Punkt Double+freq_type w_ = (+) <$> freq w_ <*> freq_snoc_dot w_++dlen :: Text -> Double+dlen = fromIntegral . Text.length++-- probability that (w_ `snoc` '.') is an abbreviation.+prob_abbr :: Text -> Punkt Double+prob_abbr w_ = compensate =<< strunk_log <$> freq_type w_ <*> freq "."+                                         <*> freq_snoc_dot w_ <*> ask_total_toks+    where+    compensate loglike = do+        f_penalty <- do+            p <- freq w_  -- c(w, ~.)+            return $ 1 / dlen (Text.filter (/= '.') w_) ** p+        return $ loglike * f_len * f_periods * f_penalty+    f_len = 1 / exp (dlen $ Text.filter (/= '.') w_)+    f_periods = 1 + dlen (Text.filter (== '.') w_)++-- decides if w is a sentence ender based on its capitalization+decide_ortho :: Text -> Punkt (Maybe Bool)+decide_ortho w_ = ask_ortho w_ >>= return . decide' w_+    where+    decide' w_ wortho+        | title && ever_lower && never_title_internal = Just True+        | lower && (ever_title || never_lower_start) = Just False+        | otherwise = Nothing+        where+        (lower, title) = (isLower $ Text.head w_, not lower)+        ever_lower = freq_lower wortho > 0+        never_title_internal = freq_internal_upper wortho == 0+        ever_title = freq_upper wortho > 0+        never_lower_start = freq_first_lower wortho == 0++-- special orthographic heuristic for post-possible-initial tokens.+decide_initial_ortho :: Text -> Punkt (Maybe Bool)+decide_initial_ortho w_ = do+    neverlower <- (== 0) . freq_lower <$> ask_ortho w_+    orthosays <- decide_ortho w_+    return $ orthosays <|> if neverlower then Just False else Nothing++-- probability that w_ is a frequent sentence starter+prob_starter :: Text -> Punkt Double+prob_starter w_ = dunning_log <$> ask_total_enders <*> freq_type w_+                              <*> fafterend <*> ask_total_toks+    where fafterend = fromIntegral . freq_after_ender <$> ask_ortho w_++prob_colloc :: Text -> Text -> Punkt Double+prob_colloc w_ x_ = dunning_log <$> freq_type w_ <*> freq_type x_+                                <*> ask_colloc w_ x_ <*> ask_total_toks++build_type_count :: [Token] -> HashMap Text Int+build_type_count = List.foldl' update initcount+    where+    initcount = Map.singleton "." 0++    update ctr (Token {entity=(Word w per)})+        | per = Map.adjust (+ 1) "." ctr_+        | otherwise = ctr_+        where+        ctr_ = Map.insertWith (+) wnorm 1 ctr+        wnorm = norm $ if per then w `Text.snoc` '.' else w+    update ctr _ = ctr++    -- TODO: catch possible abbreviations wrapped in hyphenated and apostrophe+    -- forms in lexer++build_ortho_count :: [Token] -> HashMap Text OrthoFreq+build_ortho_count toks = List.foldl' update Map.empty $+                            zip (dummy : toks) toks+    where+    dummy = Token 0 0 (Word " " False) True False+    -- hack: add dummy to process first token++    update :: HashMap Text OrthoFreq -> (Token, Token) -> HashMap Text OrthoFreq+    update ctr (prev, Token {entity=(Word w _)}) = Map.insert wnorm wortho ctr+        where+        upd (OrthoFreq a b c d e) a' b' c' d' e' =+            OrthoFreq (a |+ a') (b |+ b') (c |+ c') (d |+ d') (e |+ e')+            where int |+ bool = if bool then int + 1 else int++        wortho = upd z lower (not lower) (first && lower)+                       (internal && not lower) first+        z = Map.lookupDefault (OrthoFreq 0 0 0 0 0) wnorm ctr+        wnorm = norm w+        lower = isLower $ Text.head w+        first = sentend prev && not (is_initial prev)+        internal = not (sentend prev) && not (abbrev prev)+                   && not (is_initial prev)+    update ctr _ = ctr++build_collocs :: [Token] -> HashMap (Text, Text) Int+build_collocs toks = List.foldl' update Map.empty $ zip toks (drop 1 toks)+    where+    update ctr (Token {entity=(Word u _)}, Token {entity=(Word v _)}) =+        Map.insertWith (+) (norm u, norm v) 1 ctr+    update ctr _ = ctr++to_tokens :: Text -> [Token]+to_tokens corpus = catMaybes . map (either tok_word add_delim) $+                        re_split_pos word_seps corpus+    where+    tok_word (w, pos)+        | trim == "" = Nothing+        | otherwise = Just $ Token pos (len trim) (Word s period) False False+        where+        trim = Text.dropAround (`elem` ",:()[]{}“”’\"\')") w+        period = Text.last trim == '.'+        s = if period then Text.init trim else trim++    add_delim (delim, pos)+        | d `elem` "—-" = Just $ Token pos (len delim) Dash False False+        | d `elem` ".…" = Just $ Token pos (len delim) Ellipsis False False+        | d `elem` ";!?" = Just $ Token pos (len delim) (Punct delim) True False+        | otherwise = Nothing+        where d = Text.head delim++    len = Text.length++build_punkt_data :: [Token] -> PunktData+build_punkt_data toks = PunktData typecnt orthocnt collocs nender totes+    where+    typecnt = build_type_count toks+    temppunkt = PunktData typecnt Map.empty Map.empty 0 (length toks)+    refined = runPunkt temppunkt $ mapM classify_by_type toks+    orthocnt = build_ortho_count refined+    collocs = build_collocs refined+    nender = length . filter (sentend . fst) $ zip (dummy : refined) refined+    dummy = Token 0 0 (Word " " False) True False+    totes = length $ filter is_word toks++classify_by_type :: Token -> Punkt Token+classify_by_type tok@(Token {entity=(Word w True)}) = do+    p <- prob_abbr w+    return $ tok { abbrev = p >= 0.3, sentend = p < 0.3}+classify_by_type tok = return tok++classify_by_next :: Token -> Token -> Punkt Token+classify_by_next this (Token _ _ (Word next _) _ _)+    | is_initial this = do+        let Word thisinitial _ = entity this+        colo <- prob_colloc thisinitial next+        startnext <- prob_starter next+        orthonext <- decide_initial_ortho next+        return $ if (colo >= 7.88 && startnext < 30) || orthonext == Just False+            then this { abbrev = True, sentend = False}+            else this  -- never reclassify as sentend+    | entity this == Ellipsis || abbrev this = do+        ortho_says <- decide_ortho next+        prob_says <- prob_starter next+        return $ case ortho_says of+            Nothing -> this { sentend = prob_says >= 30 }+            Just bool -> this { sentend = bool }+classify_by_next this _ = return this++classify_punkt :: Text -> [Token]+classify_punkt corpus = runPunkt (build_punkt_data toks) $ do+    abbrd <- mapM classify_by_type toks+    final <- Reader.zipWithM classify_by_next abbrd (drop 1 abbrd)+    return $ final ++ [last toks]+    where toks = to_tokens corpus++find_breaks :: Text -> [(Int, Int)]+find_breaks corpus = slices_from endpairs 0+    where+    pairs_of xs = zip xs $ drop 1 xs+    endpairs = filter (sentend . fst) . pairs_of $ classify_punkt corpus++    -- TODO: make this less convoluted+    slices_from [] n = [(n, Text.length corpus)]+    slices_from ((endtok, nexttok):pairs) n = (n, endpos + end) : slices_from pairs (endpos + n')+        where+        endpos = offset endtok + toklen endtok+        (end, n') = fromMaybe (endpos, endpos + 1) . match_spaces $+            substring corpus endpos (offset nexttok)++substring :: Text -> Int -> Int -> Text+substring c s e = Text.take (e - s) $ Text.drop s c++match_spaces :: Text -> Maybe (Int, Int)+match_spaces w = Text.findIndex isSpace w >>= \p ->+    case Text.break notSpace (Text.drop p w) of+        (spaces, _) -> Just (p, Text.length spaces + p)+    where notSpace = not . isSpace++split_sentences :: Text -> [Text]+split_sentences corpus = map (uncurry $ substring corpus) slices+    where slices = find_breaks corpus++runPunkt :: PunktData -> Punkt a -> a+runPunkt = flip Reader.runReader
+ lib/NLP/Punkt/Match.hs view
@@ -0,0 +1,47 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE PackageImports #-}++module NLP.Punkt.Match (+    re_split_impl,+    re_split_pos,+    re_split,+    re_compile,+    word_seps,+    intrasep+    ) where++import Data.Text (Text)+import Data.Array ((!))+import "regex-tdfa-text" Text.Regex.TDFA.Text (compile)+import "regex-tdfa" Text.Regex.TDFA (Regex, matchOnceText, blankCompOpt,+                                     ExecOption(..))+import Data.Maybe (maybe)+import Data.Either (lefts)++re_split_impl :: Regex -> Text -> [Either Text Text]+re_split_impl re str = filter not_blank $ chunk re str+    where+    not_blank xs = if xs == Left "" || xs == Right "" then False else True+    chunk re str = maybe [Left str] link $ matchOnceText re str+    link (pre, match, post) = Left pre : Right (fst $ match ! 0) : chunk re post++re_split_pos :: Regex -> Text -> [Either (Text, Int) (Text, Int)]+re_split_pos re str = filter not_blank $ chunk re str 0+    where+    not_blank xs =+        case xs of { Left ("", _) -> False; Right ("", _) -> False; _ -> True; }+    chunk re str relpos = case matchOnceText re str of+        Nothing -> [Left (str, relpos)]+        Just (pre, match, post) ->+            let (mtext, (moffset, mlen)) = match ! 0+                (mpos, relpos') = (relpos + moffset, mpos + mlen)+            in Left (pre, relpos) : Right (mtext, mpos) : chunk re post relpos'++re_split :: Regex -> Text -> [Text]+re_split re str = lefts $ re_split_impl re str++re_compile :: Text -> Regex+re_compile re = rv where Right rv = compile blankCompOpt (ExecOption False) re++word_seps = re_compile "([ \t\n]+|-{2,}|—|\\.{2,}|\\.( \\.)+|…|[!\\?;:]{1,})"+intrasep = re_compile "[-'’]"
+ punkt.cabal view
@@ -0,0 +1,46 @@+name: punkt+version: 0.1.0+synopsis: Multilingual unsupervised sentence tokenization with Punkt.+description: Multilingual unsupervised sentence tokenization with Punkt.+license: MIT+license-file:        LICENSE+author:              bryant+maintainer:          bryant@nfkb+category: Natural Language Processing, Text+build-type:          Simple+cabal-version:       >=1.10+homepage: https://github.com/bryant/punkt++library+    exposed-modules: NLP.Punkt, NLP.Punkt.Match++    build-depends:+        base >= 4 && < 5,+        mtl,+        unordered-containers,+        array,+        text,+        regex-tdfa,+        regex-tdfa-text++    hs-source-dirs: lib+    default-language: Haskell2010++    ghc-options: -O3 -Wall++test-suite punkt-tests+    type: exitcode-stdio-1.0+    hs-source-dirs: tests+    main-is: Main.hs++    build-depends:+        base >= 4 && < 5,+        mtl,+        text,+        punkt,+        regex-tdfa,+        tasty,+        tasty-quickcheck,+        tasty-hunit++    default-language: Haskell2010
+ tests/Main.hs view
@@ -0,0 +1,56 @@+import Test.Tasty (defaultMain, testGroup, TestTree)+import Test.Tasty.QuickCheck (+      forAll+    , choose+    , NonNegative(..)+    , Positive(..)+    , Arbitrary(..)+    , testProperty+    )+import NLP.Punkt (dunning_log)+import Precomputed (precomputed_tests)+import Brown (benchmark_brown)++main = defaultMain $ testGroup "Tests"+    [ dunning_equiv+    , precomputed_tests+    , benchmark_brown+    ]++ellr :: Int -> Int -> Int -> Int -> Double+ellr purea pureb ab neither =+    -2 * totes * (entropy all - entropy rows - entropy cols)+    where+    totes = fromIntegral $ sum all+    all = [purea, pureb, ab, neither]+    rows = [ab + pureb, purea + neither]+    cols = [ab + purea, pureb + neither]++    entropy :: [Int] -> Double+    entropy fs = - (sum $ map (ent . (/ n) . fromIntegral) fs)+        where+        n = fromIntegral $ sum fs+        ent 0 = 0  -- lim_{x -> 0} {x * log x} = 0+        ent p = p * log p++(~~) :: (Ord f, Floating f) => f -> f -> Bool+x ~~ y = abs (x - y) < epsilon where epsilon = 10 ** (-12)++data EventSet = EventSet Int Int Int Int deriving Show++instance Arbitrary EventSet where+    arbitrary = do+        NonNegative purea <- arbitrary+        NonNegative pureb <- arbitrary+        ab <- choose (1, min purea pureb)+        Positive neither <- arbitrary+        return $ EventSet purea pureb ab neither++dunning_equiv :: TestTree+dunning_equiv = testProperty "Dunning LLR-entropy equivalence" $+    forAll arbitrary llr_equiv+    where+    llr_equiv (EventSet pa pb ab neither) =+        ellr pa pb ab neither ~~ dunning_log a b ab' n+        where [a, b, ab', n] = map fromIntegral [ pa + ab, pb + ab, ab+                                                , pa + pb + ab + neither]