自給自足,完全手寫一個樸素貝葉斯分類器,完成文本分類

Part 1: 本文解決的問題:

我在有這樣的一個數據集,里面存放了人們對近期播放電影的評價,當然評價也就分成兩部分,好評和差評。我們想利用這些數據訓練一個模型,然后可以自動的對影評做出判斷,到底是好評還是差評,差評的話,那么我們趕緊刪掉它,哈哈。
好吧,這就是自然語言處理領域的基本問題:文本分類。文本分類在我們的日常生活中有非常多的應用,最有名的當屬垃圾郵件過濾啦。我們肯定希望不要受到垃圾郵件,但是我們更不希望正常的郵件被當做垃圾郵件過濾掉了。這對我們分類的精度提出了很高的要求。

Part 2:本文的結構

  • 數據來源以及含義
  • 貝葉斯公式的簡單介紹
  • 樸素貝葉斯分類器代碼編寫
  • 劃分測試數據和訓練數據,計算分類精度
  • 使用sklearn自帶的樸素貝葉斯分類器,計算分類精度
  • 比較手寫的分類器和sklearn自帶的分類器的優點和缺點
  • 參考資料和引用

Part 3 :數據來源以及含義

本文所用的測試數據和訓練數據都是來源于康奈爾大學網站的2M影評數據集。下載地址。里面共計有1400條影評,700條好評,700條差評,作者已經為我們分好了類。

Part 4: 代碼編寫

Part4.1:文檔和單詞

新建一個文件,命名為docclass.py,里面加入一個getwords的函數,完成從文本中提取特征。

def getwords(doc):
    splitter = re.compile('\\W*')
    words = [s.lower() for s in splitter.split(doc) if len(s) > 2 and len(s) < 20]
    # 過濾掉單詞中包含數字的單詞
    words = [word for word in words if word.isalpha()]
    with open(r'E:\研究生階段課程作業\python\好玩的數據分析\stopwords.txt') as f:
        stopwords = f.read()
    stopwords = stopwords.split('\n')
    stopwords = set(stopwords)
    # 過濾掉一些經常出現的單詞,例如 a,an,we,the 
    words = [word for word in words if word not in stopwords]
    return set(words)

該函數的輸入一個文檔,一般來說是一個大的字符串,我們首先使用正則表達式劃分單個單詞,對于一些特別常見的單詞,例如a,an,the,these,這些毫無意義的單詞,我們都保存在stopwords 中,并進行過濾,最后返回一組文檔中不重復的單詞(所有的單詞都是小寫的形式)。

Part4.2: 編寫分類器

新建一個classifier的類:

class classifier:
    def __init__(self, getfeatures):
        # Counts of feature/category combinations
        self.fc = {}
        # Counts of documents in each category
        self.cc = {}
        self.getfeatures = getfeatures

該類中有三個實例變量:fc,cc, getfeatures.
變量fc將記錄位于各分類中不同特征的數量。例如:
{'python': {'bad': 0, 'good': 6}, 'money': {'bad': 5, 'good': 1}}
上述示例表明,單詞'money'被劃歸'bad'類文檔中已經出現了5次,而被劃為'good'類只有1次,單詞'python'被劃歸'bad'類文檔中已經出現了0次,而被劃為'good'類有6次。

變量cc是一個記錄各分類被使用次數的詞典。這一信息是我們稍后討論的概率計算所需的。最后一個實例變量是 getfeatures,對應一個函數,作用是從即將被歸類的文檔中提取出特征來-本例中,就是我們剛才定義的getwords函數。
向我們剛才定義的類中加入下面的幾個函數,實現分類器的訓練

#增加對特征/分類組合的計數值
def incf(self, f, cat):
    self.fc.setdefault(f, {})
    self.fc[f].setdefault(cat, {})
    self.fc[f][cat] += 1

#增加某一個分類的計數值:
def incc(self, cat):
    self.cc.setdefault(cat, {})
    self.cc[cat] += 1

#計算某一個特征在某一個分類中出現的次數
def fcount(self, f, cat):
    if f in self.fc and cat in self.fc[f]:
        return self.fc[f][cat]
    else:
        return 0.0
#屬于某一個分類的文檔總數
def catcount(self, cat):
    if cat in self.cc:
        return self.cc[cat]
    return 0
#所有的文檔總數
def totalcount(self):
    return sum(self.cc.values())
#所有文檔的種類
def categories(self):
    return self.cc.keys()

train函數接受一個文檔和其所屬分類(‘good’或者‘bad’),利用我們定義的getwords函數,對文檔進行劃分,劃分成一個個獨立的單詞,然后調用incf函數,針對該分類為每個特征增加計數值,最后增加該分類的總計數值:

def train(self, item, cat):
        features = self.getfeatures(item)
        # 針對該分類,為每個特征增加計數值
        for f in features:
            self.incf(f, cat)

    # 增加該分類的計數值
        self.incc(cat)

下面我們開始測試我們編寫的類是否可用

cl = classifier(getwords)
cl.train('the quick brown fox jumps over the lazy dog', 'good')
cl.train('make quick money in the online casino', 'bad')
cl.fcout('quick','good')
out: 1.0
cl.fcout('quick','bad')
out: 1.0

