2019119_文本文件处理方式

这是在接单过程中得到的一个好东西,感觉这个包含了所有文本处理的问题和方式,主要通过分析文本进行转换,学到了好多东西,我觉得现在接单不算是只为了挣钱而是多练手,多掌握数据分析过程以及多任务处理,我需要的是平台而不是工作。

NLTK包的安装

什么是NLTK

一个完整的自然语言处理框架

  • 自带语料库,词性分类库
  • 自带分类,分词,等等功能
  • 有强大的社区支持
  • 框架设计上没有考虑中文

NLTK的主要模块

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xszk2FH9-1574612235177)(./nltkm.png)]

如何安装NLTK

Anaconda中已经默认安装

  • pip install nltk
  • nltk.download()
import nltk
nltk.download()
import nltk
import ssl

try:
    _create_unverified_https_context = ssl._create_unverified_context
except AttributeError:
    pass
else:
    ssl._create_default_https_context = _create_unverified_https_context
    
nltk.download()

NLTK替代包

NLTK的使用稍显复杂,初学者也可以使用各种基于NLTK的简化包

TextBlob

语料库的准备

什么是语料库

# 布朗语料库示例
from nltk.corpus import brown
brown.categories()
len(brown.sents())
brown.sents()[0]
len(brown.words())

常见的语料库格式

外部文件

除直接网络抓取并加工的情况外,原始文档由于内容较多,往往会首先以单个/多个文本文件的形式保存在外部,然后读入程序

list

结构灵活松散,有利于对原始语料进行处理,也可以随时增删成员

[

‘大鱼吃小鱼也吃虾米,小鱼吃虾米。’,

‘我帮你,你也帮我。’

]

list of list

语料完成分词后的常见形式,每个文档成为词条构成的list,而这些list又是原文档list的成员

[

[‘大鱼’, ‘吃’, ‘小鱼’, ‘也’, ‘吃’, ‘虾米’, ‘,’, ‘小鱼’, ‘吃’, ‘虾米’, ‘。’],

[‘我’, ‘帮’, ‘你’, ‘,’, ‘你’, ‘也’, ‘帮’, ‘我’, ‘。’]

]

DataFrame

使用词袋模型进行后续数据分析时常见格式,行/列代表语料index,相应的列/行代表词条,或者需要加以记录的文档属性,如作者,原始超链接,发表日期等
词条/文档对应时,单元格记录相应的词条出现频率,或者相应的概率/分值
Doc2Term矩阵
Term2Doc矩阵
可以和原始语料的外部文件/list配合使用

对于单个文档,也可以建立DataFrame,用行/列代表一个句子/段落/章节。

准备《射雕》语料库

为使用Python还不熟练的学员提供一个基于Pandas的通用操作框架。

读入为数据框

import pandas as pd
# 有的环境配置下read_table出错,因此改用read_csv
raw = pd.read_csv("金庸-射雕英雄传txt精校版.txt",
                  names = ['txt'], sep ='aaa', encoding ="utf-8" ,engine='python')
print(len(raw))
raw

加入章节标识

# 章节判断用变量预处理
def m_head(tmpstr):
    return tmpstr[:1]

def m_mid(tmpstr):
    return tmpstr.find("回 ")

raw['head'] = raw.txt.apply(m_head)
raw['mid'] = raw.txt.apply(m_mid)
raw['len'] = raw.txt.apply(len)
# raw['chap'] = 0
raw.head(50)
# 章节判断
chapnum = 0
for i in range(len(raw)):
    if raw['head'][i] == "第" and raw['mid'][i] > 0 and raw['len'][i] < 30 :
        chapnum += 1
    if chapnum >= 40 and raw['txt'][i] == "附录一:成吉思汗家族" :
        chapnum = 0
    raw.loc[i, 'chap'] = chapnum
    
# 删除临时变量
del raw['head']
del raw['mid']
del raw['len']
raw.head(50)

提取出所需章节

raw[raw.chap == 7]
tmpchap = raw[raw.chap == 7].copy()

##tmpchap = raw[raw.chap == 7].copy()
#firstIdx = tmpchap.index[0]
#firstIdx
tmpchap.reset_index(drop=True, inplace=True)

tmpchap['paraidx'] = tmpchap.index
tmpchap['paralen'] = tmpchap.txt.apply(len)
tmpchap[tmpchap.paralen == tmpchap.paralen.max()]
#tmpchap.head(10)
tmppara = tmpchap.txt[2]
tmppara

pandas官网

pandas中文

正则表达式

python正则表达

import re
tmppara = tmpchap[tmpchap['paraidx'] == 100].copy()
tmppara
#tmpstr = tmppara.txt[224]
#tmpstr
#sentences = re.findall('(.*?[?。!;:](’|”)?)',tmpstr)
#sentences
from matplotlib import pyplot as plt
%matplotlib inline
raw.txt.agg(len).plot.box()
rawgrp = raw.groupby('chap')
chapter = rawgrp.sum()##.agg(sum) # 只有字符串列的情况下,sum函数自动转为合并字符串
chapter = chapter[chapter.index != 0]
chapter.txt[2]

实战1:准备工具与素材

请自行完成分析用Anaconda环境的安装和配置。

请自行熟悉Jupyter notebook环境的操作。

自行提取《射雕》任意一回的文字,并完成如下操作:

  • 将其读入为按整句分案例行的数据框格式,并用另一个变量标识其所在段落的流水号。
  • 将上述数据框转换为以整段为成员的list格式。

说明:
最后一题主要涉及到Pandas的操作,对该模块不熟悉的学员可直接继续后续课程的学习,这部分知识的欠缺并不会影响对文本挖掘课程本身的学习。当然,能懂得相应的知识是最好的

分词

分词原理简介

分词的算法分类

  • 基于字符串的匹配

    • 即扫描字符串,如果发现字符串的子串和词相同,就算匹配。
    • 通常会加入一些启发式算法,比如“正向/反向最大匹配”,“长词优先”等
    • 优点是速度快,但对歧义和未登录词处理不好
  • 基于统计以及机器学习的分词方式

    • 基于人工标注的词性和统计特征进行建模,并通过模型计算分词概率
    • 常见的序列标注模型有HMM(隐马尔可夫)和CRF(条件随机场)
    • 这类分词算法能很好的处理歧义和未登录词问题,效果比前一类要好,但是需要大量的人工标注数据,分词速度也较慢

注意:分词算法本身在中文文本挖掘里就是一个“巨坑”

基于字符串匹配的分词算法原理

  • 以现有的词典为基础进行

  • 最大匹配法:以设定的最大词长度为框架,取出其中最长的匹配词

    • “中华人民共和国”会被完整取出,而不会进一步被分词
    • 最佳匹配法:按照词典中的频率高低,优先取高频词
  • 最大概率法:对句子整体进行分词,找到最佳的词汇排列组合规律

    • 例:早上好->早上/好
  • 最短路径分词:寻找单词数最少的分词方式

分词的难点

  • 分词歧义
    • 我个人没有意见
    • 三个人没有意见
  • 未登录词识别:蔡国庆
    • 数字
    • 实体名称/专业术语
    • 成语
    • 虚词、语气词

常见的分词工具

结巴分词的基本用法

jieba是目前应用最广,评价也较高的分词工具包

安装
https://pypi.python.org/pypi/jieba/

pip install jieba

基本特点
三种分词模式

精确模式,试图将句子最精确地切开,适合做文本分析
全模式,把句子中所有的可以成词的词语都扫描出来,速度非常快,但是不能解决歧义
搜索引擎模式,在精确模式的基础上,对长词再次切分,提高召回率,适合用于搜索引擎分词

支持繁体分词

支持自定义词典

import jieba

tmpstr = "郭靖和哀牢山三十六剑。"
res = jieba.cut(tmpstr) # 精确模式

print(res) # 是一个可迭代的 generator,可以使用 for 循环来遍历结果,本质上类似list
print(' '.join(res))
res = jieba.cut(tmpstr)
list(word for word in res) # 演示generator的用法
print(jieba.lcut(tmpstr)) # 结果直接输出为list
print('/'.join(jieba.cut(tmpstr, cut_all = True))) # 全模式
# 搜索引擎模式,还有jieba.lcut_for_search可用
print('/'.join(jieba.cut_for_search(tmpstr)))
#raw['new'] =jieba.lcut( raw.txt.)
raw.head()

修改词典

动态增删新词

在程序中可以动态根据分词的结果,对内存中的词库进行更新

add_word(word)

word:新词
freq=None:词频
tag=None:具体词性

del_word(word)

# 动态修改词典
jieba.add_word("哀牢山三十六剑")
'/'.join(jieba.cut(tmpstr))
jieba.del_word("哀牢山三十六剑")
'/'.join(jieba.cut(tmpstr))

使用自定义词典

load_userdict(file_name)

file_name:文件类对象或自定义词典的路径

词典基本格式

一个词占一行:词、词频(可省略)、词性(可省略),用空格隔开
词典文件必须为 UTF-8 编码
必要时可以使用Uedit32进行文件编码转换

云计算 5

李小福 2 nr

easy_install 3 eng

台中

dict = '金庸小说词库.txt'
jieba.load_userdict(dict) # dict为自定义词典的路径
'/'.join(jieba.cut(tmpstr))

使用搜狗细胞词库

https://pinyin.sogou.com/dict/

按照词库分类或者关键词搜索方式,查找并下载所需词库

使用转换工具,将其转换为txt格式

深蓝词库转换
奥创词库转换

在程序中导入相应词库

去除停用词

常见的停用词种类

超高频的常用词:基本不携带有效信息/歧义太多无分析价值

的、地、得

虚词:如介词,连词等

只、条、件
当、从、同

专业领域的高频词:基本不携带有效信息

视情况而定的停用词

呵呵
emoj

分词后去除停用词

基本步骤

读入停用词表文件
正常分词
在分词结果中去除停用词

新列表 = [ word for word in 源列表 if word not in 停用词列表 ]

该方法存在的问题:停用词必须要被分词过程正确拆分出来才行

newlist = [ w for w in jieba.cut(tmpstr) if w not in ['和', '。'] ] 
print(newlist)
import pandas as pd
tmpdf = pd.read_csv('停用词.txt',
                    names = ['w'], sep = 'aaa', encoding = 'utf-8')
tmpdf.head()


tmpchap = raw[raw.chap == 7].copy()

##tmpchap = raw[raw.chap == 7].copy()
#firstIdx = tmpchap.index[0]
#firstIdx
tmpchap.reset_index(drop=True, inplace=True)

tmpchap['paraidx'] = tmpchap.index
tmpchap['paralen'] = tmpchap.txt.apply(len)
paratxt = tmpchap[tmpchap.paralen == tmpchap.paralen.max()].txt
paratxt.reset_index(drop=True, inplace=True)
# 熟悉Python的可以直接使用 open('stopWord.txt').readlines() 获取停用词list,效率更高
[ w for w in jieba.cut(paratxt[0]) if w not in list(tmpdf.w) ] 

