Part 1: 本文解決的問(wèn)題:
我在有這樣的一個(gè)數(shù)據(jù)集,里面存放了人們對(duì)近期播放電影的評(píng)價(jià),當(dāng)然評(píng)價(jià)也就分成兩部分,好評(píng)和差評(píng)。我們想利用這些數(shù)據(jù)訓(xùn)練一個(gè)模型,然后可以自動(dòng)的對(duì)影評(píng)做出判斷,到底是好評(píng)還是差評(píng),差評(píng)的話,那么我們趕緊刪掉它,哈哈。
好吧,這就是自然語(yǔ)言處理領(lǐng)域的基本問(wèn)題:文本分類。文本分類在我們的日常生活中有非常多的應(yīng)用,最有名的當(dāng)屬垃圾郵件過(guò)濾啦。我們肯定希望不要受到垃圾郵件,但是我們更不希望正常的郵件被當(dāng)做垃圾郵件過(guò)濾掉了。這對(duì)我們分類的精度提出了很高的要求。
Part 2:本文的結(jié)構(gòu)
數(shù)據(jù)來(lái)源以及含義
貝葉斯公式的簡(jiǎn)單介紹
樸素貝葉斯分類器代碼編寫(xiě)
劃分測(cè)試數(shù)據(jù)和訓(xùn)練數(shù)據(jù),計(jì)算分類精度
使用sklearn自帶的樸素貝葉斯分類器,計(jì)算分類精度
比較手寫(xiě)的分類器和sklearn自帶的分類器的優(yōu)點(diǎn)和缺點(diǎn)
參考資料和引用
Part 3 :數(shù)據(jù)來(lái)源以及含義
本文所用的測(cè)試數(shù)據(jù)和訓(xùn)練數(shù)據(jù)都是來(lái)源于康奈爾大學(xué)網(wǎng)站的2M影評(píng)數(shù)據(jù)集。下載地址。里面共計(jì)有1400條影評(píng),700條好評(píng),700條差評(píng),作者已經(jīng)為我們分好了類。
Part 4: 代碼編寫(xiě)
Part4.1:文檔和單詞
新建一個(gè)文件,命名為docclass.py,里面加入一個(gè)getwords的函數(shù),完成從文本中提取特征。
def getwords(doc):
splitter = re.compile('\\W*')
words = [s.lower() for s in splitter.split(doc) if len(s) > 2 and len(s) < 20]
# 過(guò)濾掉單詞中包含數(shù)字的單詞
words = [word for word in words if word.isalpha()]
with open(r'E:\研究生階段課程作業(yè)\python\好玩的數(shù)據(jù)分析\stopwords.txt') as f:
stopwords = f.read()
stopwords = stopwords.split('\n')
stopwords = set(stopwords)
# 過(guò)濾掉一些經(jīng)常出現(xiàn)的單詞,例如 a,an,we,the
words = [word for word in words if word not in stopwords]
return set(words)
該函數(shù)的輸入一個(gè)文檔,一般來(lái)說(shuō)是一個(gè)大的字符串,我們首先使用正則表達(dá)式劃分單個(gè)單詞,對(duì)于一些特別常見(jiàn)的單詞,例如a,an,the,these,這些毫無(wú)意義的單詞,我們都保存在stopwords 中,并進(jìn)行過(guò)濾,最后返回一組文檔中不重復(fù)的單詞(所有的單詞都是小寫(xiě)的形式)。
Part4.2: 編寫(xiě)分類器
新建一個(gè)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
該類中有三個(gè)實(shí)例變量:fc,cc, getfeatures.
變量fc將記錄位于各分類中不同特征的數(shù)量。例如:
{'python': {'bad': 0, 'good': 6}, 'money': {'bad': 5, 'good': 1}}
上述示例表明,單詞'money'被劃歸'bad'類文檔中已經(jīng)出現(xiàn)了5次,而被劃為'good'類只有1次,單詞'python'被劃歸'bad'類文檔中已經(jīng)出現(xiàn)了0次,而被劃為'good'類有6次。
變量cc是一個(gè)記錄各分類被使用次數(shù)的詞典。這一信息是我們稍后討論的概率計(jì)算所需的。最后一個(gè)實(shí)例變量是 getfeatures,對(duì)應(yīng)一個(gè)函數(shù),作用是從即將被歸類的文檔中提取出特征來(lái)-本例中,就是我們剛才定義的getwords函數(shù)。
向我們剛才定義的類中加入下面的幾個(gè)函數(shù),實(shí)現(xiàn)分類器的訓(xùn)練
#增加對(duì)特征/分類組合的計(jì)數(shù)值
def incf(self, f, cat):
self.fc.setdefault(f, {})
self.fc[f].setdefault(cat, {})
self.fc[f][cat] += 1
#增加某一個(gè)分類的計(jì)數(shù)值:
def incc(self, cat):
self.cc.setdefault(cat, {})
self.cc[cat] += 1
#計(jì)算某一個(gè)特征在某一個(gè)分類中出現(xiàn)的次數(shù)
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
#屬于某一個(gè)分類的文檔總數(shù)
def catcount(self, cat):
if cat in self.cc:
return self.cc[cat]
return 0
#所有的文檔總數(shù)
def totalcount(self):
return sum(self.cc.values())
#所有文檔的種類
def categories(self):
return self.cc.keys()
train函數(shù)接受一個(gè)文檔和其所屬分類(‘good’或者‘bad’),利用我們定義的getwords函數(shù),對(duì)文檔進(jìn)行劃分,劃分成一個(gè)個(gè)獨(dú)立的單詞,然后調(diào)用incf函數(shù),針對(duì)該分類為每個(gè)特征增加計(jì)數(shù)值,最后增加該分類的總計(jì)數(shù)值:
def train(self, item, cat):
features = self.getfeatures(item)
# 針對(duì)該分類,為每個(gè)特征增加計(jì)數(shù)值
for f in features:
self.incf(f, cat)
# 增加該分類的計(jì)數(shù)值
self.incc(cat)
下面我們開(kāi)始測(cè)試我們編寫(xiě)的類是否可用
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
上面的幾行代碼很好理解,我們首先實(shí)例化了 classifier 類,然后使用兩個(gè)文檔對(duì)我們的分類器進(jìn)行了簡(jiǎn)單的訓(xùn)練。cl.fcout('quick','good') 用來(lái)計(jì)算在分類為‘good’的所有文檔中,單詞‘qucik’出現(xiàn)的次數(shù)。
當(dāng)然嘍,我們現(xiàn)實(shí)生活中的分類器訓(xùn)練肯定需要使用大量數(shù)據(jù),我們新建一個(gè)函數(shù)(需要注意的是,這個(gè)函數(shù)不屬于任何一個(gè)類),來(lái)訓(xùn)練大規(guī)模數(shù)據(jù)
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')
在上述的函數(shù)中,我們已經(jīng)計(jì)算了對(duì)于每一個(gè)特征(單詞),我們計(jì)算了它在某一個(gè)分類中出現(xiàn)的次數(shù),是時(shí)候?qū)⑵滢D(zhuǎn)化成概率了。在本例中,我們對(duì)于一個(gè)特定單詞,計(jì)算它在某個(gè)分類中所占的比例。(也就是某個(gè)分類中出現(xiàn)該單詞的文檔數(shù)目 / 該分類的文檔總數(shù))
def fprob(self, f, cat):
if self.catcount(cat) == 0:
return 0
# 特征在該分類中出現(xiàn)的次數(shù) /
# 該特征下文檔的總數(shù)目
return self.fcount(f, cat)/self.catcount(cat)
通俗的來(lái)說(shuō),這個(gè)函數(shù)就是我們要求的條件概率。 P(word | classification),意思就是對(duì)于一個(gè)給定的分類,某個(gè)單詞出現(xiàn)的概率,下面我們測(cè)試一下這個(gè)函數(shù):
cl = classifier(getwords)
sampletrain(cl)
cl.fprob('quick','good')
out:0.6666666
從執(zhí)行的結(jié)果上看,在所有的三篇被歸類于‘good’文檔中,有2篇出現(xiàn)了單詞‘qucik’,所以我們要求的條件概率 p('quick' | 'good') = 2/3
Part 4.2.1 一個(gè)小小的問(wèn)題
在訓(xùn)練的樣本中,由于單詞‘money’只出現(xiàn)了一次,并且是一個(gè)賭博類的廣告,因此被分類‘bad’類,那我們計(jì)算p('money' | 'good') = 0,這是非常危險(xiǎn)和不公平的,由于我們訓(xùn)練樣本的缺失,導(dǎo)致所有含有‘money’這個(gè)單詞的文檔都被判斷為‘bad’類文檔。顯然這種結(jié)果是我們不愿意接受的,因此我們對(duì)概率進(jìn)行一些加權(quán),使一些即使在訓(xùn)練樣本中沒(méi)有出現(xiàn)的單詞,在求條件概率的時(shí)候,不至于為0。具體做法如下:
def weightedprob(self, f, cat, prf, weight=1, ap=0.5):
# 使用fprob函數(shù)計(jì)算原始的條件概率
basicprob = prf(f, cat)
totals = sum([self.fcount(f, c) for c in self.categories()])
bp = ((weight*ap)+(totals*basicprob))/(weight+totals)
return bp
這個(gè)函數(shù)就是經(jīng)過(guò)加權(quán)以后的條件概率,我們來(lái)對(duì)比一下加權(quán)前后的條件概率:
cl = classifier(getwords)
sampletrain(cl)
cl.fprob('money','good')
out:0
cl.weightedprob('money','good')
out:0.25
Part 4.3 樸素分類器
之所以稱為樸素貝葉斯分類器的前提是被組合的各個(gè)概率之間是獨(dú)立的,在我們的例子中,可以這樣理解:一個(gè)單詞在屬于某個(gè)分類文檔中概率,與其他單詞出現(xiàn)在該分類的概率是不相關(guān)的。事實(shí)上,這個(gè)假設(shè)并不成立,因?yàn)楹芏嘣~都是結(jié)伴出現(xiàn)的,但是我們可以忽略,實(shí)踐顯示,在假設(shè)各單詞互相獨(dú)立的基礎(chǔ)上,使用樸素貝葉斯對(duì)文本分類可以達(dá)到比較好的效果
Part 4.3.1 計(jì)算整篇文檔屬于某個(gè)分類的概率
假設(shè)我們已經(jīng)注意到,有20%的‘bad’文檔出現(xiàn)了‘python’單詞- P('python'| 'bad') = 0.2,同時(shí)有80%的文檔出現(xiàn)了單詞‘casino’-P('casino'| 'bad')=0.8,那么當(dāng)‘python’和‘casino’同時(shí)出現(xiàn)在一篇‘bad’文檔的概率是P('casino' & 'python' | 'bad') = 0.8 * 0.2 = 0.16。
我們新建一個(gè)子類,繼承自classifier,取名naivebayes,并添加一個(gè)docprob函數(shù)
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
現(xiàn)在我們已經(jīng)知道了如何計(jì)算P(Document|category),但是我們需要知道的是,最終我們需要的結(jié)果是P(category|Document),換而言之,對(duì)于一篇給定的文檔,我們需要找出它屬于各個(gè)分類的概率,我們感到欣慰的是,這就是貝葉斯需要解決的事情
在本例中:
P(category|Document) = P(Document|category) * P(category) / P(Document)
P(Document|category) 已經(jīng)被我們用 docprob 函數(shù)計(jì)算出來(lái)了,P(category)也很好理解和計(jì)算:代表我們隨你選擇一篇文檔,它屬于某個(gè)分類的概率。P(Document)對(duì)于所有的文檔來(lái)說(shuō),都是一樣的,我們直接選擇忽略掉他
我們?cè)趎aivebayes中新添加一個(gè)prob函數(shù),計(jì)算一篇文檔屬于某個(gè)分類的概率(P(Document|category) * P(category) )
def prob(self, item, cat):
catprob = self.catcount(cat)/self.totalcount()
docprob = self.docprob(item, cat)
return docprob * catprob
到現(xiàn)在為止,我們的樸素貝葉斯分類器編寫(xiě)基本完成。我們看看針對(duì)不同的文檔(字符串),概率值是如何變化的:
cl = naivebayes(getwords)
sampletrain(cl)
cl.prob('quick rabbit', 'good')
out: 0.156
cl.prob('quick rabbit', 'bad')
out: 0.05
根據(jù)訓(xùn)練的數(shù)據(jù),我們認(rèn)為相對(duì)于‘bad’分類而言,我們認(rèn)為‘quick rabbit’更適合于'good'分類.
最后我們完善一下我們的分類器,我們只需要給出文檔,分類器會(huì)自動(dòng)給我們找出概率最大的哪一個(gè)分類。
我們?yōu)閚aivebayes新添加一個(gè)方法 :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
繼續(xù)測(cè)試:
cl = naivebayes(getwords)
sampletrain(cl)
cl.classify('quick rabbit')
out:good
但是到目前為止,我們所使用的訓(xùn)練數(shù)據(jù),或者測(cè)試數(shù)據(jù),都是簡(jiǎn)單的字符串,同時(shí)也是我們?nèi)藶橹圃斓?,但是在真?shí)的生產(chǎn)環(huán)境中,這幾乎是不可能的,數(shù)據(jù)要更為復(fù)雜,更為龐大。回到開(kāi)頭,我這里使用在康奈爾大學(xué)下載的2M影評(píng)作為訓(xùn)練數(shù)據(jù)和測(cè)試數(shù)據(jù),里面共同、共有1400條,好評(píng)和差評(píng)各自700條,我選擇總數(shù)的70%作為訓(xùn)練數(shù)據(jù),30%作為測(cè)試數(shù)據(jù),來(lái)檢測(cè)我們手寫(xiě)的樸素貝葉斯分類器的效果
首先我們稍微修改一下:我們的訓(xùn)練函數(shù):sampletrain,以便能夠訓(xùn)練大規(guī)模數(shù)據(jù)
def sampletrain(cl, traindata, traintarget):
for left, right in zip(traindata, traintarget):.
cl.train(left, right)
我們可以把需要訓(xùn)練的數(shù)據(jù)放在一個(gè)list里面或者迭代器里面,其對(duì)應(yīng)的分類也是如此,在函數(shù)中,我們使用traindata, traintarget分別替代我們的訓(xùn)練數(shù)據(jù)和其對(duì)應(yīng)的分類。
我們定義一個(gè)函數(shù) get_dataset獲得打亂后的數(shù)據(jù)
def get_dataset():
data = []
for root, dirs, files in os.walk(r'E:\研究生階段課程作業(yè)\python\好玩的數(shù)據(jù)分析\樸素貝葉斯文本分類\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:\研究生階段課程作業(yè)\python\好玩的數(shù)據(jù)分析\樸素貝葉斯文本分類\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
在定義一個(gè)函數(shù),對(duì)我們的數(shù)據(jù)集進(jìn)行劃分,訓(xùn)練集和測(cè)試集分別占07和0.3
def train_and_test_data(data_):
filesize = int(0.7 * len(data_))
# 訓(xùn)練集和測(cè)試集的比例為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_
計(jì)算我們的分類器在真實(shí)數(shù)據(jù)上的表現(xiàn):
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)? #對(duì)訓(xùn)練我們的分類器進(jìn)行訓(xùn)練
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
對(duì)于我們的測(cè)試集,大約有420個(gè)影評(píng),我們使用簡(jiǎn)單的、完全手寫(xiě)的貝葉斯分類器達(dá)到了將近70%的預(yù)測(cè)準(zhǔn)確率,效果還算可以,從頭到尾,你是不是被貝葉斯的神奇應(yīng)用折服了呢。如果你是初學(xué)者,可以按照本片博客,一步一步完成樸素貝葉斯分類器的編寫(xiě),如果你嫌麻煩,可以直接向我要源碼。(其實(shí)把本文所有的代碼加起來(lái)就是完整的源碼啦)
Part 5 總結(jié)
作為學(xué)計(jì)算機(jī)的人,重復(fù)造輪子,恐怕是最消耗精力也是最得不償失的一件事情了,在下一篇文檔,我將會(huì)使用sklearn庫(kù)里自帶的貝葉斯分類器,對(duì)相同的數(shù)據(jù)進(jìn)行分類,比較我們手寫(xiě)的和自帶的有哪些優(yōu)點(diǎn)和缺點(diǎn)。
Part 6 參考資料
需要說(shuō)明的是,本篇文章關(guān)于分類器編寫(xiě)的部分,我參考了《集體智慧編程》一書(shū)的第六章: 文檔過(guò)濾,我真心推薦《集體智慧編程》這本書(shū),如果你是機(jī)器學(xué)習(xí)的初學(xué)者,那么這本書(shū)將使你受益頗多。