2012-03-19 97 views
13

我正要測試樸素貝葉斯分類。其中一部分將構建訓練數據的直方圖。問題在於,我使用了大量的訓練數據,幾年前的haskell-cafe郵件列表,並且該文件夾中有超過20k個文件。用haskell建立直方圖,比python慢​​了許多倍

用python創建直方圖需要花費兩分多鐘的時間,而使用haskell需要8分多鐘。我正在使用Data.Map(insertWith'),枚舉器和文本。我還能做些什麼來加速該計劃?

哈斯克爾:

import qualified Data.Text as T 
import qualified Data.Text.IO as TI 
import System.Directory 
import Control.Applicative 
import Control.Monad (filterM, foldM) 
import System.FilePath.Posix ((</>)) 
import qualified Data.Map as M 
import Data.Map (Map) 
import Data.List (foldl') 
import Control.Exception.Base (bracket) 
import System.IO (Handle, openFile, hClose, hSetEncoding, IOMode(ReadMode), latin1) 
import qualified Data.Enumerator as E 
import Data.Enumerator (($$), (>==>), (<==<), (==<<), (>>==), ($=), (=$)) 
import qualified Data.Enumerator.List as EL 
import qualified Data.Enumerator.Text as ET 



withFile' :: (Handle -> IO c) -> FilePath -> IO c 
withFile' f fp = do 
    bracket 
    (do 
     h ← openFile fp ReadMode 
     hSetEncoding h latin1 
     return h) 
    hClose 
    (f) 

buildClassHistogram c = do 
    files ← filterM doesFileExist =<< map (c </>) <$> getDirectoryContents c 
    foldM fileHistogram M.empty files 

fileHistogram m file = withFile' (λh → E.run_ $ enumHist h) file 
    where 
    enumHist h = ET.enumHandle h $$ EL.fold (λm' l → foldl' (λm'' w → M.insertWith' (const (+1)) w 1 m'') m' $ T.words l) m 

的Python:

for filename in listdir(root): 
    filepath = root + "/" + filename 
    # print(filepath) 
    fp = open(filepath, "r", encoding="latin-1") 
    for word in fp.read().split(): 
     if word in histogram: 
      histogram[word] = histogram[word]+1 
     else: 
      histogram[word] = 1 

編輯:增加進口

+0

什麼樣的容器是Python中的「直方圖」?使用哈希映射而不是基於樹的哈希映射肯定是合理的。 – leftaroundabout 2012-03-19 15:18:31

+0

只是基本的字典。我也嘗試了無序容器的HashMap,但速度減慢了,gc時間增加了。 – Masse 2012-03-19 15:20:04

+0

你用-O2編譯過嗎?它造就了一個不同的世界。 – 2012-03-19 15:30:37

回答

8

您可以嘗試使用哈希表包中的命令式哈希映射:http://hackage.haskell.org/package/hashtables 我記得我曾經比Data.Map有過中等加速比。儘管如此,我不會期待任何壯觀的事情。

UPDATE

我簡化你的Python代碼,所以我可以測試它在一個單一的大文件(100萬條用戶線):

import sys 
histogram={} 
for word in sys.stdin.readlines(): 
    if word in histogram: 
     histogram[word] = histogram[word]+1 
    else: 
     histogram[word] = 1 
print histogram.get("the") 

注意到6.06秒

使用

哈斯克爾翻譯哈希表:

{-# LANGUAGE OverloadedStrings #-} 
import qualified Data.ByteString.Char8 as T 
import qualified Data.HashTable.IO as HT 
main = do 
    ls <- T.lines `fmap` T.getContents 
    h <- HT.new :: IO (HT.BasicHashTable T.ByteString Int) 
    flip mapM_ ls $ \w -> do 
    r <- HT.lookup h w 
    case r of 
     Nothing -> HT.insert h w (1::Int) 
     Just c -> HT.insert h w (c+1) 
    HT.lookup h "the" >>= print 

運行時帶有大的分配區域:histogram +RTS -A500M 需要9.3秒,GC爲2.4%。還是比Python慢​​很多,但不是太糟糕。

按照GHC user guide,您可以更改RTS選項在編譯:

GHC lets you change the default RTS options for a program at compile time, using the -with-rtsopts flag (Section 4.12.6, 「Options affecting linking」). A common use for this is to give your program a default heap and/or stack size that is greater than the default. For example, to set -H128m -K64m, link with -with-rtsopts="-H128m -K64m".

+1

謝謝,這讓時間倒退了。今天運行python版本後,需要22秒(不知道爲什麼昨天一直持續兩分鐘),而haskell版本在30秒內完成,-A500M和115秒沒有。我能否以某種方式將-A500M「嵌入」二進制文件? – Masse 2012-03-20 07:44:02

+1

@Masse - 用標誌'-with-rtsopts = -A500M'編譯你的可執行文件 – 2012-03-20 10:36:42

+0

@Masse:用編譯時更新我的​​答案RTS控制 – 2012-03-20 10:43:04

7

你Haskell和Python實現使用的地圖,不同的複雜性。 Python字典是哈希映射,因此每個操作(成員資格測試,查找和插入)的預期時間爲O(1)。 Haskell版本使用Data.Map,它是一個平衡的二叉搜索樹,所以相同的操作需要O(lg n)時間。如果你改變你的Haskell版本來使用不同的地圖實現,比如說一個哈希表或某種類型的trie,它應該會更快。然而,對於實現這些數據結構的不同模塊來說,哪一個最好,我還不夠熟悉。我會從the Data category on Hackage開始,找一個你喜歡的。你也可能會尋找一個允許破壞性更新的地圖,如STArray

+0

從無序容器嘗試Data.HashMap,總時間幾乎相同。 – Masse 2012-03-19 15:26:28

+1

@Masse你是否嘗試過沒有迭代,只是摺疊'fmap words hGetContents'?也許看看[judy陣列](http://donsbot.wordpress.com/2009/09/26/very-fast-scalable-mutable-maps-and-hashes-for-haskell/) – 2012-03-19 15:39:46

+0

我認爲那裏有gc時間的約60-70%沒有迭代,並且約40-50%的迭代。看起來像judy陣列只支持字大小的鍵。在這種情況下使用它們需要手動散列並手動跟蹤存儲桶。這比我願意去的有點太低級 – Masse 2012-03-19 15:45:38

5

我們需要更多的信息:

  • 多久採取兩種方案,以處理來自字輸入,沒有數據結構來維護計數?

  • 有多少個不同的單詞存在,所以我們可以判斷平衡樹木的額外費用是否需要考慮?

  • GHC的分析器說什麼?特別是,分配花了多少時間? Haskell版本可能花費大部分時間來分配快速過時的樹節點。

  • UPDATE:我錯過了小寫「文本」可能意味着Data.Text。你可能會比較適用和橙子。 Python的Latin1編碼每個字符使用一個字節。雖然它試圖有效,但是Data.Text必須允許超過256個字符的可能性。如果切換到String或更好,Data.ByteString會發生什麼情況?

根據什麼這些指標說,這裏有幾件事情嘗試:

  • 如果分析輸入的是一個瓶頸,嘗試駕駛所有的I/O,分析和Data.ByteString代替Text

  • 如果數據結構是一個瓶頸,Bentley和Sedgewick的ternary search trees是純粹的功能,但與哈希表競爭性地執行。 Hackage上有一個TernaryTrees包。

+0

Masse表示他們使用'Text',而不是'String' – 2012-03-19 17:50:28

+0

@Grzegorz oops。編輯。 – 2012-03-19 18:02:40