上面的幾行代碼很好理解,我們首先實例化了 classifier 類,然后使用兩個文檔對我們的分類器進行了簡單的訓練。cl.fcout('quick','good') 用來計算在分類為‘good’的所有文檔中,單詞‘qucik’出現的次數。

當然嘍,我們現實生活中的分類器訓練肯定需要使用大量數據,我們新建一個函數(需要注意的是,這個函數不屬于任何一個類),來訓練大規模數據

def sampletrain(cl):
     cl.train('nobody owns the water','good')
     cl.train('the quick rabbit jumps fences','good')
     cl.train('buy phamaceuticals now','bad')
     cl.train('make quick money at the online casino','bad')
     cl.train('the quick borwn fox jumps','good')

在上述的函數中,我們已經計算了對于每一個特征(單詞),我們計算了它在某一個分類中出現的次數,是時候將其轉化成概率了。在本例中,我們對于一個特定單詞,計算它在某個分類中所占的比例。(也就是某個分類中出現該單詞的文檔數目 / 該分類的文檔總數)

def fprob(self, f, cat):
        if self.catcount(cat) == 0:
            return 0

        # 特征在該分類中出現的次數 /
        # 該特征下文檔的總數目
        return self.fcount(f, cat)/self.catcount(cat)

通俗的來說,這個函數就是我們要求的條件概率。 P(word | classification),意思就是對于一個給定的分類,某個單詞出現的概率,下面我們測試一下這個函數:

cl = classifier(getwords)
sampletrain(cl)
cl.fprob('quick','good')
out:0.6666666

從執行的結果上看,在所有的三篇被歸類于‘good’文檔中,有2篇出現了單詞‘qucik’,所以我們要求的條件概率 p('quick' | 'good') = 2/3

Part 4.2.1 一個小小的問題

在訓練的樣本中,由于單詞‘money’只出現了一次,并且是一個賭博類的廣告,因此被分類‘bad’類,那我們計算p('money' | 'good') = 0,這是非常危險和不公平的,由于我們訓練樣本的缺失,導致所有含有‘money’這個單詞的文檔都被判斷為‘bad’類文檔。顯然這種結果是我們不愿意接受的,因此我們對概率進行一些加權,使一些即使在訓練樣本中沒有出現的單詞,在求條件概率的時候,不至于為0。具體做法如下:

def weightedprob(self, f, cat, prf, weight=1, ap=0.5):
        # 使用fprob函數計算原始的條件概率
        basicprob = prf(f, cat)
        totals = sum([self.fcount(f, c) for c in self.categories()])
        bp = ((weight*ap)+(totals*basicprob))/(weight+totals)
        return bp

這個函數就是經過加權以后的條件概率,我們來對比一下加權前后的條件概率:

cl = classifier(getwords)
sampletrain(cl)
cl.fprob('money','good')
out:0
cl.weightedprob('money','good')
out:0.25

Part 4.3 樸素分類器

之所以稱為樸素貝葉斯分類器的前提是被組合的各個概率之間是獨立的,在我們的例子中,可以這樣理解:一個單詞在屬于某個分類文檔中概率,與其他單詞出現在該分類的概率是不相關的。事實上,這個假設并不成立,因為很多詞都是結伴出現的,但是我們可以忽略,實踐顯示,在假設各單詞互相獨立的基礎上,使用樸素貝葉斯對文本分類可以達到比較好的效果

Part 4.3.1 計算整篇文檔屬于某個分類的概率

假設我們已經注意到,有20%的‘bad’文檔出現了‘python’單詞- P('python'| 'bad') = 0.2,同時有80%的文檔出現了單詞‘casino’-P('casino'| 'bad')=0.8,那么當‘python’和‘casino’同時出現在一篇‘bad’文檔的概率是P('casino' & 'python' | 'bad') = 0.8 * 0.2 = 0.16。
我們新建一個子類,繼承自classifier,取名naivebayes,并添加一個docprob函數

class naivebayes(classifier):
  
    def __init__(self, getfeatures):
        classifier.__init__(self, getfeatures)
        
    def docprob(self, item, cat):
        features = self.getfeatures(item)
        # Multiply the probabilities of all the features together
        p = 1
        for f in features:
            p *= self.weightedprob(f, cat, self.fprob)
        return p

現在我們已經知道了如何計算P(Document|category),但是我們需要知道的是,最終我們需要的結果是P(category|Document),換而言之,對于一篇給定的文檔,我們需要找出它屬于各個分類的概率,我們感到欣慰的是,這就是貝葉斯需要解決的事情
**在本例中:
P(category|Document) = P(Document|category) * P(category) / P(Document)
P(Document|category) 已經被我們用 docprob 函數計算出來了,P(category)也很好理解和計算:代表我們隨你選擇一篇文檔,它屬于某個分類的概率。P(Document)對于所有的文檔來說,都是一樣的,我們直接選擇忽略掉他
**
我們在naivebayes中新添加一個prob函數,計算一篇文檔屬于某個分類的概率(P(Document|category) * P(category) )

