Part 1: 本文解決的問題:
我在有這樣的一個數(shù)據(jù)集场晶,里面存放了人們對近期播放電影的評價,當(dāng)然評價也就分成兩部分怠缸,好評和差評诗轻。我們想利用這些數(shù)據(jù)訓(xùn)練一個模型,然后可以自動的對影評做出判斷揭北,到底是好評還是差評扳炬,差評的話吏颖,那么我們趕緊刪掉它,哈哈恨樟。
好吧半醉,這就是自然語言處理領(lǐng)域的基本問題:文本分類。文本分類在我們的日常生活中有非常多的應(yīng)用劝术,最有名的當(dāng)屬垃圾郵件過濾啦奉呛。我們肯定希望不要受到垃圾郵件,但是我們更不希望正常的郵件被當(dāng)做垃圾郵件過濾掉了夯尽。這對我們分類的精度提出了很高的要求。
Part 2:本文的結(jié)構(gòu)
- 數(shù)據(jù)來源以及含義
- 貝葉斯公式的簡單介紹
- 樸素貝葉斯分類器代碼編寫
- 劃分測試數(shù)據(jù)和訓(xùn)練數(shù)據(jù)登馒,計算分類精度
- 使用sklearn自帶的樸素貝葉斯分類器匙握,計算分類精度
- 比較手寫的分類器和sklearn自帶的分類器的優(yōu)點和缺點
- 參考資料和引用
Part 3 :數(shù)據(jù)來源以及含義
本文所用的測試數(shù)據(jù)和訓(xùn)練數(shù)據(jù)都是來源于康奈爾大學(xué)網(wǎng)站的2M影評數(shù)據(jù)集。下載地址陈轿。里面共計有1400條影評圈纺,700條好評,700條差評麦射,作者已經(jīng)為我們分好了類蛾娶。
Part 4: 代碼編寫
Part4.1:文檔和單詞
新建一個文件,命名為docclass.py潜秋,里面加入一個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]
# 過濾掉單詞中包含數(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)
# 過濾掉一些經(jīng)常出現(xiàn)的單詞,例如 a,an,we,the
words = [word for word in words if word not in stopwords]
return set(words)
該函數(shù)的輸入一個文檔峻呛,一般來說是一個大的字符串罗售,我們首先使用正則表達式劃分單個單詞,對于一些特別常見的單詞钩述,例如a,an,the,these寨躁,這些毫無意義的單詞,我們都保存在stopwords 中牙勘,并進行過濾职恳,最后返回一組文檔中不重復(fù)的單詞(所有的單詞都是小寫的形式)。
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將記錄位于各分類中不同特征的數(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是一個記錄各分類被使用次數(shù)的詞典床蜘。這一信息是我們稍后討論的概率計算所需的辙培。最后一個實例變量是 getfeatures,對應(yīng)一個函數(shù)邢锯,作用是從即將被歸類的文檔中提取出特征來-本例中扬蕊,就是我們剛才定義的getwords函數(shù)。
向我們剛才定義的類中加入下面的幾個函數(shù)丹擎,實現(xiàn)分類器的訓(xùn)練
#增加對特征/分類組合的計數(shù)值
def incf(self, f, cat):
self.fc.setdefault(f, {})
self.fc[f].setdefault(cat, {})
self.fc[f][cat] += 1
#增加某一個分類的計數(shù)值:
def incc(self, cat):
self.cc.setdefault(cat, {})
self.cc[cat] += 1
#計算某一個特征在某一個分類中出現(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
#屬于某一個分類的文檔總數(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ù)接受一個文檔和其所屬分類(‘good’或者‘bad’)尾抑,利用我們定義的getwords函數(shù),對文檔進行劃分蒂培,劃分成一個個獨立的單詞再愈,然后調(diào)用incf函數(shù),針對該分類為每個特征增加計數(shù)值护戳,最后增加該分類的總計數(shù)值:
def train(self, item, cat):
features = self.getfeatures(item)
# 針對該分類翎冲,為每個特征增加計數(shù)值
for f in features:
self.incf(f, cat)
# 增加該分類的計數(shù)值
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 類媳荒,然后使用兩個文檔對我們的分類器進行了簡單的訓(xùn)練抗悍。cl.fcout('quick','good') 用來計算在分類為‘good’的所有文檔中,單詞‘qucik’出現(xiàn)的次數(shù)钳枕。
當(dāng)然嘍缴渊,我們現(xiàn)實生活中的分類器訓(xùn)練肯定需要使用大量數(shù)據(jù),我們新建一個函數(shù)(需要注意的是鱼炒,這個函數(shù)不屬于任何一個類)衔沼,來訓(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)計算了對于每一個特征(單詞)昔瞧,我們計算了它在某一個分類中出現(xiàn)的次數(shù)俐巴,是時候?qū)⑵滢D(zhuǎn)化成概率了。在本例中硬爆,我們對于一個特定單詞欣舵,計算它在某個分類中所占的比例。(也就是某個分類中出現(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)
通俗的來說缀磕,這個函數(shù)就是我們要求的條件概率缘圈。 P(word | classification),意思就是對于一個給定的分類袜蚕,某個單詞出現(xiàn)的概率糟把,下面我們測試一下這個函數(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 一個小小的問題
在訓(xùn)練的樣本中,由于單詞‘money’只出現(xiàn)了一次凿傅,并且是一個賭博類的廣告缠犀,因此被分類‘bad’類数苫,那我們計算p('money' | 'good') = 0,這是非常危險和不公平的辨液,由于我們訓(xùn)練樣本的缺失虐急,導(dǎo)致所有含有‘money’這個單詞的文檔都被判斷為‘bad’類文檔。顯然這種結(jié)果是我們不愿意接受的滔迈,因此我們對概率進行一些加權(quán)止吁,使一些即使在訓(xùn)練樣本中沒有出現(xiàn)的單詞,在求條件概率的時候燎悍,不至于為0敬惦。具體做法如下:
def weightedprob(self, f, cat, prf, weight=1, ap=0.5):
# 使用fprob函數(shù)計算原始的條件概率
basicprob = prf(f, cat)
totals = sum([self.fcount(f, c) for c in self.categories()])
bp = ((weight*ap)+(totals*basicprob))/(weight+totals)
return bp
這個函數(shù)就是經(jīng)過加權(quán)以后的條件概率,我們來對比一下加權(quán)前后的條件概率:
cl = classifier(getwords)
sampletrain(cl)
cl.fprob('money','good')
out:0
cl.weightedprob('money','good')
out:0.25
Part 4.3 樸素分類器
之所以稱為樸素貝葉斯分類器的前提是被組合的各個概率之間是獨立的谈山,在我們的例子中仁热,可以這樣理解:一個單詞在屬于某個分類文檔中概率,與其他單詞出現(xiàn)在該分類的概率是不相關(guān)的勾哩。事實上,這個假設(shè)并不成立举哟,因為很多詞都是結(jié)伴出現(xiàn)的思劳,但是我們可以忽略,實踐顯示妨猩,在假設(shè)各單詞互相獨立的基礎(chǔ)上潜叛,使用樸素貝葉斯對文本分類可以達到比較好的效果
Part 4.3.1 計算整篇文檔屬于某個分類的概率
假設(shè)我們已經(jīng)注意到,有20%的‘bad’文檔出現(xiàn)了‘python’單詞- P('python'| 'bad') = 0.2壶硅,同時有80%的文檔出現(xiàn)了單詞‘casino’-P('casino'| 'bad')=0.8,那么當(dāng)‘python’和‘casino’同時出現(xiàn)在一篇‘bad’文檔的概率是P('casino' & 'python' | 'bad') = 0.8 * 0.2 = 0.16威兜。
我們新建一個子類,繼承自classifier,取名naivebayes,并添加一個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)知道了如何計算P(Document|category)庐椒,但是我們需要知道的是椒舵,最終我們需要的結(jié)果是P(category|Document),換而言之约谈,對于一篇給定的文檔笔宿,我們需要找出它屬于各個分類的概率,我們感到欣慰的是棱诱,這就是貝葉斯需要解決的事情
**在本例中:
P(category|Document) = P(Document|category) * P(category) / P(Document)
P(Document|category) 已經(jīng)被我們用 docprob 函數(shù)計算出來了泼橘,P(category)也很好理解和計算:代表我們隨你選擇一篇文檔,它屬于某個分類的概率迈勋。P(Document)對于所有的文檔來說炬灭,都是一樣的,我們直接選擇忽略掉他
**
我們在naivebayes中新添加一個prob函數(shù)靡菇,計算一篇文檔屬于某個分類的概率(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)在為止重归,我們的樸素貝葉斯分類器編寫基本完成米愿。我們看看針對不同的文檔(字符串),概率值是如何變化的:
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)為相對于‘bad’分類而言吗货,我們認(rèn)為‘quick rabbit’更適合于'good'分類.
最后我們完善一下我們的分類器,我們只需要給出文檔狈网,分類器會自動給我們找出概率最大的哪一個分類宙搬。
我們?yōu)閚aivebayes新添加一個方法 :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ù)測試:
cl = naivebayes(getwords)
sampletrain(cl)
cl.classify('quick rabbit')
out:good
但是到目前為止,我們所使用的訓(xùn)練數(shù)據(jù)拓哺,或者測試數(shù)據(jù)勇垛,都是簡單的字符串,同時也是我們?nèi)藶橹圃斓氖颗福窃谡鎸嵉纳a(chǎn)環(huán)境中闲孤,這幾乎是不可能的,數(shù)據(jù)要更為復(fù)雜烤礁,更為龐大讼积。回到開頭脚仔,我這里使用在康奈爾大學(xué)下載的2M影評作為訓(xùn)練數(shù)據(jù)和測試數(shù)據(jù)勤众,里面共同、共有1400條鲤脏,好評和差評各自700條们颜,我選擇總數(shù)的70%作為訓(xùn)練數(shù)據(jù),30%作為測試數(shù)據(jù)猎醇,來檢測我們手寫的樸素貝葉斯分類器的效果
首先我們稍微修改一下:我們的訓(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ù)放在一個list里面或者迭代器里面,其對應(yīng)的分類也是如此硫嘶,在函數(shù)中阻问,我們使用traindata, traintarget分別替代我們的訓(xùn)練數(shù)據(jù)和其對應(yīng)的分類。
我們定義一個函數(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
在定義一個函數(shù),對我們的數(shù)據(jù)集進行劃分沦疾,訓(xùn)練集和測試集分別占07和0.3
def train_and_test_data(data_):
filesize = int(0.7 * len(data_))
# 訓(xùn)練集和測試集的比例為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_
計算我們的分類器在真實數(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) #對訓(xù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
對于我們的測試集则拷,大約有420個影評,我們使用簡單的曹鸠、完全手寫的貝葉斯分類器達到了將近70%的預(yù)測準(zhǔn)確率煌茬,效果還算可以,從頭到尾彻桃,你是不是被貝葉斯的神奇應(yīng)用折服了呢坛善。如果你是初學(xué)者,可以按照本片博客,一步一步完成樸素貝葉斯分類器的編寫眠屎,如果你嫌麻煩剔交,可以直接向我要源碼。(其實把本文所有的代碼加起來就是完整的源碼啦)
Part 5 總結(jié)
作為學(xué)計算機的人改衩,重復(fù)造輪子岖常,恐怕是最消耗精力也是最得不償失的一件事情了,在下一篇文檔葫督,我將會使用sklearn庫里自帶的貝葉斯分類器竭鞍,對相同的數(shù)據(jù)進行分類,比較我們手寫的和自帶的有哪些優(yōu)點和缺點橄镜。
Part 6 參考資料
需要說明的是偎快,本篇文章關(guān)于分類器編寫的部分,我參考了《集體智慧編程》一書的第六章: 文檔過濾,我真心推薦《集體智慧編程》這本書洽胶,如果你是機器學(xué)習(xí)的初學(xué)者晒夹,那么這本書將使你受益頗多。
QQ :1527927373
Email: 1527927373@qq.com