數理統計——新聞分類

嚯嚯嚯嚯什麼都不會要死了發表於2020-12-01

前言

本篇學習文字分類中常見的新聞分類,根據新聞文字中的內容,進行文字預處理,建模等操作,從而自動實現將新聞劃分到最可能的類別中。也可以將該案例遷移到其他根據文字內容來實現的分類場景,例如垃圾郵件、情感分析等。

具體:

  • 能夠對文字資料進行預處理。【文字清洗,分詞,去除停用詞,文字向量化等】
  • 能夠通過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)) 
輸出:執行時間太長,執行不出來,我哭。               

從課件中截了個圖:
在這裡插入圖片描述

還可以選擇多個模型進行測試比較。

相關文章