數理統計——新聞分類
前言
本篇學習文字分類中常見的新聞分類,根據新聞文字中的內容,進行文字預處理,建模等操作,從而自動實現將新聞劃分到最可能的類別中。也可以將該案例遷移到其他根據文字內容來實現的分類場景,例如垃圾郵件、情感分析等。
具體:
- 能夠對文字資料進行預處理。【文字清洗,分詞,去除停用詞,文字向量化等】
- 能夠通過Python實現統計詞頻,生成詞雲圖。【描述性統計分析】
- 能夠通過方差分析,進行特徵選擇。【驗證性統計分析】
- 能夠根據樣本內容,對文字資料進行分類。【統計建模】
一、具體案例
1.資料集簡介
資料集是2016年1月1日-2018年10月9日期間新聞聯播的資料,包括:
列名 | 說明 |
---|---|
date | 新聞日期 |
tag | 新聞類別 |
headline | 新聞標題 |
content | 詳細內容 |
2.載入資料
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
sns.set(style="darkgrid",font_scale=1.2)
plt.rcParams["font.family"]="SimHei"
plt.rcParams["axes.unicode_minus"]=False
news=pd.read_csv("news-Copy1.csv")
print(news.shape)
display(news.head())
3.資料預處理
(1)文字資料
結構化資料:多行多列,每行每列都有具體的含義;
非結構化資料:無法合理地表示為多列多行,即使如此,每列、行也沒有具體的含義。
(2)文字資料的預處理
- 缺失值處理:
#缺失值預覽
#news.info()
news.isnull().sum()
輸出:
date 0
tag 0
headline 0
content 107
結果發現內容列有107個缺失值。
如何處理:使用缺失列的標題來代替缺失的內容。
index=news[news["content"].isnull()].index
news["content"][index]=news["headline"][index]
news.isnull().sum()
輸出:
date 0
tag 0
headline 0
content 0
填充完成後,想要檢測一下填充的效果:
news.loc[index].sample(5)
輸出:
date tag headline content
12880 2017-10-04 詳細全文 十九大代表風采 十九大代表風采
13613 2017-11-09 詳細全文 開創中越友好新局面 開創中越友好新局面
19952 2018-09-02 詳細全文 聯合國機構 聯合國機構
3163 2016-06-22 詳細全文 習近平出席 習近平出席
4787 2016-09-14 詳細全文 李克強會見祕魯總統 李克強會見祕魯總統
事實證明填充OK
- 重複值處理:需要刪除
#檢視重複值
print(news.duplicated().sum())
display(news[news.duplicated()])
輸出:從結果可以看到,有5條蟲重複值。
刪除重複的記錄,並檢測是否刪除成功:
news.drop_duplicates(inplace=True)
print(news.duplicated().sum())
輸出:0
- 文字內容清洗
文字中的標點符號,與一些特殊字元,相當於異常值,因此需要剔除掉。
import re
re_obj = re.compile(
r"[!\"#$%&'()*+,-./:;<=>?@[\\\]^_`{|}~—!,。?、¥...():【】《》‘’“”\s]+")
def clear(text):
return re_obj.sub("", text)
news["content"] = news["content"].apply(clear)
news.sample(5)
- 分詞
分詞是將連續的文字分隔成語義合理的若干詞彙序列,中文通過jieba來實現分詞的功能。
#方案一:使用cut方法:返回的是生成器,需要使用print(list(words))進行轉換。
#分詞,中文需要使用jieba來實現
import jieba
s="今天,外面下了一場小雨。"
words=jieba.cut(s)
print(words)
print(list(words))
輸出:
<generator object Tokenizer.cut at 0x000001F5CCFD2D68>
['今天', ',', '外面', '下', '了', '一場', '小雨', '。'
#方案二:lcut方法返回的是一個列表
import jieba
s="今天,外面下了一場小雨。"
words=jieba.lcut(s)
print(words)
輸出:
['今天', ',', '外面', '下', '了', '一場', '小雨', '。']
cut與lcut的區別:前者返回生成器,後者返回列表。
優先選擇lcut,生成器放在list裡面能產生和List一樣的效果。但是當計算量特別大的時候,呼叫next方法的時候把生成器放在一個for 迴圈中進行迭代,這樣就是從生成器中一個個地獲取元素,這樣比List計算出來的佔用的空間更小。這就是生成器相比list的一個優勢所在。同時遍歷的並不是所有元素,而是符合自己條件的,因此空間佔用也較小。當然列表也有其優勢所在,即可以使用索引,在獲取某些元素時比較方便。
def cut_word(text):
return jieba.cut(text)
news["content"] = news["content"].apply(cut_word)
news.sample(5)
返回生成器:
- 停用詞處理
停用詞是在語句中大量出現,並且對語義分析沒有幫助的詞,直接將其刪除即可。
有兩種方案:使用list或者set, list的時間複雜度是o(n),而set的時間複雜度是,會進行一個雜湊對映,是一種底層實現,其時間複雜度是o(1),所以使用set的時間速度更快一些。
停用詞處理:
方案一:使用set:
def get_stopword():
s=set()
with open("stopword.txt",encoding="UTF-8")as f:
for line in f:
s.add(line.strip())
return s
def remove_stopword(words):
return[word for word in words if word not in stopword]
stopword=get_stopword()
news["content"]=news["content"].apply(remove_stopword)
news.sample(5)
方案二:使用list
def get_stopword():
s=list()
with open("stopword.txt",encoding="UTF-8")as f:
for line in f:
s.append(line.strip())
return s
def remove_stopword(words):
return[word for word in words if word not in stopword]
stopword=get_stopword()
news["content"]=news["content"].apply(remove_stopword)
news.sample(5)
4.資料探索
(1)統計每種新聞類別數量分佈
sns.countplot(x="tag",data=news)
(2)根據年月日來統計新聞的數量情況
首先需要根據date列通過向量化來轉換成dataframe形式:
#統計年月日的新聞數量分佈情況
t=news["date"].str.split("-",expand=True)
t.head()
輸出:
0 1 2
0 2016 01 01
1 2016 01 01
2 2016 01 01
3 2016 01 01
4 2016 01 01
#按照年統計
sns.countplot(t[0])
plt.xlabel("年份")
plt.ylabel("新聞數量")
可以從結果中看到,2018年新聞數量較前兩年少,這是因為統計區間在2016年1月1日至2018年10月9日,所以2018年少了兩個多的月新聞數量。
#按照月統計
sns.countplot(t[1])
plt.xlabel("月份")
plt.ylabel("新聞數量")
同樣的月份10、12、11也呈現出同年份一樣的數量趨勢,可歸於取樣原因。
#按照日統計
plt.figure(figsize=(15,5))
sns.countplot(t[2])
plt.xlabel("日")
plt.ylabel("新聞數量")
從日統計上可以看到,30、31日新聞數量較少,則是因為很多月份並沒有30、31日的存在,所以總的累計較少。
(3)詞彙統計
詞頻統計:
#統計總詞彙數量、不重複詞彙數量、前15個出現最多的詞彙
from itertools import chain
from collections import Counter
li_2d=news["content"].tolist()
#將二維列表轉化為一維列表,chain.from_iterable(li_2d)返回的是迭代器,通過list轉化成列表
li_1d=list(chain.from_iterable(li_2d))
print(f"總詞彙量:{len(li_1d)}")
#counter可以計算每個詞彙出現了多少次,是一個計數器
c=Counter(li_1d)
print(f"不重複詞彙數量:{len(c)}")
common=c.most_common(15)
print(common)
輸出:
總詞彙量:2192248
不重複詞彙數量:94476
[('發展', 20414), ('中國', 18784), ('習近平', 13424), ('合作', 12320), ('新', 11669), ('年', 11643), ('國家', 10881), ('日', 10585), ('中', 10527), ('工作', 9328), ('建設', 8331), ('月', 8179), ('經濟', 7239), ('主席', 6786), ('推動', 6271)]
詞頻柱狀圖視覺化:
d=dict(common)
plt.figure(figsize=(15,5))
sns.barplot(list(d.keys()),list(d.values()))
top15的頻率統計:
total=len(li_1d)
percentage=[v*100/total for v in d.values()]
#小數點只保留兩位
print([f"{v:2f}%" for v in percentage])
plt.figure(figsize=(15,5))
sns.barplot(list(d.keys()),percentage)
輸出:
['0.931190%', '0.856837%', '0.612339%', '0.561980%', '0.532285%', '0.531099%', '0.496340%', '0.482838%', '0.480192%', '0.425499%', '0.380021%', '0.373087%', '0.330209%', '0.309545%', '0.286053%']
所有詞頻的分佈統計:
plt.figure(figsize=(15,5))
v=list(c.values())
end=np.log10(max(v))
#hist_kws={"log":True}表示y軸取對數,原因是因為低頻詞與高頻詞的數量相差太大。因此取對數可以看到高頻詞出現的數量,否則就只能看到數量級特別大的低頻詞的y軸,看不見高頻詞的y軸,取對數能增強資訊呈現的效果;kde=False表示取消展示密度函式
ax=sns.distplot(v,bins=np.logspace(0,end,num=10),hist_kws={"log":True},kde=False)
ax.set_xscale("log")
新聞詞彙長度統計:取前邊15篇新聞
plt.figure(figsize=(15,5))
num=[len(li)for li in li_2d]
length=15
sns.barplot(np.arange(1,length+1),sorted(num,reverse=True)[:length])
新聞詞彙長度的一個直方分佈情況:
plt.figure(figsize=(15,5))
sns.distplot(num,bins=15,hist_kws={"log":True},kde=False)
可以看到詞彙長度較少的新聞數量還是佔比比較大。而個別詞彙數量特別多的新聞篇幅並不多。
生成詞雲圖
在python中,wordcloud提供了生成詞雲圖的功能,需要獨立安裝,非anaconda預設模組。
標準詞雲圖:
from wordcloud import WordCloud
#指定字型的位置,否則中文無法正常顯示
wc=WordCloud(font_path=r"C:/Windows/Fonts/STFANGSO.ttf",width=800,height=600)
li_2d=news["content"].tolist()
li_1d=list(chain.from_iterable(li_2d))
#WordCloud要求傳遞使用空格分開的字串
join_words=" ".join(li_1d)
img=wc.generate(join_words)
plt.figure(figsize=(15,10))
plt.imshow(img)
plt.axis('off')#取消座標軸的刻度
wc.to_file("wordcloud.png")#自動儲存圖片
自定義背景圖:個性化詞雲圖
wc = WordCloud(font_path=r"C:/Windows/Fonts/STFANGSO.ttf", mask=plt.imread("../map.jpg"))
img = wc.generate(join_words)
plt.figure(figsize=(15, 10))
plt.imshow(img)
plt.axis('off')
可以修改背景顏色:
wc = WordCloud(font_path=r"C:/Windows/Fonts/STFANGSO.ttf", mask=plt.imread("../map.jpg"),background_color="white")
img = wc.generate(join_words)
plt.figure(figsize=(15, 10))
plt.imshow(img)
plt.axis('off')
修改一下引數background_color="white"就將圖片變成了白色的了。
從上面可以看到,詞雲圖的詞語並非按照出現數量多少來顯示詞語的大小,因此下面的操作用來實現按照詞頻展示詞語大小的功能。
#根據詞頻來生成詞雲圖
plt.figure(figsize=(15,10))
img=wc.generate_from_frequencies(c)
plt.imshow(img)
plt.axis("off")
可以看到“發展”、“中國”變成了最大的詞語。
5.文字向量化
對文字資料進行建模,有兩個問題需要解決:
- 模型進行的是數學運算,因此需要數值型別的資料,而文字不是數值型別的資料;
- 模型需要結構化的資料,而文字是非結構化的資料。
因此將文字轉換為數值特徵向量化的過程就是文字向量化,將文字向量化有以下兩個步驟:
- 對文字分詞,拆分成更容易處理的詞;
- 將單詞轉換為數值型別,即使用合適的數值來表示每個單詞;同時需要將其轉換為結構化資料。
詞袋模型:
即一個裝滿單詞的袋子,是一種能夠將文字向量化的方式,在詞袋模型中,每個文件為一個樣本,每個不重複的單詞為一個特徵,單詞在文件中出現的次數作為特徵值。
#文字向量化——詞袋模型,將文字資料轉換為結構化資料。
from sklearn.feature_extraction.text import CountVectorizer
count=CountVectorizer()
docs=["Where there is a will, there is a way.",
"There is no royal road to learning.", ]
bag=count.fit_transform(docs)
#bag是一個稀疏矩陣,只顯示有特徵值的文字的座標和特徵值;
print(bag)
#呼叫稀疏矩陣的toarray方法,將稀疏矩陣轉換為ndarray物件(稠密矩陣)
print(bag.toarray())
輸出:
(0, 7) 1
(0, 9) 1
(0, 0) 2
(0, 5) 2
(0, 8) 1
(1, 1) 1
(1, 6) 1
(1, 3) 1
(1, 4) 1
(1, 2) 1
(1, 0) 1
(1, 5) 1
[[2 0 0 0 0 2 0 1 1 1]
[1 1 1 1 1 1 1 0 0 0]]
預設情況下,CountVectorizer只會對字元長度不小於2的單詞進行處理,僅有一個單詞則會忽略,比如上文中的“a”
±–
#獲取每個特徵對應的單詞:
print(count.get_feature_names())
#輸出單詞與編號的對映關係:
print(count.vocabulary_)
輸出:
['is', 'learning', 'no', 'road', 'royal', 'there', 'to', 'way', 'where', 'will']
{'where': 8, 'there': 5, 'is': 0, 'will': 9, 'way': 7, 'no': 2, 'royal': 4, 'road': 3, 'to': 6, 'learning': 1}
#經過訓練之後,CountVectorizer可以對未知文件(訓練集外的文件)進行向量化,向量化的特徵僅僅為訓練集中出現過的單詞特徵,如果未知文件中的單詞不在訓練集中出現,則在詞袋模型中無法體現。
test_docs=["While there is life there is hope.", "No pain, no gain."]
t=count.transform(test_docs)
#返回稠密矩陣
print(t.toarray())
輸出:
[[2 0 0 0 0 2 0 0 0 0]
[0 0 2 0 0 0 0 0 0 0]]
由於pain\gain等詞語在之前的訓練集中沒有出現,所以這些詞彙在結果中無法提現。
TF-IDF值:
有些單詞不能僅僅以當前文件中的頻數來衡量單詞的重要性,還要考慮在語料庫中,在其他文件中出現的次數,因為有些單詞很大眾化,出現非常頻繁,在許多文件中出現都比較多,因此就應該降低其在文件中的重要性。
TF:(term-frequency):指的是一個單詞在文件中出現的次數;
IDF:(inverse documennt-frequency):逆文件頻率;
在sklearn中實現tf-idf轉換,與標準公式略有不同,並且此結果會使用L1或者L2進行標準化(規範化)處理。
from sklearn.feature_extraction.text import TfidfTransformer
count=CountVectorizer()
docs=["Where there is a will, there is a way.",
"There is no royal road to learning.",]
bag=count.fit_transform(docs)
tfidf=TfidfTransformer()
t=tfidf.fit_transform(bag)
#TfidfTransformer()轉換的結果也是稀疏矩陣
print(t.toarray())
輸出:
[[0.53594084 0. 0. 0. 0. 0.53594084
0. 0.37662308 0.37662308 0.37662308]
[0.29017021 0.4078241 0.4078241 0.4078241 0.4078241 0.29017021
0.4078241 0. 0. 0. ]]
在sklearn中還提供了一個類TfidfVectorizer,可以直接地將文件轉換為TF-IDF值,該類整合了TfidfTransformer與CountVectorizer的功能。為實現提供便利。
from sklearn.feature_extraction.text import TfidfVectorizer
docs=["Where there is a will, there is a way.",
"There is no royal road to learning.",]
tfidf=TfidfVectorizer()
t=tfidf.fit_transform(docs)
print(t.toarray())
輸出:
[[0.53594084 0. 0. 0. 0. 0.53594084
0. 0.37662308 0.37662308 0.37662308]
[0.29017021 0.4078241 0.4078241 0.4078241 0.4078241 0.29017021
0.4078241 0. 0. 0. ]]
6.建立模型
構建訓練集和測試集:
首先需要對每條新聞詞彙進行整理:前面已經完成了分詞的處理,但詞彙是以列表形式呈現,而在文字向量化中需要傳遞空格分開的字串陣列型別,因此我們需要將每條新聞的詞彙組合在一起,使用空格分隔。
#將每條新聞的詞彙組合在一起,使用空格分隔
def join(text_list):
return " ".join(text_list)
news["content"]=news["content"].apply(join)
news.sample(5)
輸出結果是空格拼接的字串。
接下來需要將標籤列(tag列)的類別變數轉換為離散變數,並對兩個 離散變數進行計數:
news["tag"]=news["tag"].map({"詳細全文":0,"國內":0,"國際":1})
news["tag"].value_counts()
輸出:
0 17715
1 3018
從結果可以看到,國內的新聞有17715條,國外有3018條。
接下來對樣本資料進行切分,構建訓練集和測試集:
from sklearn.model_selection import train_test_split
x=news["content"]
y=news["tag"]
x_train,x_test,y_train,y_test=train_test_split(x,y,test_size=0.25)
print("訓練集樣本數:",y_train.shape[0],"測試集樣本數:",y_test.shape[0])
輸出:
訓練集樣本數: 15549 測試集樣本數: 5184
特徵維度:
需要先將其進行向量化操作,使用TfidfVectorizer類,在訓練集上進行訓練,然後分別對測試集和訓練集實施轉換。
vec=TfidfVectorizer(ngram_range=(1,2))#ngram_range=(1,2)表示n元組模型,不僅要拿一個詞作為特徵,還要拿兩個連續的詞作為特徵,可以保留詞語間的順序問題,確保語義儘量正確
x_train_tran=vec.fit_transform(x_train)#在訓練集上進行訓練
x_test_tran=vec.transform(x_test)#通過transform對測試集進行轉換,不要把整個資料全部放在訓練集中,在真正測試模型的時候永遠不要使用測試集的資料
display(x_train_tran,x_test_tran)
輸出:
<15549x899021 sparse matrix of type '<class 'numpy.float64'>'
with 2476447 stored elements in Compressed Sparse Rowformat>
<5184x899021 sparse matrix of type '<class 'numpy.float64'>'
with 601775 stored elements in Compressed Sparse Row format>
從結果可以看到,一共選擇了899021 個特徵,成功進行了轉換,不過資料儲存在稀疏矩陣中,如果呼叫稀疏矩陣的toarray方法,變成稠密矩陣的話,並不能看到T-IDF的值,因為遠遠超出了記憶體大小。
由於產生了特別多的特徵,對儲存和計算記憶體都會造成巨大壓力,同時也並不是所有特徵都對建模有所幫助,基於以上原則,需要將資料在送入模型之前,進行特徵選擇。 因此在進行特徵選擇的時候,看看特徵對分類是否有幫助,我們使用方差分析(ANOVA) 來進行特徵選擇,選擇與目標分類變數相關的20000個特徵,方差分析用來分析兩個或多個樣本(來自不同總體)的均值是否相同,進而用來檢驗連續變數和分類變數之間是否相關,檢驗方式為根據分類變數的不同取值,將樣本進行分組,計算組內(SSE)與組間(SSM)之間的差異:
注意:t檢驗和F檢驗的區別。
方差分析就是比較多個類別的均值,如果類別間均值差異顯著,則表明對分類有所幫助,因此在特徵選擇時可以留下來。反之。
組內的差異主要是受到抽樣的影響、而組間的差異用來每個組的均值與所有觀測值的均值,組間也有抽樣的影響,同時也有組和組之間的本身差別的影響,也就意味著SSM大於等於SSE。因此怎樣衡量特徵是否對分類有幫助呢——通過構造一個統計量SSM/SSE的F值來看,如果組與組之間的差異大的話,那麼SSM會比SSE大很多,例如“發展”一詞在國內的TF-IDF很高,在國外很低。因此F值越大,說明組與組之間差異越大,如果F值越小,趨近於0,則說明只受抽樣誤差的影響,也就對特徵選擇沒有幫助。
#F值越大,p值越小,原假設成立的可能性越小
from sklearn.feature_selection import f_classif
f_classif(x_train_tran,y_train)
輸出:
(array([1.16518778, 0.17137854, 0.17137854, ..., 0.17137854, 0.17137854,
0.17137854]),
array([0.28040898, 0.67889526, 0.67889526, ..., 0.67889526, 0.67889526,
0.67889526]))
特徵選擇從90多萬個特徵中20000個對分類有所幫助的特徵:
#特徵選擇從90多萬個特徵中20000個對分類有所幫助的特徵
from sklearn.feature_selection import SelectKBest
#tfidf不需要太多的精度,使用32位的浮點數就可以了
x_train_tran=x_train_tran.astype(np.float32)
x_test_tran=x_test_tran.astype(np.float32)
#定義特徵選擇器,用來選擇最好的特徵:
selector=SelectKBest(f_classif,k=min(20000,x_train_tran.shape[1]))
selector.fit(x_train_tran,y_train)
#對訓練集和測試集進行轉換:
x_train_tran=selector.transform(x_train_tran)
x_test_tran=selector.transform(x_test_tran)
print(x_train_tran.shape,x_test_tran.shape)
輸出:
(15549, 20000) (5184, 20000)
使用樸素貝葉斯實現文字分類:
from sklearn.naive_bayes import GaussianNB, BernoulliNB, MultinomialNB, ComplementNB
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import FunctionTransformer
from sklearn.model_selection import GridSearchCV
# 定義FunctionTransformer函式轉換器,用來將稀疏矩陣轉換為稠密矩陣。
steps = [("dense", FunctionTransformer(func=lambda X: X.toarray(), accept_sparse=True)),
("model", None)
]
pipe = Pipeline(steps=steps)
param = {"model": [GaussianNB(), BernoulliNB(), MultinomialNB(),
ComplementNB()]}
# 因為是稠密矩陣,因此比較消耗記憶體空間,記憶體小的,這裡建議改成少的併發數量。
gs = GridSearchCV(estimator=pipe, param_grid=param,
cv=2, scoring="f1", n_jobs=2, verbose=10)
gs.fit(x_train_tran, y_train)
print(gs.best_params_)
y_hat = gs.best_estimator_.predict(x_test_tran)
print(classification_report(y_test, y_hat))
輸出:執行時間太長,執行不出來,我哭。
從課件中截了個圖:
還可以選擇多個模型進行測試比較。
相關文章
- 中文新聞情感分類 Bert-Pytorch-transformersPyTorchORM
- 使用貝葉斯進行新聞分類
- C語言: 分類統計字元個數C語言字元
- 數理統計02:抽樣分佈與次序統計量
- 樸素貝葉斯--新浪新聞分類例項
- 數理統計11:區間估計,t分佈,F分佈
- 數理統計6:泊松分佈,泊松分佈與指數分佈的聯絡,離散分佈引數估計
- 數理統計基礎 統計量
- 數理統計筆記筆記
- 數理統計學概貌
- NLP入門競賽,搜狗新聞文字分類!拿幾十萬獎金!文字分類
- 領域驅動設計,構建簡單的新聞系統,20分鐘夠嗎?
- 【數理統計】基本概念
- 模式識別問題——支援向量機、數理統計方法、聚類分析模式聚類
- flutter實戰3:解析HTTP請求資料和製作新聞分類列表FlutterHTTP
- 深度學習之新聞多分類問題深度學習
- 概率論與數理統計 17
- 概率論與數理統計 19
- 概率論與數理統計(1)
- 數學處理類
- 2021數字新聞報告:全球新聞媒體線上付費情況
- OCT影像分類1:相關論文統計
- Android許可權處理分類Android
- 小程式雲開發之新聞類專案分析
- 央視新聞《一分快速三全天穩定計劃》手機搜狐網
- 央視新聞《一分快三彩票計劃表》手機搜狐網
- 進化計算中基於分類的預處理代理模型模型
- 2021年六大傳統新聞媒體戰略鎖定「新聞簡報」
- 計數類 DP
- 瑞星:週末攔截掛馬網站數減少新聞類網站佔主打網站
- 聊聊新的遊戲分類方式遊戲
- 數理統計9:完備統計量,指數族,充分完備統計量法,CR不等式
- [PAT B] 1012 數字分類
- 數學-概率與統計-數理統計-總結(四):方差分析及迴歸分析
- java統計實體類中空欄位數量Java
- IT新聞類軟文營銷的三大寫作技巧
- 數倉建模分層理論
- 【B/S】牛腩新聞釋出系統——CSSCSS