用extract_tags函数去除停用词

方法特点:

根据TF-IDF算法将特征词提取出来,在提取之前去掉停用词
可以人工指定停用词字典
jieba.analyse.set_stop_words()

# 使用预先准备的停用词表
import jieba.analyse as ana
ana.set_stop_words('停用词.txt')
jieba.lcut(tmpstr) # 读入的停用词列表对分词结果无效
ana.extract_tags(tmpstr, topK = 20) # 使用TF-IDF算法提取关键词,并同时去掉停用词

词性标注

import jieba.posseg

posseg.cut():给出附加词性的分词结果

词性标注采用和 ICTCLAS 兼容的标记法

后续可基于词性做进一步处理,如只提取出名词,动词等

import jieba.posseg as psg

tmpres = psg.cut(tmpstr) # 附加词性的分词结果
print(tmpres)

for item in tmpres:
    print(item.word, item.flag)
psg.lcut(tmpstr) # 直接输出为list,成员为pair

分词的NLTK实现

NLTK只能识别用空格作为词条分割方式,因此不能直接用于中文文本的分词。

一般的做法是先用jieba分词,然后转换为空格分隔的连续文本,再转入NLTK框架使用。

rawtext = '周伯通笑道:“你懂了吗?…”
txt = ’ '.join(jieba.cut(rawtext)) # “周伯通 笑 道 :…”
toke = nltk.word_tokenize(txt) # [‘周伯通’, ‘笑’, ‘道’, ‘:’…]

实战2:《射雕》一书分词

选取第一回的文字,应用搜狗的细胞词库和停用词表,清理出干净的分词结果。

选取第一回中最长的1个段落,比较不使用词库、不使用停用词表前后的分词结果。

熟悉搜狗细胞词库网站中的资源,思考哪些词库可能是自己需要的,下载相应的资源并进行格式转换。


tmpchap = raw[raw.chap == 1].copy()

##tmpchap = raw[raw.chap == 1].copy()
#firstIdx = tmpchap.index[0]
#firstIdx
tmpchap.reset_index(drop=True, inplace=True)

tmpchap['paraidx'] = tmpchap.index
tmpchap['paralen'] = tmpchap.txt.apply(len)
paratxt = tmpchap[tmpchap.paralen == tmpchap.paralen.max()].txt
paratxt.reset_index(drop=True, inplace=True)
# 熟悉Python的可以直接使用 open('stopWord.txt').readlines() 获取停用词list,效率更高
[ w for w in jieba.cut(paratxt[0]) if w not in list(tmpdf.w) ] 

词云展示

词频统计

绝大部分词频统计工具都是基于分词后构建词条的list进行,因此首先需要完成相应的分词工作。

import pandas as pd
# 载入语料
raw = pd.read_csv("金庸-射雕英雄传txt精校版.txt",
                  names = ['txt'], sep ='aaa', encoding ="utf-8" ,engine='python')

# 章节判断用变量预处理
def m_head(tmpstr):
    return tmpstr[:1]

def m_mid(tmpstr):
    return tmpstr.find("回 ")

raw['head'] = raw.txt.apply(m_head)
raw['mid'] = raw.txt.apply(m_mid)
raw['len'] = raw.txt.apply(len)
# 章节判断
chapnum = 0
for i in range(len(raw)):
    if raw['head'][i] == "第" and raw['mid'][i] > 0 and raw['len'][i] < 30 :
        chapnum += 1
    if chapnum >= 40 and raw['txt'][i] == "附录一:成吉思汗家族" :
        chapnum = 0
    raw.loc[i, 'chap'] = chapnum
    
# 删除临时变量
del raw['head']
del raw['mid']
del raw['len']

#取出特定的某一回
chapidx = 1
raw[raw.chap == chapidx]
tmpchap = raw[raw.chap == chapidx].copy()

##tmpchap = raw[raw.chap == 7].copy()
#firstIdx = tmpchap.index[0]
#firstIdx
tmpchap.reset_index(drop=True, inplace=True)

tmpchap['paraidx'] = tmpchap.index
tmpchap['paralen'] = tmpchap.txt.apply(len)
#tmpchap[tmpchap.paralen == tmpchap.paralen.max()]
alltxt = "".join(tmpchap.txt[1:])
alltxt
import jieba

dict = '金庸小说词库.txt'
jieba.load_userdict(dict) # dict为自定义词典的路径
tmpdf = pd.read_csv('停用词.txt',
                    names = ['w'], sep = 'aaa', encoding = 'utf-8',engine='python')
#分词
#word_list = jieba.lcut(chapter.txt[1])
#word_list[:10]
word_list = [ w for w in jieba.cut(alltxt) if w not in list(tmpdf.w) ]
Building prefix dict from the default dictionary ...
Loading model from cache C:\Users\CHENYA~1\AppData\Local\Temp\jieba.cache
Loading model cost 0.866 seconds.
Prefix dict has been built succesfully.



---------------------------------------------------------------------------

FileNotFoundError                         Traceback (most recent call last)

<ipython-input-1-01dab9076eb7> in <module>
      2 
      3 dict = '金庸小说词库.txt'
----> 4 jieba.load_userdict(dict) # dict为自定义词典的路径
      5 tmpdf = pd.read_csv('停用词.txt',
      6                     names = ['w'], sep = 'aaa', encoding = 'utf-8',engine='python')


E:\Anaconda3_2019.07\lib\site-packages\jieba\__init__.py in load_userdict(self, f)
    372         if isinstance(f, string_types):
    373             f_name = f
--> 374             f = open(f, 'rb')
    375         else:
    376             f_name = resolve_filename(f)


FileNotFoundError: [Errno 2] No such file or directory: '金庸小说词库.txt'

构建完list之后,也可以自行编写词频统计程序,框架如下:

遍历整个list,对每个词条:

if word in d:

d[word] += 1

else:

d[word] = 1

使用Pandas统计

df = pd.DataFrame(word_list, columns = ['word'])
result = df.groupby(['word']).size()
print(type(result))
freqlist = result.sort_values(ascending=False)
freqlist
freqlist[freqlist.index == '道']
freqlist[freqlist.index == '道'][0]
freqlist[freqlist.index == '黄蓉道']

使用NLTK统计

NLTK生成的结果为频数字典,在和某些程序包对接时比较有用

import nltk

fdist = nltk.FreqDist(word_list) # 生成完整的词条频数字典
fdist

# 带上某个单词, 可以看到它在整个文章中出现的次数
fdist['颜烈']
fdist.keys() # 列出词条列表
fdist.tabulate(10)
fdist.most_common(5)

词云概述¶

wordcloud包的安装

安装

docker的kaggle下推荐安装方法:

conda install -c conda-forge wordcloud

常规安装方法(docker或windows下):

pip install wordcloud

警告:常规方法安装wordcloud有可能非常顺利,也有可能会出各种问题

中文字体支持

.WordCloud(font_path=‘msyh.ttf’)

需要带路径写完整字体文件名
注意Win10的字体文件后缀可能不一样

绘制词云

WordCloud的基本语法

class wordcloud.WordCloud(

font_path,

width,

height,

max_words,

stopwords,

min_font_size,

font_step,

relative_scaling,

prefer_horizontal,

background_color,

mode,

color_func,

mask

)

常用功能:

font_path : 在图形中使用的字体,默认使用系统字体 
width / height = 200 : 图形的宽度/高度
max_words = 200 : 需要绘制的最多词条数
stopwords = None : 停用词列表,不指定时会使用系统默认停用词列表

字体设定:

min_font_size = 4 /  max_font_size = None : 字符大小范围
font_step = 1 : 字号增加的步长
relative_scaling = .5: 词条频数比例和字号大小比例的换算关系,默认为50%
prefer_horizontal = 0.90 : 图中词条水平显示的比例

颜色设定:

background_color = ”black” : 图形背景色
mode = ”RGB”: 图形颜色编码,如果指定为"RGBA"且背景色为None时,背景色为透明
color_func = None : 生成新颜色的函数,使用matplotlib的colormap

背景掩模:

mask = None : 词云使用的背景图(遮罩)

用原始文本直接分词并绘制

cloudobj = WordCloud().generate(text)

generate实际上是generate_from_text的别名

文本需要用空格/标点符号分隔单词,否则不能正确分词

import wordcloud
myfont = 'msyh.ttf'
text = 'this is shanghai, 郭靖, 和, 哀牢山 三十六剑'
cloudobj = wordcloud.WordCloud(font_path = myfont).generate(text)  
print(cloudobj)

显示词云

import matplotlib.pyplot as plt

plt.imshow(cloudobj)

plt.axis(“off”)

plt.show()

import matplotlib.pyplot as plt
plt.imshow(cloudobj)
plt.axis("off")
plt.show()
# 更改词云参数设定
cloudobj = wordcloud.WordCloud(font_path = myfont, 
    width = 360, height = 180,
    mode = "RGBA", background_color = None).generate(text)  

plt.imshow(cloudobj)
plt.axis("off")
plt.show()

保存词云

wordcloud.to_file(保存文件的路径与名称) 该命令保存的是高精度图形

cloudobj.to_file("词云.png")

生成射雕第一章的词云

cloudobj = wordcloud.WordCloud(font_path = myfont, 
    width = 1200, height = 800,
    mode = "RGBA", background_color = None).generate(' '.join(word_list)) 

plt.imshow(cloudobj)
plt.axis("off")
plt.show()
cloudobj.to_file("词云2.png")

基于分词频数绘制

generate()的实际操作

调用分词函数process_text()
调用基于频数的绘制函数fit_words()

fit_words(dict)

实际上是generate_from_frequencies的别名
Dict: 由词条和频数构成的字典
#基于分词频数绘制词云
txt_freq = {'张三':100,'李四':90,'王二麻子':50}
cloudobj = wordcloud.WordCloud(font_path = myfont).fit_words(txt_freq)

plt.imshow(cloudobj)
plt.axis("off")
plt.show() 

用频数生成射雕第一章的词云

cloudobj = wordcloud.WordCloud(font_path = myfont).fit_words(fdist)

plt.imshow(cloudobj)
plt.axis("off")
plt.show() 

词云的美化

各种详细的操作和设定可以参考官网的案例:

https://amueller.github.io/word_cloud/

设置背景图片

Mask / 掩模 / 遮罩

