一、概述
本實驗做的是一個很常見的數據挖掘任務:新聞文本分類。
語料庫來自於搜狗實驗室2008年和2012年的搜狐新聞數據,
下載地址:https://www.sogou.com/labs/resource/cs.php
實驗工作主要包括以下幾步:
1)語料庫的數據預處理;
2)文本建模;
3)訓練分類器;
4)對測試集文本分類;
5)結果評估。
二、實驗環境搭建
本實驗在Google Drive平台進行,利用平台免費的運算資源以及存儲空間,使用Colaboratory實驗環境完成。
不過Colab分配的環境是臨時環境,文件不會一直保存,所以實驗的第一步是連接Google Drive, 將相關文件保存在Drive里,便於訪問和保存。
!apt-get install -y -qq software-properties-common python-software-properties module-init-tools
!add-apt-repository -y ppa:alessandro-strada/ppa 2>&1 > /dev/null
!apt-get update -qq 2>&1 > /dev/null
!apt-get -y install -qq google-drive-ocamlfuse fuse
from google.colab import auth
auth.authenticate_user()
from oauth2client.client import GoogleCredentials
creds = GoogleCredentials.get_application_default()
import getpass
!google-drive-ocamlfuse -headless -id={creds.client_id} -secret={creds.client_secret} < /dev/null 2>&1 | grep URL
vcode = getpass.getpass()
!echo {vcode} | google-drive-ocamlfuse -headless -id={creds.client_id} -secret={creds.client_secret}
!mkdir -p drive
!google-drive-ocamlfuse drive
import os
os.chdir("drive")
!ls
三、語料庫的構建
本實驗所采用的語料庫是來自於搜狗實驗室的搜狐新聞數據,包括2008年和2012年的新聞數據,我並從中選取了11種新聞分類的數據作為實驗所需的訓練和測試數據。
2012年新聞數據的文件格式是dat,編碼格式為gb2312,數據組織形式為xml。
2008年的新聞數據解壓后則是若干個txt文件,不過編碼格式也是gb2312,數據組織形式也是xml,格式一致。
從精簡版的示例數據,下圖可以看到數據的格式。
處理步驟如下:
1)將文件編碼格式轉為utf-8;
2)用正則表達式匹配出其中的url和content部分;
3)從url中抽取出二級域名字段作為該文本的類別標簽(content為文本內容);
4)忽略過長或過短的文本,統計閾值[30, 3000]內各類別下的文本數量,並排序展示;
5)從中選取適合做分類任務的11種分類:科技,汽車,股票,娛樂,體育,財經,健康,教育,女性,旅游,房地產,共計137萬篇文本數據;
6)最后將所需要的數據保存為pkl文件。
# 以下實驗所需要的包
import os
import re
import math
import operator
import chardet
import pickle
import jieba
import random
import datetime
from collections import defaultdict
from chardet import detect
import numpy as np
import pandas as pd
from sklearn.metrics import confusion_matrix,precision_score,recall_score,f1_score
1. 2012年新聞數據的處理
#轉換2012年新聞數據的文件格式及編碼格式
!cat news_sohusite_xml.dat | iconv -f gbk -t utf-8 -c > corpus.txt
#各類別文章統計
def cate_statistics(path):
classfiy = {} #各類別下的文章數
total = 0 #文章總數
cnt_gt = 0 #大於閾值長度的文章數
cnt_lt = 0 #小於閾值長度的文章數
f = open(path)
articles = f.readlines()
f.close()
for i in range(len(articles)//6):
line = articles[i*6+1]
content = articles[i*6+4]
line = re.sub('<url>|</url>','',line)
content = re.sub('<content>|</content>', '', content)
url_split = line.replace('http://', '').split('.')
sohu_index = url_split.index('sohu')
if len(content) > 30 and len(content) < 3000:
if url_split[sohu_index-1] not in classfiy.keys():
classfiy[url_split[sohu_index-1]] = 1
else:
classfiy[url_split[sohu_index-1]] += 1
total += 1
elif (len(content) <= 30):
cnt_lt += 1
elif (len(content) >= 3000):
cnt_gt += 1
sorted_classfiy = sorted(classfiy.items(), key=operator.itemgetter(1), reverse=True) #排序
print("文件名:", path)
print("文件格式示例:")
for i in range(6):
print(articles[i])
print("總共{}篇大小在(30, 3000)內的文章".format(total))
print("小於30字符的文章數:", cnt_lt)
print("大於3000字符的文章數:", cnt_gt)
print("各類別文章統計如下:(>=1000)")
for c in sorted_classfiy:
if c[1] >= 1000:
print(c[0], c[1])
#類別統計
cate_statistics("corpus.txt")
2. 2008年新聞數據的處理
#2008年數據文件格式修改以及txt文件合並
#獲取原始語料文件夾下文件列表
def listdir_get(path, list_name):
for file in os.listdir(path):
file_path = os.path.join(path, file)
if os.path.isdir(file_path):
listdir_get(file_path, list_name)
else:
list_name.append(file_path)
#修改文件編碼為utf-8
def code_transfer(list_name):
for fn in list_name:
with open(fn, 'rb+') as fp:
content = fp.read()
print(fn, ":現在修改")
codeType = detect(content)['encoding']
content = content.decode(codeType, "ignore").encode("utf8")
fp.seek(0)
fp.write(content)
print(fn, ":已修改為utf8編碼")
fp.close()
#合並各txt文件
def combine_txt(data_original_path, list_name, out_path):
cnt = 0 #未正確讀取的文件數
for name in list_name:
try:
file = open(name, 'rb')
fp = file.read().decode("utf8")
except UnicodeDecodeError:
cnt += 1
print("Error:", name)
file.close()
continue
print(name)
corpus_old = open(out_path, "a+", encoding="utf8")
corpus_old.write(fp)
corpus_old.close()
file.close()
print("共:", cnt, "文件未正確讀取")
#2008年數據文件格式修改以及txt文件合並
#原始語料路徑
data_original_path = "./SogouCS/"
out_path = "./corpus_old.txt"
#獲取文件路徑
list_name = []
listdir_get(data_original_path, list_name)
#修改編碼
code_transfer(list_name)
#合並各txt文件
combine_txt(data_original_path, list_name, out_path)
#類別統計
cate_statistics("corpus_old.txt")
3. 數據選取:選用部分類別數據
#讀取原始文件
f = open("corpus.txt")
article_list1 = f.readlines()
f.close()
f = open("corpus_old.txt")
article_list2 = f.readlines()
f.close()
article_list1.extend(article_list2)
labels = [] #文章標簽
texts = [] #文章內容
#選取[科技,汽車,股票,娛樂,體育,財經,健康,教育,女性,旅游,房地產]這11種類別數據
classes = ['it', 'auto', 'stock', 'yule', 'sports', 'business', 'health', 'learning', 'women', 'travel', 'house']
for i in range(len(article_list1)//6):
line = article_list1[i*6+1]
content = article_list1[i*6+4]
line = re.sub('<url>|</url>','',line)
content = re.sub('<content>|</content>', '', content)
url_split = line.replace('http://', '').split('.')
sohu_index = url_split.index('sohu')
if len(content) > 30 and len(content) < 3000:
if url_split[sohu_index-1] in classes:
labels.append(url_split[sohu_index-1])
texts.append(content)
cnt_class = list(np.zeros(11))
for label in labels:
for i in range(len(classes)):
if (classes[i] == label):
cnt_class[i] += 1
print("總共{}篇文章".format(len(labels)))
for i in range(len(classes)):
print("類別", classes[i], "數量為:", cnt_class[i])
#保存數據
with open("raw_data.pkl", 'wb') as f:
pickle.dump(labels, f)
pickle.dump(texts, f)
總共1370223篇文章
類別 it 數量為: 203316.0
類別 auto 數量為: 125100.0
類別 stock 數量為: 44424.0
類別 yule 數量為: 171270.0
類別 sports 數量為: 316819.0
類別 business 數量為: 233553.0
類別 health 數量為: 42240.0
類別 learning 數量為: 27478.0
類別 women 數量為: 54506.0
類別 travel 數量為: 28195.0
類別 house 數量為: 123322.0
四、數據預處理
數據預處理共以下幾步:
1)去除停用詞,通過停用詞表過濾掉一些在分類中不需要用到的字符;
2)中文分詞:使用Jieba分詞(精確模式);
3)數據測試集,訓練集:將原始數據按1:1分為測試集和訓練集;
4)提取特征詞:計算文檔中的詞項t與文檔類別c的互信息MI,MI度量的是詞的存在與否給類別c帶來的信息量。將每個類別下互信息排名前1000的單詞保留加入特征詞集合;
5)保存中間結果文件。
1. Jieba分詞以及去停用詞
#讀取停用詞表
def get_stopwords():
#加載停用詞表
stopword_set = set()
with open("./stop_words_ch.txt", 'r', encoding="utf-8") as stopwords:
for stopword in stopwords:
stopword_set.add(stopword.strip("\n"))
return stopword_set
for i in range(len(texts)):
line = texts[i]
result_content = "" #單行分詞結果
stopwords = get_stopwords()
words = jieba.cut(line, cut_all=False)
for word in words:
if word not in stopwords:
result_content += word + " "
texts[i] = result_content
if i % 10000 == 0:
print("已對{}萬篇文章分詞".format(i/10000))
#保存數據
with open("split_data.pkl", 'wb') as f:
pickle.dump(labels, f)
pickle.dump(texts, f)
2. 計算互信息,抽取特征詞,統計數量
#讀取上一步保存的數據
with open("./split_data.pkl", "rb") as f:
labels = pickle.load(f)
texts = pickle.load(f)
#划分訓練集和測試集,大小各一半
trainText = []
for i in range(len(labels)):
trainText.append(labels[i] + ' ' + texts[i])
#數據隨機
random.shuffle(trainText)
num = len(trainText)
testText = trainText[num//2:]
trainText = trainText[:num//2]
print("訓練集大小:", len(trainText)) # 685111
print("測試集大小:", len(testText)) # 685112
#文章類別列表
classes = ['it', 'auto', 'stock', 'yule', 'sports', 'business', 'health', 'learning', 'women', 'travel', 'house']
#獲取對應類別的索引下標值
def lable2id(label):
for i in range(len(classes)):
if label == classes[i]:
return i
raise Exception('Error label %s' % (label))
#構造和類別數等長的0向量
def doc_dict():
return [0]*len(classes)
#計算互信息,這里log的底取為2
def mutual_info(N, Nij, Ni_, N_j):
return 1.0*Nij/N * math.log(N*1.0*(Nij+1)/(Ni_*N_j)) / math.log(2)
#統計每個詞在每個類別出現的次數,和每類的文檔數,計算互信息,提取特征詞
def count_for_cates(trainText, featureFile):
docCount = [0] * len(classes) #各類別單詞計數
wordCount = defaultdict(doc_dict) #每個單詞在每個類別中的計數
#掃描文件和計數
for line in trainText:
lable, text = line.strip().split(' ',1)
index = lable2id(lable) #類別索引
words = text.split(' ')
for word in words:
if word in [' ', '', '\n']:
continue
wordCount[word][index] += 1
docCount[index] += 1
#計算互信息值
print("計算互信息,提取特征詞中,請稍后...")
miDict = defaultdict(doc_dict)
N = sum(docCount)
#遍歷每個分類,計算詞項k與文檔類別i的互信息MI
for k,vs in wordCount.items():
for i in range(len(vs)):
N11 = vs[i] #類別i下單詞k的數量
N10 = sum(vs) - N11 #非類別i下單詞k的數量
N01 = docCount[i] - N11 #類別i下其他單詞數量
N00 = N - N11 - N10 - N01 #其他類別中非k單詞數目
mi = mutual_info(N,N11,N10+N11,N01+N11) + mutual_info(N,N10,N10+N11,N00+N10) + mutual_info(N,N01,N01+N11,N01+N00) + mutual_info(N,N00,N00+N10,N00+N01)
miDict[k][i] = mi
fWords = set()
#遍歷每個單詞
for i in range(len(docCount)):
keyf = lambda x:x[1][i]
sortedDict = sorted(miDict.items(), key=keyf, reverse=True)
# 打印每個類別中排名前20的特征詞
t=','.join([w[0] for w in sortedDict[:20]])
print(classes[i], ':', t)
for j in range(1000):
fWords.add(sortedDict[j][0])
out = open(featureFile, 'w', encoding='utf-8')
#輸出各個類的文檔數目
out.write(str(docCount)+"\n")
#輸出互信息最高的詞作為特征詞
for fword in fWords:
out.write(fword+"\n")
print("特征詞寫入完畢!")
out.close()
count_for_cates(trainText, 'featureFile')
計算互信息,提取特征詞中,請稍后...
it : G,M,系列,U,P,I,e,華碩,n,英寸,o,尺寸,屏幕,主頻,C,硬盤容量,內存容量,芯片,顯卡,z
auto : m,座椅,/,調節,發動機,汽車,電動,車型,后排,方向盤,車,系統,車身,元,大燈,變速箱,天窗,懸掛,自動,雜費
stock : 萬股,-,.,公司,公告,股,流通,股票走勢,股份,經濟,U,投資,ㄔ,鶉,嗉,基金,e,偽,M,比賽
yule : 電影,娛樂,導演,.,觀眾,演員,拍攝,訊,影片,/,主演,G,市場,電視劇,飾演,明星,演出,歌手,-,周筆暢
sports : 比賽,球隊,球員,分,體育,賽季,體育訊,主場,聯賽,對手,火箭,M,市場,冠軍,球迷,公司,-,決賽,e,中國隊
business : 證券,資訊,投資者,公司,合作,市場,風險,機構,自擔,本頻道,轉引,基金,據此,e,入市,謹慎,-,判斷,觀點,立場
health : 患者,治療,醫院,本品,【,】,疾病,主任醫師,醫生,葯品,處方,病人,看病,服用,症狀,票下夜,感染,葯物,劑量,大夫
learning : 考生,學生,高考,專業,招生,教育,學校,錄取,考試,類,志願,大學,高校,第二批,老師,家長,孩子,經濟學,笱,院校
women : 女人,男人,肌膚,.,皮膚,a,時尚,減肥,女性,徐勇,搭配,-,市場,比賽,頭發,傅羿,公司,美容,膚質,護膚
travel : 旅游,游客,酒店,旅行社,旅客,景區,航空,航班,游,航空公司,機場,線路,民航,飛機,.,東航,景點,G,出游,李妍
house : O,恪,小區,編號,煌,面積,散布,/,鰲,建築面積,裝修,民族,J,衛,喬與,行期,ゲ,廳,層,〕
特征詞寫入完畢!
五、模型訓練
訓練朴素貝葉斯模型,計算每個類中特征詞的出現次數,也即類別i下單詞k的出現概率,並使用拉普拉斯平滑(加一平滑)。得到模型后對測試數據進行分類。
#從特征文件導入特征詞
def load_feature_words(featureFile):
f = open(featureFile, encoding='utf-8')
#各個類的文檔數目
docCounts = eval(f.readline())
features = set()
#讀取特征詞
for line in f:
features.add(line.strip())
f.close()
return docCounts, features
# 訓練貝葉斯模型,實際上計算每個類中特征詞的出現次數
def train_bayes(featureFile, textFile, modelFile):
print("使用朴素貝葉斯訓練中...")
start = datetime.datetime.now()
docCounts, features = load_feature_words(featureFile) #讀取詞頻統計和特征詞
wordCount = defaultdict(doc_dict)
#每類文檔特征詞出現的次數
tCount = [0]*len(docCounts)
#遍歷每個文檔
for line in textFile:
lable, text = line.strip().split(' ',1)
index = lable2id(lable)
words = text.strip().split(' ')
for word in words:
if word in features and word not in [' ', '', '\n']:
tCount[index] += 1 #類別index中單詞總數計數
wordCount[word][index] += 1 #類別index中單詞word的計數
end = datetime.datetime.now()
print("訓練完畢,寫入模型...")
print("程序運行時間:"+str((end-start).seconds)+"秒")
#拉普拉斯平滑
outModel = open(modelFile, 'w', encoding='utf-8')
#遍歷每個單詞
for k,v in wordCount.items():
#遍歷每個類別i,計算該類別下單詞的出現概率(頻率)
scores = [(v[i]+1) * 1.0 / (tCount[i]+len(wordCount)) for i in range(len(v))]
outModel.write(k+"\t"+str(scores)+"\n") #保存模型,記錄類別i下單詞k的出現概率(頻率)
outModel.close()
train_bayes('./featureFile', trainText, './modelFile')
使用朴素貝葉斯訓練中...
訓練完畢,寫入模型...
程序運行時間:45秒
六、預測分類
#從模型文件中導入計算好的貝葉斯模型
def load_model(modelFile):
print("加載模型中...")
f = open(modelFile, encoding='utf-8')
scores = {}
for line in f:
word,counts = line.strip().rsplit('\t',1)
scores[word] = eval(counts)
f.close()
return scores
#預測文檔分類,標准輸入每一行為一個文檔
def predict(featureFile, modelFile, testText):
docCounts, features = load_feature_words(featureFile) #讀取詞頻統計和特征詞
docScores = [math.log(count * 1.0 /sum(docCounts)) for count in docCounts] #每個類別出現的概率
scores = load_model(modelFile) #加載模型,每個單詞在類別中出現的概率
indexList = []
pIndexList = []
start = datetime.datetime.now()
print("正在使用測試數據驗證模型效果...")
for line in testText:
lable, text = line.strip().split(' ', 1)
index = lable2id(lable)
words = text.split(' ')
preValues = list(docScores)
for word in words:
if word in features and word not in [' ', '', '\n']:
for i in range(len(preValues)):
#利用貝葉斯公式計算對數概率,后半部分為每個類別中單詞word的出現概率
preValues[i] += math.log(scores[word][i])
m = max(preValues) #取出最大值
pIndex = preValues.index(m) #取出最大值類別的索引
indexList.append(index)
pIndexList.append(pIndex)
end = datetime.datetime.now()
print("程序運行時間:"+str((end-start).seconds)+"秒")
return indexList, pIndexList
indexList, pIndexList = predict('./featureFile', './modelFile', testText)
加載模型中...
正在使用測試數據驗證模型效果...
程序運行時間:287秒
七、結果評估
對分類得到結果打印混淆矩陣,並計算各類的精確率,召回率,F1值。
精確率p= TP/(TP+FP), 召回率r= TP/(TP+FN), F1分數=2pr/(p+r)
#打印混淆矩陣
C=confusion_matrix(indexList, pIndexList)
pd.DataFrame(C, index=classes, columns=classes)
#計算各類的精確率,召回率,F1值
p = precision_score(indexList, pIndexList, average=None)
r = recall_score(indexList, pIndexList, average=None)
f1 = f1_score(indexList, pIndexList, average=None)
p_max,r_max,p_min,r_min = 0,0,1,1
for i in range(len(classes)):
print("類別{:8}".format(classes[i]), end=" ")
print("精確率為:{}, 召回率為:{}, F1值為:{}".format(p[i], r[i], f1[i]))
if (p[i] > p_max): p_max = p[i]
if (r[i] > r_max): r_max = r[i]
if (p[i] < p_min): p_min = p[i]
if (r[i] < r_min): r_min = r[i]
#計算總體的精確率,召回率,F1值
pa = precision_score(indexList, pIndexList, average="micro")
ra = recall_score(indexList, pIndexList, average="micro")
f1a = f1_score(indexList, pIndexList, average="micro")
print("總體{:8}".format("====>"), end=" ")
print("精確率為:{}, 召回率為:{}, F1值為:{}".format(pa, ra, f1a))
print("最大{:8}".format("====>"), end=" ")
print("精確率為:{}, 召回率為:{}".format(p_max, r_max))
print("最小{:8}".format("====>"), end=" ")
print("精確率為:{}, 召回率為:{}".format(p_min, r_min))
類別it 精確率為:0.9369704295863791, 召回率為:0.8111881207669371, F1值為:0.8695541738325174
類別auto 精確率為:0.9719916415850315, 召回率為:0.8107520347954011, F1值為:0.8840802092414995
類別stock 精確率為:0.6812699400824839, 召回率為:0.7910906298003072, F1值為:0.732084622460072
類別yule 精確率為:0.876883401920439, 召回率為:0.9332196580398019, F1值為:0.9041748468166722
類別sports 精確率為:0.9860387278704942, 召回率為:0.9407128040731662, F1值為:0.9628426304496779
類別business 精確率為:0.8546096508434681, 召回率為:0.8414276149765669, F1值為:0.8479674058311384
類別health 精確率為:0.8022101355434689, 召回率為:0.8812176964294182, F1值為:0.8398599028358378
類別learning 精確率為:0.703915453915454, 召回率為:0.895986474566304, F1值為:0.7884217335058215
類別women 精確率為:0.677366842781665, 召回率為:0.8686003076246979, F1值為:0.7611559506426405
類別travel 精確率為:0.4035347064813902, 召回率為:0.8687411598302688, F1值為:0.5510867858504745
類別house 精確率為:0.9723457054612322, 召回率為:0.881294498381877, F1值為:0.9245838744450952
總體> 精確率為:0.8746088230829412, 召回率為:0.8746088230829412, F1值為:0.8746088230829411
最大> 精確率為:0.9860387278704942, 召回率為:0.9407128040731662
最小====> 精確率為:0.4035347064813902, 召回率為:0.7910906298003072