数理统计——新闻分类

前言

本篇学习文本分类中常见的新闻分类,根据新闻文本中的内容,进行文本预处理,建模等操作,从而自动实现将新闻划分到最可能的类别中。也可以将该案例迁移到其他根据文本内容来实现的分类场景,例如垃圾邮件、情感分析等。

具体:

  • 能够对文本数据进行预处理。【文本清洗,分词,去除停用词,文本向量化等】
  • 能够通过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)) 
输出:运行时间太长,运行不出来,我哭。               

从课件中截了个图:
在这里插入图片描述

还可以选择多个模型进行测试比较。

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 1024 设计师:上身试试 返回首页