用于控制词云的整体形状
指定mask后,设置的宽高值将被忽略,遮罩形状被指定图形的形状取代。除全白的部分仍然保留外,其余部分会用于绘制词云。因此背景图片的画布一定要设置为白色(#FFFFFF)
字的大小,布局和颜色也会基于Mask生成
必要时需要调整颜色以增强可视效果

基本调用方式

from scipy.misc import imread
mask = imread(背景图片名称)
from scipy.misc import imread


cloudobj = wordcloud.WordCloud(font_path = myfont, 
    mask = imread("射雕背景1.png"), 
    mode = "RGBA", background_color = None
    ).generate(' '.join(word_list)) 

plt.imshow(cloudobj)
plt.axis("off")
plt.show() 

指定图片色系

读取指定图片的色系设定

imgarray = np.array(imread(imgfilepath))

获取图片颜色

bimgColors = wordcloud.ImageColorGenerator(imgarray)

重置词云颜色

cloudobj.recolor(color_func=bimgColors)
# 利用已有词云对象直接重绘颜色,输出速度要比全部重绘快的多
import numpy as np

imgobj = imread("射雕背景2.png")
image_colors = wordcloud.ImageColorGenerator(np.array(imgobj))
cloudobj.recolor(color_func=image_colors)

plt.imshow(cloudobj)
plt.axis("off")
plt.show()

指定单词组颜色

理想的状况应该是分组比较词频,在两组中都高频的词条在图形中相互抵消。

Python目前只能实现词条分组上色。

color_to_words = {

‘#00ff00’: [‘颜烈’, ‘武官’, ‘金兵’, ‘小人’],

‘red’: [‘包惜弱’, ‘郭啸天’, ‘杨铁心’, ‘丘处机’]

} '#00ff00’为绿色的代码

default_color = ‘grey’ # 其余单词的默认颜色

cloudobj.recolor()

# 官网提供的颜色分组类代码,略有修改
from wordcloud import get_single_color_func

class GroupedColorFunc(object):

    def __init__(self, color_to_words, default_color):
        self.color_func_to_words = [
            (get_single_color_func(color), set(words))
            for (color, words) in color_to_words.items()]

        self.default_color_func = get_single_color_func(default_color)

    def get_color_func(self, word):
        """Returns a single_color_func associated with the word"""
        try:
            color_func = next(
                color_func for (color_func, words) in self.color_func_to_words
                if word in words)
        except StopIteration:
            color_func = self.default_color_func

        return color_func

    def __call__(self, word, **kwargs):
        return self.get_color_func(word)(word, **kwargs)

######

# 指定分组色系
color_to_words = {
    '#00ff00': ['颜烈', '武官', '金兵', '官兵'],
    'red': ['包惜弱', '郭啸天', '杨铁心', '丘处机']
}

default_color = 'grey' # 指定其他词条的颜色

grouped_color_func = GroupedColorFunc(color_to_words, default_color)

cloudobj.recolor(color_func=grouped_color_func)

plt.imshow(cloudobj)
plt.axis("off")
plt.show()

实战3:优化射雕词云

尝试进一步清理分词结果,并且只保留所有的名称(人名、地名)。

提示:可以使用词性标注功能,只保留名词和未知词性的词。
     可以考虑对自定义词典做优化,通过强行调整权重等方法改善分词效果。

将所有的人名按照蓝色系,地名按照红色系进行词云绘制。

自行制作两个纯色图片,分别为绿色和蓝色,然后将其分别指定为绘图所用的色系,观察图形效果。

尝试使用不同的背景图片作为掩模,思考怎样的图片才能使得绘图效果最佳。

文档信息的向量化

所谓文档信息的向量化,就是将文档信息***数值化*** ,从而便于进行建模分析。

词袋模型(One-hot表示方式)

  • 几乎是最早的用于提取文本特征的方法
  • 将文本直接简化为一系列词的集合
    • 不考虑其语法和词序关系,每个词都是独立的
  • 举例:
    1. 对语料进行清理,并完成分词
    • 大鱼/吃/小鱼/也/吃/虾米,小鱼吃虾米。
    1. 对每个词进行编号,形成字典(顺序无关的流水号即可)
    • {“大鱼”:1,“吃”:2,“小鱼”:3,“也”:4,“虾米”:5}
    1. 用0/1代表该词是否在文本中出现,从而将文本记录为一个特征向量
    • 大鱼吃小鱼也吃虾米 ->[大鱼,吃,小鱼,也,虾米]->[1,2,1,1,1]
    • 小鱼吃虾米 ->[小鱼,吃,虾米]->[0,1,1,0,1]
  • 该方式也被称为词袋模型,Bag of Words,BOW
    • 词和文本的关联就相当于文本是一个袋子,词只是直接装在袋子里
  • 显然,词袋模型是比较简单的模型,对文本中的信息有较多丢失,但已经可以解决很多实际问题
    • 词袋模型的提出最初是为了解决文档分类问题,目前主要应用在NLP(Natural Language Process),IR(Information Retrival),CV(Computer Vision)等领域
  • 也可以不考虑词频,减少模型复杂度
    • 词集模型:Set Of Words,单词构成的集合,常见于短文本分析
    • 大鱼吃小鱼也吃虾米 ->[大鱼,吃,小鱼,也,虾米]->[1,1,1,1,1]
  • 优点:
    • 解决了分类器不好处理离散数据的问题
    • 在一定程度上也起到了扩充特征的作用
  • 缺点:
    • 不考虑词与词之间的顺序
    • 它假设词与词之间相互独立(在大多数情况下,词与词是相互有关联的)
      • 老公 vs 老婆,老婆 vs 孩子他妈
    • 它得到的特征是离散稀疏的(维度灾难)
      • 每个词都是茫茫"0"海中的一个1:[0 0 0 0 0 1 0 0 0 0 0 0 …]

词袋模型的gensim实现

gensim的安装

pip install gensim

或者

conda install gensim

安装完成后如果使用word2vec时报错,建议去gensim官网下载MS windows install的exe程序进行安装:

https://pypi.python.org/pypi/gensim

建立字典

Dictionary类用于建立word<->id映射关系,把所有单词取一个set(),并对set中每个单词分配一个Id号的map

class gensim.corpora.dictionary.Dictionary(

documents=None : 若干个被拆成单词集合的文档的集合,一般以list in list形式出现
prune_at=2000000 : 字典中的最大词条容量
)

from gensim.corpora import Dictionary

texts = [['human', 'interface', 'computer']]
dct = Dictionary(texts)  # fit dictionary
dct.num_nnz

Dictionary类的属性

token2id

dict of (str, int) – token -> tokenId.

id2token

dict of (int, str) – Reverse mapping for token2id, initialized in lazy manner to > save memory.

dfs

dict of (int, int) – Document frequencies: token_id -> in how many documents > > > contain this token.

num_docs

int – Number of documents processed.

num_pos

int – Total number of corpus positions (number of processed words).

num_nnz

int – Total number of non-zeroes in the BOW matrix.

# 向字典增加词条
dct.add_documents([["cat", "say", "meow"], ["dog"]])  
dct.token2id

转换为BOW稀疏向量

dct.doc2bow( # 转换为BOW格式:list of (token_id, token_count)

document : 用于转换的词条list
allow_update = False : 是否直接更新所用字典
return_missing = False : 是否返回新出现的(不在字典中的)词
)

输出结果

[(0, 2), (1, 2)],表明在文档中id为0,1的词汇各出现了2次,至于其他词汇则没有出现
return_missing = True时,输出list of (int, int), dict of (str, int)

dct.doc2bow(["this", "is", "cat", "not", "a", "dog"])
dct.doc2bow(["this", "is", "cat", "not", "a", "dog"], return_missing = True)

转换为BOW长向量
可考虑的思路:

从稀疏格式自行转换。
直接生成文档-词条矩阵。

doc2idx( # 转换为list of token_id

document : 用于转换的词条list
unknown_word_index = -1 : 为不在字典中的词条准备的代码

输出结果

按照输入list的顺序列出所出现的各词条ID
dct.doc2idx(["this", "is", "a", "dog", "not", "cat"])

生成文档-词条矩阵

用Pandas库实现

基本程序框架:

原始文档分词并清理
拼接为同一个dataframe
汇总并转换为文档-词条矩阵格式
去除低频词
import pandas as pd
# 载入语料
raw = pd.read_csv("金庸-射雕英雄传txt精校版.txt",
                  names = ['txt'], sep ='aaa', encoding ="utf-8" ,engine='python')

# 章节判断用变量预处理
def m_head(tmpstr):
    return tmpstr[:1]

def m_mid(tmpstr):
    return tmpstr.find("回 ")

raw['head'] = raw.txt.apply(m_head)
raw['mid'] = raw.txt.apply(m_mid)
raw['len'] = raw.txt.apply(len)
# 章节判断
chapnum = 0
for i in range(len(raw)):
    if raw['head'][i] == "第" and raw['mid'][i] > 0 and raw['len'][i] < 30 :
        chapnum += 1
    if chapnum >= 40 and raw['txt'][i] == "附录一:成吉思汗家族" :
        chapnum = 0
    raw.loc[i, 'chap'] = chapnum
    
# 删除临时变量
del raw['head']
del raw['mid']
del raw['len']

rawgrp = raw.groupby('chap')
chapter = rawgrp.agg(sum) # 只有字符串列的情况下,sum函数自动转为合并字符串
chapter = chapter[chapter.index != 0]
chapter.head()
# 设定分词及清理停用词函数
# 熟悉Python的可以使用 open('stopWord.txt').readlines() 获取停用词list,效率更高
stoplist = list(pd.read_csv('停用词.txt', names = ['w'], sep = 'aaa', 
                            encoding = 'utf-8', engine='python').w)
import jieba 
def m_cut(intxt):
    return [ w for w in jieba.cut(intxt) 
            if w not in stoplist and len(w) > 1 ] 

# 设定数据框转换函数
def m_appdf(chapnum):
    tmpdf = pd.DataFrame(m_cut(chapter.txt[chapnum + 1]), columns = ['word'])
    tmpdf['chap'] = chapter.index[chapnum] # 也可以直接 = chapnum + 1
    return tmpdf
# 全部读入并转换为数据框

df0 = pd.DataFrame(columns = ['word', 'chap']) # 初始化结果数据框

for chapidx in range(len(chapter)):
    df0 = df0.append(m_appdf(chapidx))
df0.tail(50)
# 输出为序列格式
df0.groupby(['word', 'chap']).agg('size').head()
# 直接输出为数据框
t2d = pd.crosstab(df0.word, df0.chap)
len(t2d)
t2d.head(100)
# 计算各词条的总出现频次,准备进行低频词删减,axis=1表示按行统计
totnum = t2d.agg(func = 'sum', axis=1)
totnum
#按行选取
t2dclean = t2d.iloc[list(totnum >= 10),:]
#求转置
t2dclean.T

用sklearn库实现

CountVectorizer类的基本用法

文本信息在向量化之前很难直接纳入建模分析,考虑到这一问题,专门用于数据挖掘的sklearn库提供了一个从文本信息到数据挖掘模型之间的桥梁,即CountVectorizer类,通过这一类中的功能,可以很容易地实现文档信息的向量化。

class sklearn.feature_extraction.text.CountVectorizer(

input = ‘content’ : {‘filename’, ‘file’, ‘content’}

#filename为所需读入的文件列表, file则为具体的文件名称。

encoding=‘utf-8’ #文档编码

stop_words = None #停用词列表,当analyzer == 'word’时才生效

min_df / max_df : float in range [0.0, 1.0] or int, default = 1 / 1.0

#词频绝对值/比例的阈值,在此范围之外的将被剔除
#小数格式说明提供的是百分比,如0.05指的就是5%的阈值

CountVectorizer.build_analyzer()

#返回文本预处理和分词的可调用函数

from sklearn.feature_extraction.text import CountVectorizer
countvec = CountVectorizer(min_df = 2) # 在两个以上文档中出现的才保留

analyze = countvec.build_analyzer()
analyze('郭靖 和 哀牢山 三十六 剑 。')
countvec.fit(['郭靖 和 黄蓉 哀牢山 三十六 剑 。', '黄蓉 和 郭靖 郭靖'])
countvec.get_feature_names() # 词汇列表,实际上就是获取每个列对应的词条
countvec.vocabulary_ # 词条字典
x = countvec.transform(['郭靖 和 黄蓉 哀牢山 三十六 剑 。', '黄蓉 和 郭靖 郭靖'])
x
x.todense() # 将稀疏矩阵直接转换为标准格式矩阵
countvec.fit_transform(['郭靖 和 哀牢山 三十六 剑 。', '黄蓉 和 郭靖 郭靖']) # 一次搞定

使用sklearn生成射雕的章节d2m矩阵

将章节文档数据框处理为空格分隔词条的文本格式

使用fit_transform函数生成bow稀疏矩阵

转换为标准格式的d2m矩阵

rawchap = [ " ".join(m_cut(w)) for w in chapter.txt.iloc[:5]] 
rawchap[0]
from sklearn.feature_extraction.text import CountVectorizer
countvec = CountVectorizer(min_df = 5) # 在5个以上章节中出现的才保留

res = countvec.fit_transform(rawchap)
res
res.todense()
countvec.get_feature_names()

从词袋模型到Bi-gram

  • 词袋模型完全无法利用语序信息

    • 我帮你 vs 你帮我

    • P(我帮你) = P(我)*P(帮)*P(你)

  • Bi-gram:进一步保留顺序信息,两个词一起看

    • P(我帮你) = P(我)*P(帮|我)*P(你|帮)

    • {“我帮”:1, “帮你”:2,“你帮”:3,“帮我”:4}

    • 我帮你 -> [1,1,0,0]

    • 你帮我 -> [0,0,1,1]

  • 显然,Bi-gram可以保留更多的文本有效信息。

从Bi-gram到N-gram

  • 考虑更多的前后词

    • 可以直接扩展至tri-gram,4-gram直至N-gram
  • 优点:考虑了词的顺序,信息量更充分

    • 长度达到5之后,效果有明显提升
  • 缺点:

    • 词表迅速膨胀,数据出现大量的稀疏化问题

    • 没增加一个词,模型参数增加40万倍

离散表示方式所面临的问题总结

  • 无法衡量词向量之间的关系

    • 老公 [0,1,0,0,0,0,0,0,0,0]

    • 丈夫 [0,0,0,0,1,0,0,0,0,0]

    • 当家的 [0,0,0,0,0,0,0,1,0,0]

    • 挨千刀的 [0,0,0,0,0,0,0,0,1,0]

    • 各种度量(与或费、距离)都不合适,只能靠字典进行补充

  • 词表维度随着语料库增长膨胀

  • N-gram词序列随语料库膨胀更快

  • 数据稀疏问题(导致分析性能成为严重瓶颈)

文档信息的分布式表示

分布式表示(Distributed representation)

  • 如何定位围棋棋盘上的落子位置?

    • 方法一:每个点单独记忆,共361个记忆单元

    • 方法二:行坐标+列坐标,共19+19=38个记忆单元

  • 将分布式表示用于NLP

    • 不直接考虑词与词在原文中的相对位置、距离、语法结构等,先把每个词看作一个单独向量

    • 根据一个词在上下文中的临近词的含义,应当可以归纳出词本身的含义

    • 单个词的词向量不足以表示整个文本,能表示的仅仅只是这个词本身

    • 事先决定用多少维度的向量来表示这个词条

      • 维度以50维和100维比较常见

      • 向量中每个维度的取值由模型训练决定,且不再是唯一的

        • [0.762, 0.107, 0.307, -0.199, 0.521,…]
    • 所有的词都在同一个高维空间中构成不同的向量

      • 从而词与词之间的关系就可以用空间中的距离来加以表述
    • 所有训练方法都是在训练语言模型的同时,顺便得到词向量的

      • 语言模型其实就是看一句话是不是正常人说出来的,具体表现为词条先后出现的顺序和距离所对应的概率是否最大化

共现矩阵 (Cocurrence matrix)

  • 例如:语料库如下:

    • I like deep learning.

    • I like NLP.

    • I enjoy flying.

  • 确定取词长度:

    • 取词长度为1的结果
  • 窗口长度越长,则信息量越丰富,但数据量也越大

    • 一般设为5–10
  • 共现矩阵的行/列数值自然就表示出各个词汇的相似度

    • 从而可以用作分析向量

则共现矩阵表示如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9xGo4dMr-1574612235181)(./concurrence_matrix.jpg)]

例如:“I like”出现在第1,2句话中,一共出现2次,所以=2。
对称的窗口指的是,“like I”也是2次

将共现矩阵行(列)作为词向量表示后,可以知道like,enjoy都是在I附近且统计数目大约相等,他们意思相近

仍然存在的问题

  • 如果将共现矩阵(列)直接作为词向量

    • 向量维数随着词典大小线性增长

    • 存储整个词典的空间消耗非常大

    • 一些模型如文本分类模型会面临稀疏性问题

    • 高度的稀疏性导致模型会欠稳定

实战4:生成词向量

尝试编制以下程序:

以段为单位依次读入射雕第一回的内容。
为每一段分别生成bow稀疏向量。
生成稀疏向量的同时动态更新字典。

请自行编制bow稀疏向量和标准长向量互相转换的程序。

在文档词条矩阵中可以看到许多类似“黄蓉道”、“黄蓉说”之类的词条,请思考对此有哪些处理办法。

关键词提取

用途

  • 用核心信息代表原始文档

  • 在文本聚类、分类、自动摘要等领域中有着重要应用

需求:针对一篇文章,在不加人工干预的情况下提取关键词

当然,首先要进行分词

关键词分配: 事先给定关键词库,然后在文档中进行关键词检索

关键词提取:根据某种规则,从文档中抽取最重要的词作为关键词

  • 有监督:抽取出候选词并标记是否为关键词,然后训练相应的模型

  • 无监督:给词条打分,并基于最高分值抽取

无监督方式的分析思路——基于词频

分析思路1:按照词频高低进行提取

  • 大量的高频词并无多少意义(停用词)

  • 即使出现频率相同,常见词价值也明显低于不常见词

分析思路2:按照词条在文档中的重要性进行提取

  • 如何确定词条在该文档中的重要性?

常见的方法:TF-IDF、网络图

TF-IDF算法

信息检索(IR)中最常用的一种文本关键信息表示法

基本思想:

  • 如果某个词在一篇文档中出现频率较高,并且在语料库中其他文本中出现频率较低,甚至不出现,则认为这个词具有很好的类别区分能力

词频TF:Term Frequency,衡量一个词在文档中出现的频率

  • 平均而言出现越频繁的词,其重要性可能就越高

考虑到文章长度的差异,需要对词频做标准化

  • TF(w) = (w出现在文档中的次数)/(文档中的词的总数)

  • TF(w) = (w出现在文档中的次数)/(文档中出现最多的词的次数)

逆文档频率IDF:Inverse Document Frequency,用于模拟在该语料库中,某一个词有多重要

  • 有些词到处出现,但是明显是没有用的。比如各种停用词,过渡句用词等。

  • 因此把罕见的词的重要性(weight)调高,把常见词的重要性调低

IDF的具体算法

  • IDF(w) = log(语料库中的文档总数/(含有该w的文档总数+1))

TF-IDF = TF * IDF

  • TF-IDF与一个词在文档中的出现次数成正比

  • 与该词在整个语料中的出现次数成反比

优点

  • 简单快速

  • 结果也比较符合实际情况

缺点

  • 单纯以“词频”横量一个词的重要性,不够全面,有时重要的词可能出现的次数并不多

  • 无法考虑词与词之间的相互关系

  • 这种算法无法体现词的位置信息,出现位置靠前的词与出现位置靠后的词,都被视为重要性相同,这是不正确的

    • 一种解决方式是,对全文的第一段和每一段的第一句话,给予较大的权重

TF-IDF的具体实现

jieba, NLTK, sklearn, gensim等程序包都可以实现TF-IDF的计算。除算法细节上会有差异外,更多的是数据输入/输出格式上的不同。

jieba

输出结果会自动按照TF-IDF值降序排列,并且直接给出的是词条而不是字典ID,便于阅读使用。

可在计算TF-IDF时直接完成分词,并使用停用词表和自定义词库,非常方便。

有默认的IDF语料库,可以不训练模型,直接进行计算。

以单个文本为单位进行分析。

jieba.analyse.extract_tags(

sentence 为待提取的文本

topK = 20 : 返回几个 TF/IDF 权重最大的关键词

withWeight = False : 是否一并返回关键词权重值

allowPOS = () : 仅包括指定词性的词,默认值为空,即不筛选

)

jieba.analyse.set_idf_path(file_name)

关键词提取时使用自定义逆向文件频率(IDF)语料库 

劳动防护 13.900677652

生化学 13.900677652

奥萨贝尔 13.900677652

奧薩貝爾 13.900677652

考察队员 13.900677652

jieba.analyse.set_stop_words(file_name)

关键词提取时使用自定义停止词(Stop Words)语料库 

jieba.analyse.TFIDF(idf_path = None)

新建 TFIDF模型实例
idf_path : 读取已有的TFIDF频率文件(即已有模型)
使用该实例提取关键词:TFIDF实例.extract_tags()
import pandas as pd
# 载入语料
raw = pd.read_csv("金庸-射雕英雄传txt精校版.txt",
                  names = ['txt'], sep ='aaa', encoding ="utf-8" ,engine='python')

# 章节判断用变量预处理
def m_head(tmpstr):
    return tmpstr[:1]

def m_mid(tmpstr):
    return tmpstr.find("回 ")

raw['head'] = raw.txt.apply(m_head)
raw['mid'] = raw.txt.apply(m_mid)
raw['len'] = raw.txt.apply(len)
# 章节判断
chapnum = 0
for i in range(len(raw)):
    if raw['head'][i] == "第" and raw['mid'][i] > 0 and raw['len'][i] < 30 :
        chapnum += 1
    if chapnum >= 40 and raw['txt'][i] == "附录一:成吉思汗家族" :
        chapnum = 0
    raw.loc[i, 'chap'] = chapnum
    
# 删除临时变量
del raw['head']
del raw['mid']
del raw['len']

rawgrp = raw.groupby('chap')
chapter = rawgrp.agg(sum) # 只有字符串列的情况下,sum函数自动转为合并字符串
#chapter = chapter[chapter.index != 0]

chapter
txt
chap
0.0 全本全集精校小说尽在:http://www.yimuhe.com/u/anglewing26...
1.0 第一回 风雪惊变钱塘江浩浩江水,日日夜夜无穷无休的从两浙西路临安府牛家村边绕过,东流入海。江...
2.0 第二回 江南七怪颜烈跨出房门,过道中一个中年士人拖着鞋皮,踢跶踢跶的直响,一路打着哈欠迎面过...
3.0 第三回 黄沙莽莽寺里僧众见焦木圆寂,尽皆悲哭。有的便为伤者包扎伤处,抬入客舍。忽听得巨钟下的...
4.0 第四回 黑风双煞完颜洪熙笑道:“好,再打他个痛快。”蒙古兵前哨报来:“王罕亲自前来迎接大金国...
5.0 第五回 弯弓射雕一行人下得山来,走不多时,忽听前面猛兽大吼声一阵阵传来。韩宝驹一提缰,胯下黄...
6.0 第六回 崖顶疑阵午饭以后,郭靖来到师父帐中。全金发道:“靖儿,我试试你的开山掌练得怎样了。”...
7.0 第七回 比武招亲江南六怪与郭靖晓行夜宿,向东南进发,在路非止一日,过了大漠草原。这天离张家口...
8.0 第八回 各显神通王处一脚步好快,不多时便带同郭靖到了城外,再行数里,到了一个山峰背后。他不住...
9.0 第九回 铁枪破犁郭黄二人来到赵王府后院,越墙而进,黄蓉柔声道:“你轻身功夫好得很啊!”郭靖伏...
10.0 第十回 往事如烟完颜康陡然见到杨铁心,惊诧之下,便即认出,大叫:“啊,是你!”提起铁枪,“行...
11.0 第十一回 长春服输沙通天见师弟危殆,跃起急格,挡开了梅超风这一抓,两人手腕相交,都感臂酸心惊...
12.0 第十二回 亢龙有悔黄蓉正要将鸡撕开,身后忽然有人说道:“撕作三份,鸡屁股给我。”两人都吃了一...
13.0 第十三回 五湖废人黄蓉回到客店安睡,自觉做了一件好事,大为得意,一宵甜睡,次晨对郭靖说了。郭...
14.0 第十四回 桃花岛主五男一女,走进厅来,却是江南六怪。他们自北南来,离故乡日近,这天经过太湖,...
15.0 第十五回 神龙摆尾陆冠英扶起完颜康,见他给点中了穴道,动弹不得,只两颗眼珠光溜溜地转动。陆乘...
16.0 第十六回 《九阴真经》郭黄二人自程府出来,累了半夜,正想回客店安歇,忽听马蹄声响,一骑马自南...
17.0 第十七回 双手互搏周伯通道:“你道是我师哥死后显灵?还是还魂复生?都不是,他是假死。”郭靖“...
18.0 第十八回 三道试题郭靖循着蛇声走去,走出数十步,月光下果见数千条青蛇排成长队蜿蜒而前。十多名...
19.0 第十九回 洪涛群鲨洪七公万想不到这场背书比赛竟会如此收场,较之郭靖将欧阳克连摔十七八个筋斗都...
20.0 第二十回 九阴假经洪七公与郭靖见欧阳锋叔侄领周伯通走入后舱,径行到前舱换衣。四名白衣少女过来...
21.0 第二十一回 千钧巨岩欧阳锋只感身上炙热,脚下船板震动甚剧,知道这截船身转眼就要沉没,但洪七公...
22.0 第二十二回 骑鲨遨游黄蓉见欧阳锋拖泥带水地将侄儿抱上岸来,他向来阴鸷的脸上竟也笑逐颜开,可是...
23.0 第二十三回 大闹禁宫黄药师满腔悲愤,指天骂地,咒鬼斥神,痛责命运对他不公,命舟子将船驶往大陆...
24.0 第二十四回 密室疗伤黄蓉向外走了两步,回过头来,见郭靖眼光中露出怀疑神色,料想是自己脸上的杀...
25.0 第二十五回 荒村野店黄药师仰天一笑,说道:“冠英和这位姑娘留着。”陆冠英早知是祖师爷到了,但...
26.0 第二十六回 新盟旧约黄药师心想不明不白地跟全真七子大战一场,更不明不白地结下了深仇,真是好没...
27.0 第二十七回 轩辕台前两人正闹间,楼梯声响,适才随杨康下去的丐帮三长老又回了上来,走到郭黄二人...
28.0 第二十八回 铁掌峰顶此时鲁有脚已经醒转,四长老聚在一起商议。鲁有脚道:“现下真相未明,咱们须...
29.0 第二十九回 黑沼隐女郭靖在雕背连声呼叫,召唤小红马在地下跟来。转眼之间,双雕已飞出老远。雌雄...
30.0 第三十回 一灯大师两人顺着山路向前走去,行不多时,山路就到了尽头,前面是条宽约尺许的石梁,横...
31.0 第三十一回 鸳鸯锦帕一灯大师低低叹了口气道:“其实真正的祸根,还在我自己。我乃大理国小君,虽...
32.0 第三十二回 湍江险滩穆念慈右手让黄蓉握着,望着水面的落花,说道:“我见他杀了欧阳克,只道他从...
33.0 第三十三回 来日大难郭靖与黄蓉此刻心意欢畅,原不想理会闲事,但听到“老顽童”三字,心中一凛,...
34.0 第三十四回 岛上巨变郭靖低声道:“蓉儿,你还要什么?”黄蓉道:“我还要什么?什么也不要啦!”...
35.0 第三十五回 铁枪庙中船靠岸边,走上二三十人来,彭连虎、沙通天等人均在其内。最后上岸的一高一矮...
36.0 第三十六回 大军西征黄蓉幽幽地道:“欧阳伯伯赞得我可太好了。现下郭靖中你之计,和我爹爹势不两...
37.0 第三十七回 从天而降这一日郭靖驻军那密河畔,晚间正在帐中研读兵书,忽听帐外喀的一声轻响。帐门...
38.0 第三十八回 锦囊密令郭靖陪了丘处机与他门下十八名弟子李志常、尹志平、夏志诚、于志可,张志素、...
39.0 第三十九回 是非善恶郭靖纵马急驰数日,已离险地。缓缓南归,天时日暖,青草日长,沿途兵革之余,...
40.0 第四十回 华山论剑欧阳锋冷冷地道:“早到早比,迟到迟比。老叫化,你今日跟我是比武决胜呢,还是...
import jieba
import jieba.analyse

# 注意:函数是在使用默认的TFIDF模型进行分析!
jieba.analyse.extract_tags(chapter.txt[1])
Building prefix dict from the default dictionary ...
Loading model from cache /tmp/jieba.cache
Loading model cost 0.908 seconds.
Prefix dict has been built succesfully.





['杨铁心',
 '包惜弱',
 '郭啸天',
 '颜烈',
 '丘处机',
 '武官',
 '杨二人',
 '官兵',
 '曲三',
 '金兵',
 '那道人',
 '道长',
 '娘子',
 '段天德',
 '咱们',
 '临安',
 '说道',
 '丈夫',
 '杨家枪',
 '两人']
jieba.analyse.extract_tags(chapter.txt[1], withWeight = True) # 要求返回权重值
[('杨铁心', 0.21886511509515091),
 ('包惜弱', 0.1685852913570757),
 ('郭啸天', 0.09908082913091291),
 ('颜烈', 0.05471627877378773),
 ('丘处机', 0.049556061537506184),
 ('武官', 0.04608486747703612),
 ('杨二人', 0.044305304110440376),
 ('官兵', 0.040144546232276104),
 ('曲三', 0.03439059290450272),
 ('金兵', 0.0336976598949901),
 ('那道人', 0.03117114380098961),
 ('道长', 0.02912588670625928),
 ('娘子', 0.026796070076125684),
 ('段天德', 0.025139911869037603),
 ('咱们', 0.023296768210644483),
 ('临安', 0.022991990912831523),
 ('说道', 0.022350916333591046),
 ('丈夫', 0.02221595763081643),
 ('杨家枪', 0.019765724469755074),
 ('两人', 0.0192267944114003)]
# 应用自定义词典改善分词效果
jieba.load_userdict('金庸小说词库.txt') # dict为自定义词典的路径

# 在TFIDF计算中直接应用停用词表
jieba.analyse.set_stop_words('停用词.txt')

TFres = jieba.analyse.extract_tags(chapter.txt[1], withWeight = True)
TFres[:10]
[('杨铁心', 0.24787133516800222),
 ('包惜弱', 0.1909279203321098),
 ('郭啸天', 0.11221202335308209),
 ('曲三', 0.06426483083720931),
 ('颜烈', 0.061967833792000555),
 ('丘处机', 0.056123732343681704),
 ('武官', 0.052192500516161394),
 ('杨二人', 0.050177091402185486),
 ('官兵', 0.04546490778113197),
 ('金兵', 0.038163614820832165)]
# 使用自定义TF-IDF频率文件
jieba.analyse.set_idf_path("idf.txt.big")
TFres1 = jieba.analyse.extract_tags(chapter.txt[1], withWeight = True)
TFres1[:10]
[('杨铁心', 0.24787133516800222),
 ('包惜弱', 0.1909279203321098),
 ('郭啸天', 0.11221202335308209),
 ('武官', 0.07034186538551414),
 ('颜烈', 0.061967833792000555),
 ('说道', 0.05861822115459512),
 ('丘处机', 0.056123732343681704),
 ('曲三', 0.055268608517189684),
 ('一个', 0.053593802198486966),
 ('杨二人', 0.053593802198486966)]

sklearn

输出格式为矩阵,直接为后续的sklearn建模服务。

需要先使用背景语料库进行模型训练。

结果中给出的是字典ID而不是具体词条,直接阅读结果比较困难。

class sklearn.feature_extraction.text.TfidfTransformer()

发现参数基本上都不用动,所以这里就不介绍了…

from sklearn.feature_extraction.text import TfidfTransformer

txtlist = [ " ".join(m_cut(w)) for w in chapter.txt.iloc[:5]] 

vectorizer = CountVectorizer() 
X = vectorizer.fit_transform(txtlist) # 将文本中的词语转换为词频矩阵  

transformer = TfidfTransformer()  
tfidf = transformer.fit_transform(X)  #基于词频矩阵X计算TF-IDF值  
tfidf
tfidf.toarray() # 转换为数组
tfidf.todense() # 转换为矩阵
tfidf.todense().shape
print("字典长度:", len(vectorizer.vocabulary_))
vectorizer.vocabulary_

gensim

输出格式为list,目的也是为后续的建模分析服务。

需要先使用背景语料库进行模型训练。

结果中给出的是字典ID而不是具体词条,直接阅读结果比较困难。

gensim也提供了sklearn的API接口:sklearn_api.tfidf,可以在sklearn中直接使用。

# 文档分词及预处理  
chaplist = [m_cut(w) for w in chapter.txt.iloc[:5]]
chaplist
from gensim import corpora, models  

# 生成文档对应的字典和bow稀疏向量
dictionary = corpora.Dictionary(chaplist)  
corpus = [dictionary.doc2bow(text) for text in chaplist] # 仍为list in list  
corpus
tfidf_model = models.TfidfModel(corpus) # 建立TF-IDF模型  
corpus_tfidf = tfidf_model[corpus] # 对所需文档计算TF-IDF结果
corpus_tfidf
corpus_tfidf[3] # 列出所需文档的TF-IDF计算结果
dictionary.token2id # 列出字典内容

TextRank算法

TextRank算法的jieba实现

jieba.analyse.textrank(

sentence, topK=20, withWeight=False,

allowPOS=(‘ns’, ‘n’, ‘vn’, ‘v’)

) # 注意默认过滤词性

实战练习

请使用《射雕》全文计算出jieba分词的IDF语料库,然后使用该语料库重新对第一章计算关键词。比较这样的分析结果和以前有何不同。

请自行编制将jieba分词的TF-IDF结果转换为文档-词条矩阵格式的程序。

请自行思考本章提供的三种TF-IDF实现方式的使用场景是什么。

抽取文档主题

什么是主题模型

LDA,Latent Dirichlet Allocation

  • Q: 有这么一篇文章,里面提到了詹姆斯、湖人队、季后赛,请问这篇文章最可能的主题是什么?

    • 军事
    • 体育
    • 养生
    • 教育

LDA由Blei于2003年提出,其基本思想是把文档看成各种隐含主题的混合,而每个主题则表现为跟该主题相关的词项的概率分布

  • 该方法不需要任何关于文本的背景知识
  • 隐含主题的引入使得分析者可以对“一词多义”和“一义多词”的语言现象进行建模,更接近人类语言交互的特征

LDA基于词袋模型构建,认为文档和单词都是可交换的,忽略单词在文档中的顺序和文档在语料库中的顺序,从而将文本信息转化为易于建模的数字信息

  • 主题就是一个桶,里面装了出现概率较高的单词,这些单词与这个主题有很强的的相关性

LDA模型包含词项、主题和文档三层结构

本质上,LDA简单粗暴的认为:文章中的每个词都是通过“以一定概率选择某个主题,再从该主题中以一定概率选择某个词”得到的

一个词可能会关联很多主题,因此需要计算各种情况下的概率分布,来确定最可能出现的主题是哪种

  • 体育:{姚明:0.3,篮球:0.5,拳击:0.2,李现:0.03,王宝强:0.03,杨紫:0.04}
  • 娱乐:{姚明:0.03,篮球:0.03,足球:0.04,李现:0.6,王宝强:0.7,杨紫:0.8}

一篇文章可能会涉及到几个主题,因此也需要计算多个主题的概率

  • 体育新闻:[废话,体育,体育,体育,…,娱乐,娱乐]
  • 八卦消息:[废话,废话,废话,废话,…,娱乐,娱乐]

LDA中涉及到的数学知识

多项式分布:主题和词汇的概率分布服从多项式分布

  • 如果1个词汇主题,就是大家熟悉的二项分布

Dirichlet分布:上述多项式分布的参数为随机变量,均服从Dirichlet分布

Gibbs抽样:直接求LDA的精确参数分布计算量太大,实际上不可行,因此通过Gibbs抽烟减小计算量,得到逼近的结果

  • 通过现有文章(已有主题,或者需要提取主题)训练处LDA模型
  • 用模型预测新的文章所属主题分类

主题模型对于 短文本 效果不好

主题模型的sklearn实现

在scikit-learn中,LDA主题模型的类被放置在sklearn.decomposition.LatentDirichletAllocation类中,其算法实现主要基于变分推断EM算法,而没有使用基于Gibbs采样的MCMC算法实现。

注意由于LDA是基于词频统计的,因此理论上一般不宜用TF-IDF来做文档特征,但并非不能尝试。实际分析中也确实会见到此类操作。

class sklearn.decomposition.LatentDirichletAllocation(

n_components = None : 隐含主题数K,需要设置的最重要参数。
K的设定范围和具体的研究背景有关。
K越大,需要的文档样本越多。

doc_topic_prior = None : 文档主题先验Dirichlet分布的参数α,未设定则用1/K。

topic_word_prior = None : 主题词先验Dirichlet分布的参数η,未设定则用1/K。

learning_method = ‘online’ : 即LDA的求解算法。‘batch’ | ‘online’
batch: 变分推断EM算法,会将将训练样本分批用于更新主题词分布,新版默认算法。
样本量不大只是用来学习的话用batch比较好,这样可以少很多参数要调。
需注意n_components(K), doc_topic_prior(α), topic_word_prior(η)
online: 在线变分推断EM算法,大样本时首选。
需进一步注意learning_decay, learning_offset,
total_samples和batch_size等参数。

仅在online算法时需要设定的参数

learning_decay = 0.7 :控制"online"算法的学习率,一般不用修改。
取值最好在(0.5, 1.0],以保证"online"算法渐进的收敛。

learning_offset = 10. :用来减小前面训练样本批次对最终模型的影响。
取值要大于1。

total_samples = 1e6 : 分步训练时每一批文档样本的数量。
使用partial_fit进行模型拟合时才需要此参数。

batch_size = 128 : 每次EM算法迭代时使用的文档样本的数量。

)

将语料库转换为所需矩阵

除直接使用分词清理后文本进行转换外,也可以先计算关键词的TF-IDF值,然后使用关键词矩阵进行后续分析。

# 设定分词及清理停用词函数
# 熟悉Python的可以使用 open('stopWord.txt').readlines() 获取停用词list,效率更高
stoplist = list(pd.read_csv('停用词.txt', names = ['w'], sep = 'aaa', 
                            encoding = 'utf-8', engine='python').w)
import jieba 
def m_cut(intxt):
    return [ w for w in jieba.cut(intxt) 
            if w not in stoplist and len(w) > 1 ] 
# 生成分词清理后章节文本
cleanchap = [ " ".join(m_cut(w)) for w in chapter.txt] 
# 将文本中的词语转换为词频矩阵  
from sklearn.feature_extraction.text import CountVectorizer

countvec = CountVectorizer(min_df = 5) 

wordmtx = countvec.fit_transform(cleanchap) 
wordmtx
#基于词频矩阵X计算TF-IDF值  
from sklearn.feature_extraction.text import TfidfTransformer

transformer = TfidfTransformer()  
tfidf = transformer.fit_transform(wordmtx)  
tfidf
# 设定LDA模型
from sklearn.decomposition import LatentDirichletAllocation

n_topics = 10
ldamodel = LatentDirichletAllocation(n_components = n_topics)
# 拟合LDA模型
ldamodel.fit(wordmtx)
# 拟合后模型的实质
print(ldamodel.components_.shape)
ldamodel.components_[:2]
# 主题词打印函数
def print_top_words(model, feature_names, n_top_words):
    for topic_idx, topic in enumerate(model.components_):
        print("Topic #%d:" % topic_idx)
        print(" ".join([feature_names[i] 
                        for i in topic.argsort()[:-n_top_words - 1:-1]]))
    print()
n_top_words = 12
tf_feature_names = countvec.get_feature_names()
print_top_words(ldamodel, tf_feature_names, n_top_words)

gensim实现

class gensim.models.ldamodel.LdaModel(

corpus = None : 用于训练模型的语料

num_topics = 100 : 准备提取的主题数量

id2word = None : 所使用的词条字典,便于结果阅读

passes = 1 :模型遍历语料库的次数,次数越多模型越精确,但是也更花时间

)

用新出现的语料更新模型

ldamodel.update(other_corpus)

gensim也提供了sklearn的API接口:sklearn_api.ldamodel,可以在sklearn中直接使用。

# 设定分词及清理停用词函数
# 熟悉Python的可以使用 open('stopWord.txt').readlines() 获取停用词list,效率更高
stoplist = list(pd.read_csv('停用词.txt', names = ['w'], sep = 'aaa', 
                            encoding = 'utf-8', engine='python').w)
import jieba 
def m_cut(intxt):
    return [ w for w in jieba.cut(intxt) 
            if w not in stoplist and len(w) > 1 ] 
# 文档预处理,提取主题词  
chaplist = [m_cut(w) for w in chapter.txt]
# 生成文档对应的字典和bow稀疏向量
from gensim import corpora, models  

dictionary = corpora.Dictionary(chaplist)  
corpus = [dictionary.doc2bow(text) for text in chaplist] # 仍为list in list  

tfidf_model = models.TfidfModel(corpus) # 建立TF-IDF模型  
corpus_tfidf = tfidf_model[corpus] # 对所需文档计算TF-IDF结果
corpus_tfidf
from gensim.models.ldamodel import LdaModel

# 列出所消耗的时间备查
%time ldamodel = LdaModel(corpus, id2word = dictionary, \
                          num_topics = 10, passes = 2) 

列出最重要的前若干个主题

print_topics(num_topics=20, num_words=10)

ldamodel.print_topics()
# 计算各语料的LDA模型值
corpus_lda = ldamodel[corpus_tfidf] # 此处应当使用和模型训练时相同类型的矩阵

for doc in corpus_lda:
    print(doc)
ldamodel.get_topics()
# 检索和文本内容最接近的主题
query = chapter.txt[1] # 检索和第1章最接近的主题
query_bow = dictionary.doc2bow(m_cut(query)) # 频数向量
query_tfidf = tfidf_model[query_bow] # TF-IDF向量
print("转换后:", query_tfidf[:10])

ldamodel.get_document_topics(query_bow) # 需要输入和文档对应的bow向量
# 检索和文本内容最接近的主题
ldamodel[query_tfidf]

结果的图形化呈现

pyLDAvis包引入自R,可以用交互式图形的方式呈现主题模型的分析结果。

同时支持sklearn和gensim包。

在许多系统配置下都会出现兼容问题。

# 对sklearn的LDA结果作呈现
import pyLDAvis
import pyLDAvis.sklearn

pyLDAvis.enable_notebook()

pyLDAvis.sklearn.prepare(ldamodel, tfidf, countvec)
pyLDAvis.gensim.prepare(ldamodel, corpus, dictionary)
pyLDAvis.disable_notebook() # 关闭notebook支持后,可以看到背后所生成的数据

实战练习

在其余参数全部固定不变的情况下,尝试分别用清理前矩阵、清理后原始矩阵、TF-IDF矩阵进行LDA模型拟合,比较分析结果。

在gensim拟合LDA时,分别将passes参数设置为1、5、10、50、100等,观察结果变化的情况,思考如何对该参数做最优设定。

请尝试对模型进行优化,得到对本案例较好的分析结果。

提示:使用gensim进行拟合更容易一些。

文档相似度

用途

  • 搜索引擎的类似文章推荐
  • 购物网站的类似商品推荐
  • 点评网站/微博微信平台上的类似内容推荐

基于词袋模型的基本思路

  • 如果两个文档/两句话的用词越相似,他们的内容就应该越相似。因此,可以从词频入手,计算他们的相似程度
  • 文档向量化之后,相似度的考察就可以直接转化为计算空间中的距离问题
  • 缺陷: 不能考虑否定词的巨大作用,不能考虑词序的差异

在本质上,向量空间中文本相似度的计算和任何聚类方法所考虑的问题***没有区别***

余弦相似度

两个向量间的夹角能够很好的反映其相似度

  • 但夹角大小使用不便,因此用夹角的余弦值作为相似度衡量指标
  • 思考:为什么只考虑夹角,不考虑相对距离?

余弦值越接近1,夹角越接近0度,两个向量也就越相似

可以证明余弦值的计算公式可以直接扩展到n维空间

因此在由n维向量所构成的空间中,可以利用余弦值来计算文档的相似度

相似度计算:基本分析思路

语料分词、清理

  • 原始语料分词
  • 语料清理

语料向量化

  • 将语料转换为词频向量
  • 为了避免文章长度的差异,长度悬殊时可以考虑使用相对词频

计算相似度

  • 计算两个向量的余弦相似度,值越大表示越相似

仍然存在的问题

  • 高频词不一定具有文档代表性,导致相似度计算结果变差

相似度计算:基本分析思路

语料分词、清理

  • 原始语料分词
  • 语料清理

语料向量化

  • 将语料转换为基于关键词的词频向量
  • 为了避免文章长度的差异,长度悬殊时可以考虑使用相对词频

使用TF-IDF算法,找出两篇文章的关键词

  • 例如取前20个,或者前50个

计算相似度

  • 计算两个向量的余弦相似度,值越大表示越相似

当向量表示概率分布式,其他相似度测量方法比余弦相似度更好

词条相似度:word2vec

词袋模型不考虑词条之间的相关性,因此无法用于计算词条相似度。

分布式表达会考虑词条的上下文关联,因此能够提取出词条上下文中的相关性信息,而词条之间的相似度就可以直接利用此类信息加以计算。

目前主要使用gensim实现相应的算法。

gensim也提供了sklearn的API接口:sklearn_api.w2vmodel,可以在sklearn中直接使用。

设置word2vec模型

class gensim.models.word2vec.Word2Vec(

sentences = None : 类似list of list的格式,对于特别大的文本,尽量考虑流式处理

size = 100 : 词条向量的维度,数据量充足时,300/500的效果会更好

window = 5 : 上下文窗口大小

workers = 3 : 同时运行的线程数,多核系统可明显加速计算

其余细节参数设定:

min_count = 5 : 低频词过滤阈值,低于该词频的不纳入模型

max_vocab_size = None : 每1千万词条需要1G内存,必要时设定该参数以节约内存

sample=0.001 : 负例采样的比例设定

negative=5 : 一般为5-20,设为0时不进行负例采样

iter = 5 : 模型在语料库上的迭代次数,该参数将被取消

与神经网络模型有关的参数设定:

seed=1, alpha=0.025, min_alpha=0.0001, sg=0, hs=0

)

chapter.head()
# 分词和预处理,生成list of list格式
import jieba

chapter['cut'] = chapter.txt.apply(jieba.lcut)
chapter.head()
# 初始化word2vec模型和词表
from gensim.models.word2vec import Word2Vec

n_dim = 300 # 指定向量维度,大样本量时300~500较好

w2vmodel = Word2Vec(size = n_dim, min_count = 10)
w2vmodel.build_vocab(chapter.cut) # 生成词表
w2vmodel

对word2vec模型进行训练

word2vecmodel.train(

sentences : iterable of iterables格式,对于特别大量的文本,尽量考虑流式处理

total_examples = None : 句子总数,int,可直接使用model.corpus_count指定

total_words = None : 句中词条总数,int,该参数和total_examples至少要指定一个

epochs = None : 模型迭代次数,需要指定

其他带默认值的参数设定:

start_alpha=None, end_alpha=None, word_count=0, queue_factor=2,

report_delay=1.0, compute_loss=False, callbacks=()

)

# 在评论训练集上建模(大数据集时可能会花费几分钟)
# 本例消耗内存较少
#time 
w2vmodel.train(chapter.cut, \
               total_examples = w2vmodel.corpus_count, epochs = 10)
# 训练完毕的模型实质
print(w2vmodel.wv["郭靖"].shape)
w2vmodel.wv["郭靖"]

w2v模型的保存和复用

w2vmodel.save(存盘路径及文件名称)
w2vmodel.load(存盘路径及文件名称)

词向量间的相似度

w2vmodel.wv.most_similar(词条)

w2vmodel.wv.most_similar("郭靖")
w2vmodel.wv.most_similar("黄蓉", topn = 20)
w2vmodel.wv.most_similar("黄蓉道")
# 寻找对应关系
w2vmodel.wv.most_similar(['郭靖', '小红马'], ['黄药师'], topn = 5)
w2vmodel.wv.most_similar(positive=['郭靖', '黄蓉'], negative=['杨康'], topn=10)
# 计算两个词的相似度/相关程度
print(w2vmodel.wv.similarity("郭靖", "黄蓉"))
print(w2vmodel.wv.similarity("郭靖", "杨康"))
print(w2vmodel.wv.similarity("郭靖", "杨铁心"))
# 寻找不合群的词
w2vmodel.wv.doesnt_match("小红马 黄药师 鲁有脚".split())
w2vmodel.wv.doesnt_match("杨铁心 黄药师 黄蓉 洪七公".split())

文档相似度

基于词袋模型计算

sklearn实现

sklearn.metrics.pairwise.pairwise_distances(

X : 用于计算距离的数组

[n_samples_a, n_samples_a] if metric == ‘precomputed’

[n_samples_a, n_features] otherwise

Y = None : 用于计算距离的第二数组,当metric != 'precomputed’时可用

metric = ‘euclidean’ : 空间距离计算方式

scikit-learn原生支持 : [‘cityblock’, ‘cosine’, ‘euclidean’,

   'l1', 'l2', 'manhattan'\],可直接使用稀疏矩阵格式

来自scipy.spatial.distance : [‘braycurtis’, ‘canberra’,

‘chebyshev’, ‘correlation’, ‘dice’, ‘hamming’, ‘jaccard’,

   'kulsinski', 'mahalanobis', 'matching', 'minkowski',

   'rogerstanimoto', 'russellrao', 'seuclidean', 'sokalmichener',

   'sokalsneath', 'sqeuclidean', 'yule'\] 不支持稀疏矩阵格式

n_jobs = 1 : 用于计算的线程数,为-1时,所有CPU内核都用于计算

)

cleanchap = [ " ".join(m_cut(w)) for w in chapter.txt.iloc[:5]] 

from sklearn.feature_extraction.text import CountVectorizer

countvec = CountVectorizer() 

resmtx = countvec.fit_transform(cleanchap)
resmtx
from sklearn.metrics.pairwise import pairwise_distances

pairwise_distances(resmtx, metric = 'cosine')
pairwise_distances(resmtx) # 默认值为euclidean
# 使用TF-IDF矩阵进行相似度计算
pairwise_distances(tfidf[:5], metric = 'cosine')

gensim实现

基于LDA计算余弦相似度

需要使用的信息:

拟合完毕的lda模型
按照拟合模型时矩阵种类转换的需检索文本
    需检索的文本
    建模时使用的字典
from gensim import similarities
simmtx = similarities.MatrixSimilarity(corpus)
simmtx
# 检索和第1章内容最相似(所属主题相同)的章节
simmtx = similarities.MatrixSimilarity(corpus) # 使用的矩阵种类需要和拟合模型时相同
simmtx
simmtx.index[:2]
# 使用gensim的LDA拟合结果进行演示
query = chapter.txt[1] 
query_bow = dictionary.doc2bow(m_cut(query))

lda_vec = ldamodel[query_bow] # 转换为lda模型下的向量
sims = simmtx[lda_vec] # 进行矩阵内向量和所提供向量的余弦相似度查询
sims = sorted(enumerate(sims), key=lambda item: -item[1])
sims

doc2vec

word2vec用来计算词条相似度非常合适。

较短的文档如果希望计算文本相似度,可以将各自内部的word2vec向量分别进行平均,用平均后的向量作为文本向量,从而用于计算相似度。

但是对于长文档,这种平均的方式显然过于粗糙。

doc2vec是word2vec的拓展,它可以直接获得sentences/paragraphs/documents的向量表达,从而可以进一步通过计算距离来得到sentences/paragraphs/documents之间的相似性。

模型概况

分析目的:获得文档的一个固定长度的向量表达。
数据:多个文档,以及它们的标签,一般可以用标题作为标签。 
影响模型准确率的因素:语料的大小,文档的数量,越多越高;文档的相似性,越相似越好。
import jieba 
import gensim
from gensim.models import doc2vec

def m_doc(doclist):
    reslist = []
    for i, doc in enumerate(doclist):
        reslist.append(doc2vec.TaggedDocument(jieba.lcut(doc), [i]))
    return reslist

corp = m_doc(chapter.txt)
corp[:2]
d2vmodel = gensim.models.Doc2Vec(vector_size = 300, 
                window = 20, min_count = 5)
d2vmodel.build_vocab(corp)
d2vmodel.wv.vocab
# 将新文本转换为相应维度空间下的向量
newvec = d2vmodel.infer_vector(jieba.lcut(chapter.txt[1]))
d2vmodel.docvecs.most_similar([newvec], topn = 10)

文档聚类

在得到文档相似度的计算结果后,文档聚类问题在本质上已经和普通的聚类分析没有区别。

注意:最常用的Kmeans使用的是平方欧氏距离,这在文本聚类中很可能无法得到最佳结果。

算法的速度和效果同样重要。

# 为章节增加名称标签
chapter.index = [raw.txt[raw.chap == i].iloc[0] for i in chapter.index]
chapter.head()
import jieba
cuttxt = lambda x: " ".join(m_cut(x)) 
cleanchap = chapter.txt.apply(cuttxt) 
cleanchap[:2]
# 计算TF-IDF矩阵
from sklearn.feature_extraction.text import TfidfTransformer

vectorizer = CountVectorizer() 
wordmtx = vectorizer.fit_transform(cleanchap) # 将文本中的词语转换为词频矩阵  

transformer = TfidfTransformer()  
tfidf = transformer.fit_transform(wordmtx)  #基于词频矩阵计算TF-IDF值  
tfidf
# 进行聚类分析
from sklearn.cluster import KMeans  

clf = KMeans(n_clusters = 5)  
s = clf.fit(tfidf)  
print(s)  
clf.cluster_centers_
clf.cluster_centers_.shape
clf.labels_
chapter['clsres'] = clf.labels_
chapter.head()
chapter.sort_values('clsres').clsres
chapgrp = chapter.groupby('clsres')
chapcls = chapgrp.agg(sum) # 只有字符串列的情况下,sum函数自动转为合并字符串

cuttxt = lambda x: " ".join(m_cut(x)) 
chapclsres = chapcls.txt.apply(cuttxt) 
chapclsres
# 列出关键词以刻画类别特征
import jieba.analyse as ana

ana.set_stop_words('停用词.txt')

for item in chapclsres:
    print(ana.extract_tags(item, topK = 10))

文档分类

什么是文本分类

  • 通过程序对文本按照一定的分类体系或标准自动分类标记

  • 应用场景

    • 对抓取到的新闻进行自动归类
    • 邮件服务器对收到的邮件进行垃圾邮件甄别
    • 监测系统对采集到的文本信息进行优先级评估,将高优先级的信息优先发送至人工处理流程

文本分类的基本步骤

  • 文本有效信息的提取

    • 文本预处理:分词、清理等工作
    • 特征抽取:从文档中抽取出反映文档主题的特征
  • 分类器的选择与训练

  • 分类结果的评价与反馈

    • 该步骤与普通的模型完全相同
  • 基于词袋模型时可考虑的特征抽取方法:

    • 词频(基于词袋模型的文档-词条矩阵)
    • 关键词(TF-IDF)
    • 文档主题(LDA模型)

基于词袋模型的文本分类算法选择

  • 逻辑上所有用于分类因变量预测的模型都可以用于文本分类

    • 判别分析、Logistics回归、树模型、神经网络、SVM、KNN、朴素贝叶斯、遗传算法、Bagging、Boosting
  • 文本分类的数据特征

    • 自变量(词条)数量极多
    • 各自变量(词条)之间不可能完全独立
    • 大部分自变量(词条)只是混杂项而已,对分类无贡献
  • 选择算法的几个考虑方向

    • 速度 变量筛选能力 容错性 共线性容忍度
  • 算法简单的贝叶斯公式(朴素贝叶斯)往往会成为优先考虑的算法

  • 模型中,改进后的随机森林,以及SVM相对应用较多

  • 有的算法会在特定数据集下表现较好,不能一概而论

  • 语料的事先清理至关重要,甚至可以考虑只使用关键词来分类

Python下的文本分类工具包选择

  • sklearn:

    • 基于D2M矩阵结构,将文本分类问题看做标准的样本分类预测问题来处理
    • 非常完善的模型拟合、诊断功能
    • 同时也提供了朴素贝叶斯算法
  • NLTK:

    • 主要基于朴素贝叶斯算法进行文本分类
    • 可以直接使用稀疏向量格式进行分析,使用上很方便
  • gensim:

    • 基于LDA等更先进的模型,提供文本分类功能

朴素贝叶斯的算法原理

  • 乘法公式: P ( A B ) = P ( B A ) P ( A ) = P ( A B ) P ( B ) P(AB) = P(B|A) * P(A) = P(A|B) * P(B)

    • P ( A B ) P(AB) :联合概率
    • P ( A ) P(A) P ( B ) P(B) :先验概率
    • P ( B A ) P(B|A) P ( A B ) P(A|B) :后验概率
  • 贝叶斯公式: P ( B A ) = P ( A B ) P ( B ) P ( A ) P(B|A) = \frac{P(A|B) * P(B)}{P(A)}

    • 拥有某特征的案例,属于某类的概率 = 该类出现的概率 * 该类有某特征的概率 / 该特征出现的概率
  • 该公式可以很容易的扩展至多条件的情况

    • 具体公式大家可以自己尝试写出来
  • 应用案例

    • 判断一下邮件正文中含有句子:“我司可办理正规发票/增值税发票,点数优惠!”的是垃圾邮件的概率有多大?
  • 句子的变化形式很多,但是核心信息就包含在“办理”、“发票”、“优惠”等几个词条的组合中,因此考虑分词后建模

    • 发垃圾邮件的人也很敬业的…
  • 朴素贝叶斯(Naive Bayes)=贝叶斯公式+条件独立假设

    • 抛弃词条建的关联,假设各个词条完全独立,完全基于词袋模型进行计算
    • 如此***蠢萌***的朴素贝叶斯方法,实践证明至少在垃圾邮件的识别中的应用效果是非常好的

sklearn实现

sklearn是标准的数据挖掘建模工具包,在语料转换为d2m矩阵结构之后,就可以使用所有标准的DM建模手段在sklearn中进行分析。

在sklearn中也实现了朴素贝叶斯算法,使用方式上也和其他模型非常相似。

生成D2M矩阵

# 从原始语料df中提取出所需的前两章段落
raw12 = raw[raw.chap.isin([1,2])]
raw12ana = raw12.iloc[list(raw12.txt.apply(len) > 50), :] # 只使用超过50字的段落
raw12ana.reset_index(drop = True, inplace = True)
print(len(raw12ana))
raw12ana.head()
# 分词和预处理
import jieba

cuttxt = lambda x: " ".join(jieba.lcut(x)) # 这里不做任何清理工作,以保留情感词
raw12ana["cleantxt"] = raw12ana.txt.apply(cuttxt) 
raw12ana.head()

from sklearn.feature_extraction.text import CountVectorizer
countvec = CountVectorizer()

wordmtx = countvec.fit_transform(raw12ana.cleantxt)
wordmtx

划分训练集和测试集

# 作用:将数据集划分为 训练集和测试集
from sklearn.model_selection import train_test_split

x_train, x_test, y_train, y_test = train_test_split(wordmtx, raw12ana.chap, 
    test_size = 0.3, random_state = 111)

拟合朴素贝叶斯模型

from sklearn import naive_bayes

NBmodel = naive_bayes.MultinomialNB()
# 拟合模型
NBmodel.fit(x_train, y_train)
# 进行验证集预测
x_test
NBmodel.predict(x_test)

模型评估

# 预测准确率(给模型打分)
print('训练集:', NBmodel.score(x_train, y_train), 
      ',验证集:', NBmodel.score(x_test, y_test))
from sklearn.metrics import classification_report

print(classification_report(y_test, NBmodel.predict(x_test)))

使用Logistic回归模型进行分类

from sklearn.linear_model import LogisticRegression

logitmodel = LogisticRegression() # 定义Logistic回归模型
# 拟合模型
logitmodel.fit(x_train, y_train)
print(classification_report(y_test, logitmodel.predict(x_test)))

模型预测

将需要预测的文本转换为和建模时格式完全对应的d2m矩阵格式,随后即可进行预测。

countvec.vocabulary_
string = "杨铁心和包惜弱收养穆念慈"
words = " ".join(jieba.lcut(string))
words_vecs = countvec.transform([words]) # 数据需要转换为可迭代的list格式
words_vecs
NBmodel.predict(words_vecs)

NLTK实现

NLTK中内置了朴素贝叶斯算法,可直接实现文档分类。

数据集中语料的格式

用于训练的语料必须是分词完毕的字典形式,词条为键名,键值则可以是数值、字符、或者T/F

{‘张三’ : True, ‘李四’ : True, ‘王五’ : False}

{‘张三’ : 1, ‘李四’ : 1, ‘王五’ : 0}

{‘张三’ : ‘有’, ‘李四’ : ‘有’, ‘王五’ : ‘无’}

# 使用Pandas的命令进行转换
freqlist.to_dict()
df0.groupby(['word']).agg('size').tail(10).to_dict()

训练用数据集的格式
训练用数据集为list of list格式,每个成员为list[语料字典, 结果变量]

[

[{‘张三’ : 1, ‘李四’ : 1, ‘王五’ : 0}, ‘合格’],

[{‘张三’ : 0, ‘李四’ : 1, ‘王五’ : 0}, ‘不合格’]

]

构建模型

考虑到过拟合问题,此处需要先拆分好训练集和测试集

model = NaiveBayesClassifier.train(training_data)

# 这里直接以章节为一个单元进行分析,以简化程序结构
import nltk
from nltk import FreqDist

# 生成完整的词条频数字典,这部分也可以用遍历方式实现
fdist1 = FreqDist(m_cut(chapter.txt[1])) 
fdist2 = FreqDist(m_cut(chapter.txt[2])) 
fdist3 = FreqDist(m_cut(chapter.txt[3])) 
fdist1
from nltk.classify import NaiveBayesClassifier

training_data = [ [fdist1, 'chap1'], [fdist2, 'chap2'], [fdist3, 'chap3'] ]
# 训练分类模型
NLTKmodel = NaiveBayesClassifier.train(training_data)
print(NLTKmodel.classify(FreqDist(m_cut("杨铁心收养穆念慈"))))
print(NLTKmodel.classify(FreqDist(m_cut("钱塘江 日日夜夜 包惜弱 颜烈 使出杨家枪"))))

模型拟合效果的考察

nltk.classify.accuracy(NLTKmodel, training_data) # 准确度评价
NLTKmodel.show_most_informative_features(5)#得到似然比,检测对于哪些特征有用

分类结果评估

精确率和召回率

精确率和召回率主要用于二分类问题(从其公式推导也可看出),结合混淆矩阵有:

分类1 分类2
预测分类1 TP FP
预测分类2 FN TN

精确率 P r e c i s i o n = T P T P + F P Precision = \frac{TP}{TP+FP}

召回率 R e c a l l = T P T P + F N Recall = \frac{TP}{TP+FN}

理想情况下,精确率和召回率两者都越高越好。然而事实上这两者在某些情况下是矛盾的,精确率高时,召回率低;精确率低时,召回率高;关于这个性质通过观察PR曲线不难观察出来。比如在搜索网页时,如果只返回最相关的一个网页,那精确率就是100%,而召回率就很低;如果返回全部网页,那召回率为100%,精确率就很低。因此在不同场合需要根据实际需求判断哪个指标跟重要。

# 分类报告:precision/recall/fi-score/均值/分类个数
 from sklearn.metrics import classification_report
 class_true = [1, 2, 3, 3, 1] #正确的分类结果
 class_pred = [1, 1, 3, 3, 1] #实际的分类结果
 target_names = ['class 1', 'class 2', 'class 3']
 print(classification_report(class_true, class_pred, target_names=target_names))
             precision    recall  f1-score   support

    class 1       0.67      1.00      0.80         2
    class 2       0.00      0.00      0.00         1
    class 3       1.00      1.00      1.00         2

avg / total       0.67      0.80      0.72         5
发布了76 篇原创文章 · 获赞 23 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_39309652/article/details/103231363