def prob(self, item, cat):
        catprob = self.catcount(cat)/self.totalcount()
        docprob = self.docprob(item, cat)
        return docprob * catprob

到現在為止,我們的樸素貝葉斯分類器編寫基本完成。我們看看針對不同的文檔(字符串),概率值是如何變化的:

cl = naivebayes(getwords)
sampletrain(cl)
cl.prob('quick rabbit', 'good')
out: 0.156
cl.prob('quick rabbit', 'bad')
out: 0.05

根據訓練的數據,我們認為相對于‘bad’分類而言,我們認為‘quick rabbit’更適合于'good'分類.
最后我們完善一下我們的分類器,我們只需要給出文檔,分類器會自動給我們找出概率最大的哪一個分類。
我們為naivebayes新添加一個方法 :classify

def classify(self, item):
        max = 0.0
        for cat in self.categories():
            probs[cat] = self.prob(item, cat)
            if probs[cat] > max:
                max = probs[cat]
                best = cat
        return best

繼續測試:

cl = naivebayes(getwords)
sampletrain(cl)
cl.classify('quick rabbit')
out:good

但是到目前為止,我們所使用的訓練數據,或者測試數據,都是簡單的字符串,同時也是我們人為制造的,但是在真實的生產環境中,這幾乎是不可能的,數據要更為復雜,更為龐大。回到開頭,我這里使用在康奈爾大學下載的2M影評作為訓練數據和測試數據,里面共同、共有1400條,好評和差評各自700條,我選擇總數的70%作為訓練數據,30%作為測試數據,來檢測我們手寫的樸素貝葉斯分類器的效果
首先我們稍微修改一下:我們的訓練函數:sampletrain,以便能夠訓練大規模數據

def sampletrain(cl, traindata, traintarget):
    for left, right in zip(traindata, traintarget):.
        cl.train(left, right)

我們可以把需要訓練的數據放在一個list里面或者迭代器里面,其對應的分類也是如此,在函數中,我們使用traindata, traintarget分別替代我們的訓練數據和其對應的分類。
我們定義一個函數 get_dataset獲得打亂后的數據

def get_dataset():
    data = []
    for root, dirs, files in os.walk(r'E:\研究生階段課程作業\python\好玩的數據分析\樸素貝葉斯文本分類\tokens\neg'):
        for file in files:
            realpath = os.path.join(root, file)
            with open(realpath, errors='ignore') as f:
                data.append((f.read(), 'bad'))
    for root, dirs, files in os.walk(r'E:\研究生階段課程作業\python\好玩的數據分析\樸素貝葉斯文本分類\tokens\pos'):
        for file in files:
            realpath = os.path.join(root, file)
            with open(realpath, errors='ignore') as f:
                data.append((f.read(), 'good'))
    random.shuffle(data)
    return data

在定義一個函數,對我們的數據集進行劃分,訓練集和測試集分別占07和0.3

def train_and_test_data(data_):
    filesize = int(0.7 * len(data_))
    # 訓練集和測試集的比例為7:3
    train_data_ = [each[0] for each in data_[:filesize]]
    train_target_ = [each[1] for each in data_[:filesize]]

    test_data_ = [each[0] for each in data_[filesize:]]
    test_target_ = [each[1] for each in data_[filesize:]]

    return train_data_, train_target_, test_data_, test_target_

計算我們的分類器在真實數據上的表現:

if __name__ == '__main__':
    cl = naivebayes(getwords)
    data = dataset()
    train_data, train_target, test_data, test_target = train_and_test_data(data)
    sampletrain(cl, train_data, train_target)  #對訓練我們的分類器進行訓練
    predict = []
    for each in test_data:
        predict.append(cl.classify(each))
    count = 0
    for left,right in zip(predict,test_target ):
          if left == right:
                count += 1
    print(count/len(test_target))
    out :0.694
  

對于我們的測試集,大約有420個影評,我們使用簡單的、完全手寫的貝葉斯分類器達到了將近70%的預測準確率,效果還算可以,從頭到尾,你是不是被貝葉斯的神奇應用折服了呢。如果你是初學者,可以按照本片博客,一步一步完成樸素貝葉斯分類器的編寫,如果你嫌麻煩,可以直接向我要源碼。(其實把本文所有的代碼加起來就是完整的源碼啦)

Part 5 總結

作為學計算機的人,重復造輪子,恐怕是最消耗精力也是最得不償失的一件事情了,在下一篇文檔,我將會使用sklearn庫里自帶的貝葉斯分類器,對相同的數據進行分類,比較我們手寫的和自帶的有哪些優點和缺點。

Part 6 參考資料

需要說明的是,本篇文章關于分類器編寫的部分,我參考了《集體智慧編程》一書的第六章: 文檔過濾,我真心推薦《集體智慧編程》這本書,如果你是機器學習的初學者,那么這本書將使你受益頗多。

QQ :1527927373
Email: 1527927373@qq.com

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,501評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,673評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,610評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,939評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,668評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,004評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,001評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,173評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,705評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,426評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,656評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,139評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,833評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,247評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,580評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,371評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,621評論 2 380

推薦閱讀更多精彩內容