13自然语言处理基础入门

字符串基础操作及应用

自然语言处理简介

做一个中文文本分类任务,首先要做的是文本的预处理,对文本进行分词和去停用词操作,来把字符串分割成词与词组合而成的字符串集合并去掉其中的一些非关键词汇(像是:的、地、得等)。再就是对预处理过后的文本进行特征提取。最后将提取到的特征送进分类器进行训练。
什么是自然语言处理
NLP(Natural Language Processing,自然语言处理)当中所谓的「自然」是为了与人造的语言(比如 C 语言, JAVA 等)区分开来,指自然形成的语言,即平时人们日常使用的交流的语言。「语言」则是人类区别其他动物的本质特性。在所有生物中,只有人类才具有语言能力。人类的多种智能都与语言有着密切的关系。人类的逻辑思维以语言为形式,人类的绝大部分知识也是以语言文字的形式记载和流传下来的。「处理」则指的是对自然语言的各种处理方法与运用。
NLP 不仅是计算语言学的应用领域,还是计算机科学和人工智能(AI)领域的一个重要研究方向。NLP 主要研究人与计算机之间用自然语言进行有效通信的各种理论和方法。因此,又可以说 NLP 是一门集语言学、计算机科学、数学于一体的学科。
不过,NLP 虽然是与语言学有关, NLP 又与语言学那种纯粹的研究自然语言有所区别。语言学是以自然语言为研究对象,研究语言的性质、功能、结构、运用和历史发展以及其他与语言有关的问题。NLP 则是在于研究能有效实现人机间自然语言通信的计算机系统。
总的来说,自然语言处理可以概括为: 就是利用计算机的强大的运算能力,采用统计手段来对语言进行处理,然后获得需要的信息,以达到最终想要的目的,而使用各种方法的一门技术。
NLP 分类
自然语言的理解和自然语言的生成。

自然语言理解是个综合的系统工程,涉及了很多细分的学科。
代表声音的音系学:语言中发音的系统化组织。
代表构词法的词态学:研究单词构成以及相互之间的关系。
代表语句结构的句法学:给定文本的那部分是语法正确的。
代表理解的语义句法学和语用学 :给定文本的含义和目的是什么。
语言理解涉及语言、语境和各种语言形式的学科。但总的来说,自然语言理解又可以分为三个方面:
词义分析
句法分析
语义分析

自然语言的生成则是从结构化的数据(可以通俗理解为自然语言理解分析后的数据)以读取的方式自动生成文本。主要有三个阶段:
文本规划:完成结构化数据中的基础内容规划。
语句规划:从结构化数据中组合语句来表达信息流。
实现:产生语法通顺的语句来表达文本。
研究与应用
NLP 在目前大火的 AI 领域有着十分丰富的应用。总体来说,自然语言处理的研究问题(主要)有下面几种:
信息检索:对大规模文档进行索引。
语音识别:识别包含口语在内的自然语言的声学信号转换成符合预期的信号。
机器翻译:将一种语言翻译成另外一种语言。
智能问答:自动回答问题。
对话系统:通过多回合对话,跟用户进行聊天、回答、完成某项任务。
文本分类:将文本自动归类。
情感分析:判断某段文本的情感倾向
文本生成:根据需求自动生成文本
自动文摘:归纳,总结文本的摘要。
相关术语
分词:词是 NLP 中能够独立活动的有意义的语言成分。即使某个中文单字也有活动的意义,但其实这些单字也是词,属于单字成词。
词性标注:给每个词语的词性进行标注,比如 :跑/动词、美丽的/形容词等等。
命名实体识别:从文本中识别出具有特定类别的实体。像是识别文本中的日期,地名等等。
词义消歧:多义词判断最合理的词义。
句法分析:解析句子中各个成分的依赖关系。
指代消解:消除和解释代词「这个,他,你」等的指代问题。

字符串操作

字符作为文本类数据的基本单元,其在自然语言处理中的地位可以说是非常的重要。而且,大部分的自然语言处理任务都是从字符上着手。
.count() 方法返回特定的子串在字符串中出现的次数。

seq = '12345,1234,123,12,1'
seq1 = '1'
a = seq.count(seq1)
a

.strip()方法可以去除字符串首尾的指定符号。无指定时,默认去除空格符 ' ' 和换行符 '\n'。

seq = ' 我们正在使用蓝桥云课,蓝桥云课学会很多!'
seq.strip()
seq.strip('!')
seq.strip(' 我们')

有时候只想要去除字符串开头的某个字符串,但是字符串的末尾有一个同样的字符串并不需要去掉。这时候可以使用 .lstrip() 方法。

seq = '12321'
seq.lstrip('1')

同样,可以使用.rstrip() 方法来单独去除末尾的字符。

seq.rstrip('1')

经常会遇到需要将字符串拼接起来的情况,这时可以用运算符 + 来简单暴力的拼接。

seq1 = '实'
seq2 = '验'
seq3 = '楼'
seq = seq1 + seq2 + seq3
seq
seq1 = ''
seq = ['实', '验', '楼']

# 将 seq 里面的每个字符拼接起来
for n in seq:
    seq1 += n
seq1

需要将字符串用特定的符号拼接起来的字符的时候,可以用 .join() 方法来进行拼接。

seq = ['2018', '10', '31']
seq = '-'.join(seq)  # 用 '-' 拼接
seq
seq = ['实', '验', '楼']
seq = ''.join(seq)  # 用''空字符拼接
seq

当想要比较两个字符串的大小时,这里需要加载 operator 工具,它是 Python 的标准库。不需要额外下载,直接通过 import 调用即可。operator 从左到右第一个字符开始,根据设定的规则比较,返回布尔值( True,False )。
判断 a < b 型:

import operator
seq1 = '字符串 1 号'
seq2 = '字符串 2 号'
operator.lt(seq1, seq2)

除了使用 operator 之外,还有一种方法更简便: 直接使用运算符比较。
直接使用运算符比较 a < b:

seq1 < seq2

判断 a <= b:

operator.le(seq1, seq2)
seq1 <= seq2

判断 a == b:

operator.eq(seq1, seq2)
seq1 == seq2

判断 a != b:

operator.ne(seq1, seq2)
seq1 != seq2

判断 a > b:

operator.gt(seq1, seq2)
seq1 > seq2

判断 a >=b:

operator.ge(seq1, seq2)
seq1 >= seq2

在处理英文类文本的时候会遇到需要将文本全部转化为大写或者小写的时候。使用 .upper() 和 .lower() 可以很方便的完成这个任务。
使用 .upper() 将文本转化为大写。

seq = 'appLE'
seq = seq.upper()
seq

使用 .lower()将文本转化为小写。

seq = 'APPle'
seq = seq.lower()
seq

为了查找到某段字符串当中某个子串的位置信息,有两种方法。一种是.index ,一种是 .find。 两种方法都可实现这个功能,不同的是 index 如果未找到的话,会报错,而 find 未找到的则会返回 -1 值。
.find() 方法,在序列当中找到子串的起始位置。PS:第一个位置是 0 。

seq = '这个是一段字符串'
seq1 = '字符串'
seq.find(seq1)

.find() 方法,字符串中不存在子串返回 -1

seq2 = '无'
seq.find(seq2)
seq3 = '字符串'
seq.index(seq3)
seq4 = '无'
seq.index(seq4)

当想要切分字符串时,有两种常用的方法。第一种是直用序列截取的方法。这种方法十分的简单,就是根据顺序来截取序列上你想要的某些片段。
想要截取 [这是字符串], 中的‘这是字符’的时候。[0:4] 中 : 左边的 0 的意思是从序列位置 0(从 0 开始数),: 右边的 4 意思是截取到第 4 个字符(但并不包括从零开始数的第 4 个字符)。

seq = '这是字符串'
seq1 = seq[0:4]
seq1

截取某个字符的时候可以通过这样。

seq2 = seq[0]
seq2
seq3 = seq[1:4]
seq3

如果结合字符串拼接的操作,还能把截出来的字符串拼接起来。

seq = '小了白了兔'
a = seq[0]
b = seq[2]
c = seq[4]
seq1 = a+b+c
seq1

需要把一个字符串按照某个字符切分开处理。比如[今天天气很好,我们出去玩],要把两句话以 ','切开成两句话。split()函数可以完成这个操作,函数返回一个由切分好的字符串组成的列表。

seq = '今天天气很好,我们出去玩'
seq = seq.split(',')
seq
seq = '2018-11-11'
seq = seq.split('-')
seq
seq = 'I have an apple'
seq = seq.split(' ')
seq
seq = '号外!号外!特大新闻'
seq = seq.split('!')
seq

需要翻转字符串的时候,那么我们直接用序列操作,直接以上面截取序列的方法,但是按照逆向的来截取实现翻转。

seq = '12345'
seq = seq[::-1]
seq

遇到需要判断某子串在字符串中是否出现,并根据判断做出后续操作的情况。可以用 in 来作出判断,若存在则返回 True,不存在则返回 False,然后配合 if ,作出后续操作。

seq = 'abcdef'
'a' in seq
seq = '你的名字真好听!'
'的' in seq

in 关键字可以用在任何容器对象上,判断一个子对象是否存在于容器当中,并不局限于判断字符串是否存在某子串,还可以用在其他容器对象例如 list,tuple,set 等类型。

nums = [1, 2, 3, 4]
2 in nums
seq = 'abcd'
n = 0
if 'a' in seq:
    n += 1
n

有时需要把字符串中的某段字符串用另一段字符串代替,比如 2018-01-01 中的 - 用 '/' 代替。我们可以用到 .replace(a,b) ,他可以将某字符串中的 a 字符串 替换成 b 字符串。下面来实现一下。

seq = '2018-11-11'
seq = seq.replace('-', '/')
seq
seq = '等会你还要来做实验吗?'
seq = seq.replace('吗?', '哦!')
seq
seq = '小了白了兔'
seq = seq.replace('了', '')
seq

当遇到需要判断字符串是否以某段字符开头的时候。比如想要判断 ‘abcdefg’ 是否以 'a' 开头。可以用 .startswish() 方法。

seq = 'abcdefg'
seq.startswith('a')
seq = '我在蓝桥云课'
seq.startswith('me')

同样的方法,我们可以用 .endswith() 来确定字符串是否以某段字符串结尾。

seq = 'abcd'
seq.endswith('d')
seq = '我在蓝桥云课'
seq.endswith('喽')

有时候,当想要检查字符串的构成,像是检查字符串是否由纯数字构成。

seq = 's123'
seq.isdigit()
seq = '123'
seq.isdigit()

正则表达式

正则表达式是用于处理字符串的强大工具,它由一个特殊的字符序列构成一定的规则,根据这个规则可以帮你检查字符串是否与这个规则的字符串匹配。
在下面这几个日期信息中,如果想要提取其中的年份信息:
A : 2018/01/01
B : 2018-01-01
C : 2018.01.01
D :01/01/2018

观察 A,B,C,D 四个人的输入,因为每个人输入习惯的不同,导致了格式方面的不统一。这个时候,我们无法通过 Python 提供的字符串前 4 位 [0:4] 的方法来提取每段序列当中的年份信息。这时候,可以考虑用到正则表达式。我们设计一个正则表达式 [0-9]{4} ,这个正则表达式代表的是 0 到 9 的数字连续 4 次。

Python 对于正则表达式的支持,一般是通过 re 模块来提供。re 模块是 Python 自带的标准库,直接 import 加载就可以了,不需要另外下载第三方库。
首先,我们需要通过 re.compile() 将编写好的正则表达式编译为一个实例。

# 加载 re 模块
import re

# 将正则表达式编写成实例
pattern = re.compile(r'[0-9]{4}')
pattern

然后我们对字符串进行匹配,并对匹配结果进行操作。这里,我们要用到的是 re.search() 方法。 这个方法是将正则表达式与字符串进行匹配,如果找到第一个符合正则表达式的结果,就会返回,然后匹配结果存入group()中供后续操作。

import re

pattern = re.compile(r'[0-9]{4}')
time = '2018-01-01'
# 用刚刚编译好的 pattern,去匹配 time
match = pattern.search(time)
# 匹配结果存放在 group()当中的
match.group()

可以看到我们的正则表达式匹配到了 2018 这个符合我们规则(连续4个字符 都是 0-9的数字)的结果。 现在我们根据上面两段代码的内容,设计一个小程序来提取这种不规则日期当中的年份信息。

import re

pattern = re.compile(r'[0-9]{4}')
times = ('2018/01/01', '01/01/2019', '01.2017.01')

for time in times:
    match = pattern.search(time)
    if match:
        print('年份有:', match.group())

.findall():这个方法可以找到符合正则表达式的所有匹配结果。这里我们使用了 \d 规则的正则表达式,这个正则表达式可以替我们识别数字。

import re

pattern = re.compile(r'\d')
pattern.findall('o1n2m3k4')

同样的方法,我们编写一个 \D 正则表达式,这个可以匹配一个非数字字符。

pattern = re.compile('\D')
pattern.findall('1A2B3C4D')

.match() 方法与 .search() 方法类似,只匹配一次,并且只从字符串的开头开始匹配。同样,match 结果也是存在 group() 当中。

# 不止是规则,字符也是可以单独作为正则表达式使用。
pattern = re.compile('c')
pattern.match('comcdc').group()

.match() 只从开头匹配。若匹配不成功,则 group()不会有内容。

# 我们编写了字符 1 作为正则表达式去匹配。
pattern = re.compile('1')
pattern.match('abcdefg1').group()

正则表达式的应用场景十分广泛:在去除网页类文本语料库中 html 符号,去除一些聊天的表情符号像是 Orz ,T_T 等等有着十分重要的应用。

中英文分词方法及实现

英文分词

在语言理解中,词是最小的能够独立活动的有意义的粒度。由词到句,由句成文。因此,将词确定下来是理解自然语言处理的第一步,只有跨越了这一步,才能进行后续任务。
英文原文: i have a pen,i have an apple!
英文分词结果: i , have , a , pen , i , have , an , apple , !
通过上面的英文分词例子,可以发现英文文本词与词之间有空格或者标点符号,如果想要对这种普通的英文文本进行分词的话是不需要什么算法支撑,只需暴力拆分即可,即直接通过空格或者标点来将文本进行分开就可以完成英文分词。
按空格切分无限次:

a = 'i have a pen'
a1 = a.split(' ')
a1

按空格切分 1 次:

a2 = a.split(' ', 1)
a2

按 have 切分 1 次:

a3 = a.split('have', 1)
a3

实现对多个英文文本分词,要求同时以 , , . , ? , ! , 五个符号分词。
首先对原文本以其中一个规则切分后,再对分好后的文本进行下一个规则的切分,再对分好的文本进行切分,直到按 5 个规则切分完成,最后将切分好的词添加进 tokenized_text 并返回。

def tokenize_english_text(text):
    # 首先,我们按照标点来分句
    # 先建立一个空集用来,用来将分好的词添加到里面作为函数的返回值
    tokenized_text = []
    # 一个 text 中可能不止一个内容,我们对每个文本单独处理并存放在各自的分词结果中。
    for data in text:
        # 建立一个空集来存储每一个文本自己的分词结果,每对 data 一次操作我们都归零这个集合
        tokenized_data = []
        # 以 '.'分割整个句子,对分割后的每一小快 s:
        for s in data.split('.'):
            # 将's'以 '?'分割,分割后的每一小快 s2:
            for s1 in s.split('?'):
                # 同样的道理分割 s2,
                for s2 in s1.split('!'):
                    # 同理
                    for s3 in s2.split(','):
                        # 将 s3 以空格分割,然后将结果添加到 tokenized_data 当中
                        tokenized_data.extend(
                            s4 for s4 in s3.split(' ') if s4 != '')
                        # 括号内的部分拆开理解
                        # for s4 in s3.split(' '):
                        #    if s4!='':  这一步是去除空字符''。注意与' ' 的区别。
        # 将每个 tokenized_data 分别添加到 tokenized_text 当中
        tokenized_text.append(tokenized_data)

    return tokenized_text
a = ['i am a boy?i am a boy ! i am a boy,i', 'god is a girl', 'i love you!']
result = tokenize_english_text(a)
result

中文分词

通过计算机将句子转化成词的表示,自动识别句子中的词,在词与词之间加入边界分隔符,分割出各个词汇。这个切词过程就叫做中文分词。
中文原文: 我们今天要做实验。
中文分词结果:我们 | 今天 | 要 | 做 | 实验 。
由于中文的结构与印欧体系语种有很大的差异。在中文中,文本是由连续的字序列构成,词和词之间没有天然的分隔符。不同分词方法的结果会影响到词性,句法等问题。分词作为一个方法,如果运用场景不同,要求不同,最终对任务达到的效果也不同。

困难之一:歧义。
原文:以前喜欢一个人,现在喜欢一个人
这里有两个「一个人」,但是代表的意思完全不一样。

困难之二:分词界限。
原文:这杯水还没有冷
分词一: 这 | 杯 | 水 | 还 | 没有 | 冷
分词二: 这 | 杯 | 水 | 还没 | 有 | 冷
分词三: 这 | 杯 | 水 | 还没有 | 冷

中文分词这个概念自提出以来,经过多年的发展,主要可以分为三个方法:
机械分词方法;
统计分词方法;
两种结合起来的分词方法。

机械分词方法

机械分词方法又叫做基于规则的分词方法:这种分词方法按照一定的规则将待处理的字符串与一个词表词典中的词进行逐一匹配,若在词典中找到某个字符串,则切分,否则不切分。机械分词方法按照匹配规则的方式,又可以分为:正向最大匹配法,逆向最大匹配法和双向匹配法三种。
正向最大匹配法
正向最大匹配法(Maximum Match Method,MM 法)是指从左向右按最大原则与词典里面的词进行匹配。假设词典中最长词是 m 个字,那么从待切分文本的最左边取 m 个字符与词典进行匹配,如果匹配成功,则分词。如果匹配不成功,那么取 m−1 个字符与词典匹配,一直取直到成功匹配为止。

通过用一个简单的例子来讲一下 MM 法的过程:
句子:中华民族从此站起来了
词典:"中华","民族","从此","站起来了"
使用 MM 法分词:
第一步:词典中最长是 4 个字,所以我们将 【中华民族】 取出来与词典进行匹配,匹配失败。
第二步:于是,去掉 【族】,以 【中华民】 进行匹配,匹配失败
第三步:去掉 【中华民】 中的 【民】,以 【中华】 进行匹配,匹配成功。
第四步:在带切分句子中去掉匹配成功的词,待切分句子变成 【民族从此站起来了】。
第五步:重复上面的第 1 - 4 步骤
第六步:若最后一个词语匹配成功,结束。
最终句子被分成:【中华 | 民族 | 从此 | 站起来了】
逆向最大匹配法
逆向最大匹配法( Reverse Maximum Match Method, RMM 法)的原理与正向法基本相同,唯一不同的就是切分的方向与 MM 法相反。逆向法从文本末端开始匹配,每次用末端的最长词长度个字符进行匹配。
另外,由于汉语言结构的问题,里面有许多偏正短语,即结构是:
定语 + 中心词(名、代):(祖国)大地、(一朵)茶花、(前进)的步伐。
状语 + 中心词(动、形):(很)好看、(独立)思考、(慢慢)地走。
如果采用逆向匹配法,可以适当提高一些精确度。换句话说,使用逆向匹配法要比正向匹配法的误差要小。
双向最大匹配法
双向最大匹配法(Bi-direction Matching Method ,BMM)则是将正向匹配法得到的分词结果与逆向匹配法得到的分词结果进行比较,然后按照最大匹配原则,选取次数切分最少的作为结果。
实现正向最大匹配法
整个正向最大匹配算法的流程图:

image.png

算法步骤:
导入分词词典 dic,待分词文本 text,创建空集 words 。
遍历分词词典,找到最长词的长度,max_len_word 。
将待分词文本从左向右取 max_len=max_len_word 个字符作为待匹配字符串 word 。
将 word 与词典 dic 匹配
若匹配失败,则 max_len = max_len - 1 ,然后
重复 3 - 4 步骤
匹配成功,将 word 添加进 words 当中。
去掉待分词文本前 max_len 个字符
重置 max_len 值为 max_len_word
重复 3 - 8 步骤
返回列表 words
创建一个简单的词典 dic 和测试文本 text 。
首先创建一个测试文本 text:

# 文本
text = '我们是共产主义的接班人'
text

创建一个词典 dic:

# 词典
dic = ('我们', '是', '共产主义', '的', '接班', '人', '你', '我', '社会', '主义')
dic

我们需要遍历词典来求出最长词的长度:

# 初始最长词长度为 0
max_len_word0 = 0
for key in dic:
    # 若当前词长度大于 max_len_word,则将 len(key)值赋值给 max_len_word
    if len(key) > max_len_word0:
        max_len_word0 = len(key)

max_len_word0

通过循环来完成 MM 法:

sent = text
words = []   # 建立一个空数组来存放分词结果:
max_len_word = max_len_word0
# 判断 text 的长度是否大于 0,如果大于 0 则进行下面的循环
while len(sent) > 0:
    # 初始化想要取的字符串长度
    # 按照最长词长度初始化
    word_len = max_len_word
    # 对每个字符串可能会有(max_len_word)次循环
    for i in range(0, max_len_word):
        # 令 word 等于 text 的前 word_len 个字符
        word = sent[0:word_len]
        # 为了便于观察过程,我们打印一下当前分割结果
        print('用 【', word, '】 进行匹配')
        # 判断 word 是否在词典 dic 当中
        # 如果不在词典当中
        if word not in dic:
            # 则以 word_len - 1
            word_len -= 1
            # 清空 word
            word = []
        # 如果 word 在词典当中
        else:
            # 更新 text 串起始位置
            sent = sent[word_len:]
            # 为了方便观察过程,我们打印一下当前结果
            print('【{}】 匹配成功,添加进 words 当中'.format(word))
            print('-'*50)
            # 把匹配成功的word添加进上面创建好的words当中
            words.append(word)
            # 清空word
            word = []

统计分词方法

基于的字典的方法实现比较简单,而且性能也还不错。但是其有一个缺点,那就是不能切分 未登录词 ,也就是不能切分字典里面没有的词。为解决这一问题,于是有学者提出了基于统计的分词方法。
目前基于统计的分词方法大体可以分两种方法:
语料统计方法
序列标注方法
语料统计方法
对于语料统计方法可以这样理解:我们已经有一个由很多个文本组成的的语料库 D ,假设现在对一个句子【我有一个苹果】进行分词。其中两个相连的字 【苹】【果】在不同的文本中连续出现的次数越多,就说明这两个相连字很可能构成一个词【苹果】。与此同时 【个】【苹】 这两个相连的词在别的文本中连续出现的次数很少,就说明这两个相连的字不太可能构成一个词【个苹】。所以,我们就可以利用这个统计规则来反应字与字成词的可信度。当字连续组合的概率高过一个临界值时,就认为该组合构成了一个词语。
序列标注方法
序列标注方法则将中文分词看做是一个序列标注问题。首先,规定每个字在一个词语当中有着 4 个不同的位置,词首 B,词中 M,词尾 E,单字成词 S。我们通过给一句话中的每个字标记上述的属性,最后通过标注来确定分词结果。

例如:我今天要去实验室
标注后得到:我/S 今/B 天/E 要/S 去/S 实/B 验/M 室/E
标注序列是:S B E S S B M E
找到 S 、B 、 E 进行切词:S / B E / S / S / B M E /
所以得到的切词结果是:我 / 今天 / 要 / 去 / 实验室
在训练时,输入中文句子和对应的标注序列,训练完成得到一个模型。在测试时,输入中文句子,通过模型运算得到一个标注序列。然后通过标注序列来进行切分句子。
在统计学习方法中,可以用于序列标注任务的方法有很多。例如,隐马尔可夫模型,条件随机场等。
可以在 Python 用第三方的中文分词工具 jieba ,来替我们在实际应用中省掉训练分词隐马可夫模型的繁琐步骤。并且,jieba 工具用的是隐马尔可夫模型与字典相结合的方法,比直接单独使用隐马尔可夫模型来分词效率高很多,准确率也高很多。
目前在实际应用中,jieba 分词是使用率很高的一款工具。不仅使用起来十分的方便、快速,而且分词效果也比较理想。

在使用 jieba 进行分词时,有三种模式可选:
全模式
精确模式
搜索引擎模式

import jieba

全模式:

string = '我来到北京清华大学'
seg_list = jieba.cut(string, cut_all=True)

jieba 是将分词后的结果存放在生成器当中的。

seg_list

无法直接显示,若想要显示,可以下面这样。用 ‘|’ 把生成器中的词串起来显示。这个方法在下面提到的精确模式和搜索引擎模式中同样适用。

seg_list = jieba.cut(string, cut_all=False)
'|'.join(seg_list)

搜索引擎模式:

seg_list = jieba.cut_for_search(string)
'|'.join(seg_list)

全模式和搜索引擎模式,jieba 会把全部可能组成的词都打印出来。在一般的任务当中,我们使用默认的精确模式就行了,在模糊匹配时,则需要用到全模式或者搜索引擎模式。
我们试着对一篇长文本作分词。首先,导入某一段文本。

text = '市场有很多机遇但同时也充满杀机,野蛮生长和快速发展中如何慢慢稳住底盘,驾驭风险,保持起伏冲撞在合理的范围,特别是新兴行业,领军企业更得有胸怀和大局,需要在竞争中保持张弛有度,促成行业建立同盟和百花争艳的健康持续的多赢局面,而非最后比的是谁狠,比的是谁更有底线,劣币驱逐良币,最终谁都逃不了要还的。'
text

适用精确模式对文本进行分词:

a = jieba.cut(text, cut_all=False)
'|'.join(a)

jieba 在某些特定的情况下分词,可能表现不是很好。比如一篇非常专业的医学论文,含有一些特定领域的专有名词。不过,为了解决此类问题, jieba 允许用户自己添加该领域的自定义词典,我们可以提前把这些词加进自定义词典当中,来增加分词的效果。调用的方法是:jieba.load_userdic()。
自定义词典的格式要求每一行一个词,有三个部分,词语,词频(词语出现的频率),词性(名词,动词……)。其中,词频和词性可省略。用户自定义词典可以直接用记事本创立即可,但是需要以 utf-8 编码模式保存。 格式像下面这样:

凶许 1 a
脑斧 2 b
福蝶 c
小局 4 
海疼

除了使用 jieba.load_userdic() 函数在分词开始前加载自定义词典之外,还有两种方法在可以在程序中动态修改词典。
使用 add_word(word, freq=None, tag=None) 和 del_word(word) 可在程序中动态修改词典。
使用 suggest_freq(segment, tune=True) 可调节单个词语的词频,使其能(或不能)被分出来。
使用自定义词典,有时候可以取得更好的效果,例如「今天天气不错」这句话,本应该分出「今天」、「天气」、「不错」三个词,而来看一下直接使用结巴分词的结果:

string = '今天天气不错'
seg_list = jieba.cut(string, cut_all=False)
'|'.join(seg_list)

可以看到结果并没有被完整分割,这时候就可以加载自定义的词典了,将「今天」和「天气」两个词语添加到词典中,并重新分词:

jieba.suggest_freq(('今天', '天气'), True)
seg_list = jieba.cut(string, cut_all=False)
'|'.join(seg_list)

也可以从词典直接删除该词语:

jieba.del_word('今天天气')
seg_list = jieba.cut(string, cut_all=False)
'|'.join(seg_list)

还有一种情况是「台中」总是被切成「台」和「中」,因为 P(台中) < P(台)×P(中),“台中”词频不够导致其成词概率较低,这时候可以添加词典,强制调高词频。

string = '台中'
seg_list = jieba.cut(string, cut_all=False)
'|'.join(seg_list)

强制调高「台中」的词频,使它被分为一个词:

jieba.add_word('台中')
seg_list = jieba.cut(string, cut_all=False)
'|'.join(seg_list)

利用 jiaba 来做一个简单过滤器,这个在实际的应用中十分常用。比如有的词【的】,【地】,【得】,对数据分析没有什么实际作用,但是文章中大量的这类词又会占据大量的存储资源,因此我们想要过滤掉这类词。
首先建立停用词表,为了便于理解,我们直接建立一个小型的停用词表。实际中常常需要一个由大量的停用词组成的词表。

stopwords = ('的', '地', '得')
stopwords

自定义待过滤的文本:

string = '我喜欢的和你讨厌地以及最不想要得'
string

对 string 进行分词操作,看看没过滤之前的分词结果;并将结果存放在一个 seg_list 中:

seg_list = jieba.cut(string, cut_all=False)
'|'.join(seg_list)

接下来,查看过滤后结果。首先创建一个空数组来存放过滤后的词语,然后通过循环迭代的方法,将过滤后的词语依次添加到刚刚建立的空数组当中。

a = []
seg_list = jieba.cut(string, cut_all=False)
for word in seg_list:
    if word not in stopwords:
        a.append(word)
a

中文邮件文本分类实战

文本分类简介

一般而言,文本分类是指在一定的规则下,根据内容自动确定文本类别这一过程。文本分类在实际场景中有诸多方面的应用,比如常见的有垃圾邮件分类,情感分析等,新闻分类等等。

按照分类要求的不同,文本分类主要可以分为二分类,多分类,多标签分类三大类。
二分类问题:也是最基础的分类,顾名思义是将文本归为两种类别,比如将正常邮件邮件划分问题,垃圾邮件或者正常邮件。一段影评,判断是好评还是差评的问题。
多分类问题:是将文本划分为多个类别,比如将新闻归为政治类,娱乐类,生活类等等。
多标签分类:是给文本贴上多个不同的标签,比如一部小说可以同时被划分为多个主题,可能既是修仙小说,又是玄幻小说。

文本分类主要有两种方法:传统机器学习文本分类算法、深度学习文本分类算法。
传统方法:特征提取 + 分类器。就是将文本转换成固定维度的向量,然后送到分类器中进行分类。
深度学习方法:可以自动提取特征,实现端到端的训练,有较强的特征表征能力,所以深度学习进行文本分类的效果往往要好于传统的方法。

支持向量机

SVM 作为传统机器学习的一个非常重要的分类算法,给定训练样本,支持向量机找到一个划分超平面,将不同的类别划分开来。通俗来讲,这样的超平面有很多,支持向量机就是要找到位于两类训练样本「正中间」的划分超平面。为了便于理解,我们用了下面图中这个简单的二维空间例子来讲解支持向量机的基本原理,实际应用中常常是复杂的高维空间。


image.png

可以看到,图中有很多的线都可以正确将样本划分为两个类别,但是红色的线位于两个样本的「正中间」位置。因为受噪声和训练集局限性的因素呢,训练集外的样本可能比训练集的样本更接近两个类别的分界,这样就会导致分类错误。恰恰红色这条线受影响最小,支持向量机的目的就是要找到这条红线。


image.png

图中红线为最大分类间隔超平面,虚线上的点是距离分界面最近的点集,这类点就称为 「支持向量」。
超平面用线性方程式表示为:
image.png

其中


image.png

是权重值,决定了超平面的方向; b 是位移项,决定了超平面与原点之间的距离。
在空间中,任意一点xi到超平面的距离 r 公式为:
image.png

其中,∣ω∣ 是 ω 的 L2 范数,L2范数:比如 ω=(a,b,c), 那么
image.png

当且仅当,点在两个异类支持向量集合上,分别使等号成立。此时两个支持向量之间的「间隔」为:
image.png

显然为了最大化间隔,我们需要找到最小的∣ω∣^ 2,这就是支持向量机的基本型。

中文邮件分类

垃圾邮件分类任务整个实验步骤大致如下:
导入数据,并进行分词和剔除停用词。
划分训练集和测试集。
将文本数据转化为数字特征数据。
构建分类器。
训练分类器。
测试分类器。
数据准备
本次用到的数据包含 3 个文件, ham_data.txt 文件里面包含 5000 条正常邮件样本,spam_data.txt 文件里面包含 5001 个垃圾邮件样本,stopwords 是停用词表。

!wget - nc "http://labfile.oss.aliyuncs.com/courses/1208/ham_data.txt"
!wget - nc "http://labfile.oss.aliyuncs.com/courses/1208/spam_data.txt"
!wget - nc "http://labfile.oss.aliyuncs.com/courses/1208/stop_word.txt"

获得了样本之后,首先要做是给正常邮件和垃圾邮件贴上标签,我们用 1 代表正常邮件,0 代表垃圾邮件。

path1 = 'ham_data.txt'  # 正常邮件存放地址
path2 = 'spam_data.txt'  # 垃圾邮件地址

用 utf-8 编码模式打开正常样本:

h = open(path1, encoding='utf-8')
h

因为我们准备的数据是每一行一封邮件,这里我们要用 readlines() 来以行来读取文本的内容。

h_data = h.readlines()
h_data[0:3]  # 显示前3封正常邮件

同理方式处理垃圾样本:

s = open(path2, encoding='utf-8')
s_data = s.readlines()
s_data[0:3]  # 显示前3个封垃

读取之后,我们的 h_data 数是由 5000 条邮件字符串组成的正常邮件样本集, s_data 是由 5001 条邮件字符串组成的垃圾邮件样本集。下面我们为将两个样本组合起来,并贴上标签,将正常邮件的标签设置为 1,垃圾邮件的标签设置为 0。
生成一个 len(h_data) 长的的一维全 1 列表:

import numpy as np

h_labels = np.ones(len(h_data)).tolist()  # 生成一个len(h_data)长的的一维全1列表
h_labels[0:10]  # 我们显示前10个数据

生成一个 len(s_data) 长的的一维全 0 列表:

s_labels = np.zeros(len(s_data)).tolist()
s_labels[0:10]  # 我们显示前10个数据

拼接样本集和标签集:

datas = h_data + s_data  # 将正常样本和垃圾样本整合到datas当中
labels = h_labels + s_labels

这样我们得到一个由所有邮件样本组合起来的样本集 datas 以及一个标签集 labels。
因为我们没有事先准备测试集,所以我们在 10001 个样本当中,随机划出 25% 个样本和标签来作为我们的测试集,剩下的 75% 作为训练集来进行训练我们的分类器。这里我们可以用到 scikit-learn 工具里面的 train_test_split 类。

sklearn.model_selection.train_test_split(datas, labels, test_size=0.25, random_state=5 )

参数的意义:
datas : 样本集
labels: 标签集
train_test_split:划分到测试集的比例
random_state:随机种子,取同一个的随机种子那么每次划分出的测试集是一样的。

from sklearn.model_selection import train_test_split

train_d, test_d, train_y, test_y = train_test_split(
    datas, labels, test_size=0.25, random_state=5)
train_y[0:30]

分词
现在对文本进行分词,将分词设计成 tokenize_words 函数,供后续直接调用。

import jieba


def tokenize_words(corpus):
    tokenized_words = jieba.cut(corpus)
    tokenized_words = [token.strip() for token in tokenized_words]
    return tokenized_words

验证一下函数:

string = '我爱自然语言处理'
b = tokenize_words(string)
b

去除停用词
因为一些字是没有实际意义的,比如:【的】【了】【得】等,因此要将其剔除。首先加载我们刚刚下载好的停用词表。这里也可以自行在网上下载,编码格式为 utf-8,每行一个停用词。为了方便调用,我们将去除停用词的操作放到 remove_stopwords 函数当中。

def remove_stopwords(corpus):  # 函数输入为样本集
    sw = open('stop_word.txt', encoding='utf-8')  # stopwords 停词表
    sw_list = [l.strip() for l in sw]  # 去掉文本中的回车符,然后存放到 sw_list 当中
    # 调用前面定义好的分词函数返回到 tokenized_data 当中
    tokenized_data = tokenize_words(corpus)
    # 过滤停用词,对每个在 tokenized_data 中的词 data 进行判断
    # 如果 data 不在 sw_list 则添加到 filtered_data 当中
    filtered_data = [data for data in tokenized_data if data not in sw_list]
    # 用''将 filtered_data 串起来赋值给 filtered_datas
    filtered_datas = ' '.join(filtered_data)
    return filtered_datas  # 返回去停用词之后的 datas

构建一个函数完成分词和剔除停用词。这里使用 tqdm 模块显示进度。

from tqdm.notebook import tqdm


def preprocessing_datas(datas):
    preprocessed_datas = []
    # 对 datas 当中的每一个 data 进行去停用词操作
    # 并添加到上面刚刚建立的 preprocessed_datas 当中
    for data in tqdm(datas):
        data = remove_stopwords(data)
        preprocessed_datas.append(data)

    return preprocessed_datas  # 返回去停用词之后的新的样本集

用上面预处理函数对样本集进行处理。

pred_train_d = preprocessing_datas(train_d)
pred_train_d[0]

同样,对测试集进行预处理:

pred_test_d = preprocessing_datas(test_d)
pred_test_d[0]

我们得到了分词过后并且去除停用词了的样本集 pred_train_d 和 测试集 pred_test_d。
特征提取
在进行分词及去停用词处理过后,得到的是一个分词后的文本。现在我们的分类器是 SVM,而 SVM 的输入要求是数值型的特征。这意味着我们要将前面所进行预处理的文本数据进一步处理,将其转换为数值型数据。转换的方法有很多种,为了便于理解,这里使用 TF-IDF 方法。为了更好的理解 TF-IDF,我们先从词袋模型开始讲解。
词袋模型
词袋模型是最原始的一类特征集,忽略掉了文本的语法和语序,用一组无序的单词序列来表达一段文字或者一个文档。可以这样理解,把整个文档集的所有出现的词都丢进袋子里面,然后无序的排出来(去掉重复的)。对每一个文档,按照词语出现的次数来表示文档。

句子1:我/有/一个/苹果
句子2:我/明天/去/一个/地方
句子3:你/到/一个/地方
句子4:我/有/我/最爱的/你
把所有词丢进一个袋子:我,有,一个,苹果,明天,去,地方,你,到,最爱的。这 4 句话中总共出现了这 10 个词。
现在我们建立一个无序列表:我,有,一个,苹果,明天,去,地方,你,到,最爱的。并根据每个句子中词语出现的次数来表示每个句子。

image.png

句子 1 特征: ( 1 , 1 , 1 , 1 , 0 , 0 , 0 , 0 , 0 , 0 )
句子 2 特征: ( 1 , 0 , 1 , 0 , 1 , 1 , 1 , 0 , 0 , 0 )
句子 3 特征: ( 0 , 0 , 1 , 0 , 0 , 0 , 1 , 1 , 1 , 0 )
句子 4 特征: ( 2 , 1 , 0 , 0 , 0 , 0 , 0 , 1 , 0 , 1 )
这样的一种特征表示,我们就称之为词袋模型的特征。
TF-IDF 模型
这种模型主要是用词汇的统计特征来作为特征集。TF-IDF 由两部分组成:TF(Term frequency,词频),IDF(Inverse document frequency,逆文档频率)两部分组成。
TF:
image.png

其中分子nij表示词i 在文档j 中出现的频次。分母则是所有词频次的总和,也就是所有词的个数。

举个例子:
句子1:上帝/是/一个/女孩
句子2:桌子/上/有/一个/苹果
句子3:小明/是/老师
句子4:我/有/我/最喜欢/的/
每个句子中词语的 TF :


image.png

IDF:


image.png

其中∣D∣ 代表文档的总数,分母部分∣Di∣ 则是代表文档集中含有i词的文档数。原始公式是分母没有+1的,这里+1是采用了拉普拉斯平滑,避免了有部分新的词没有在语料库中出现而导致分母为零的情况出现。
用 IDF 计算公式计算句子中每个词的 IDF 值:
image.png

最后,把 TF 和 IDF 两个值相乘就可以得到 TF-IDF 的值。即:
image.png

每个句子中,词语的 TF-IDF 值:
image.png

把每个句子中每个词的 TF-IDF 值 添加到向量表示出来就是每个句子的 TF-IDF 特征。
句子 1 的特征:


image.png

同样的方法得到句子 2,3,4 的特征。
在 Python 当中,我们可以通过 scikit-learn 来实现 TF-IDF 模型。这里主要用到了 TfidfVectorizer() 类。
sklearn.feature_extraction.text.TfidfVectorizer(min_df=1,norm='l2',smooth_idf=True,use_idf=True,ngram_range=(1,1))

min_df: 忽略掉词频严格低于定阈值的词。
norm :标准化词条向量所用的规范。
smooth_idf:添加一个平滑 IDF 权重,即 IDF 的分母是否使用平滑,防止 0 权重的出现。
use_idf: 启用 IDF 逆文档频率重新加权。
ngram_range:同词袋模型

首先加载 TfidfVectorizer 类,并定义 TF-IDF 模型训练器 vectorizer 。

from sklearn.feature_extraction.text import TfidfVectorizer

vectorizer = TfidfVectorizer(
    min_df=1, norm='l2', smooth_idf=True, use_idf=True, ngram_range=(1, 1))

对预处理过后的 pred_train_d 进行特征提取:

tfidf_train_features = vectorizer.fit_transform(pred_train_d)
tfidf_train_features

通过这一步,我们得到了 7500 个 28335 维数的向量作为我们的训练特征集。我们可以查看转换结果,这里为了便于观察,使用 toarray 方法转换成为数组数据。

tfidf_train_features.toarray()[0]

用训练集训练好特征后的 vectorizer 来提取测试集的特征: 注意这里不能用 vectorizer.fit_transform() 要用 vectorizer.transform(),否则,将会对测试集单独训练 TF-IDF 模型,而不是在训练集的词数量基础上做训练。这样词总量跟训练集不一样多,排序也不一样,将会导致维数不同,最终无法完成测试。

tfidf_test_features = vectorizer.transform(pred_test_d)
tfidf_test_features

完成之后,我们得到 2501 个 28335 维数的向量作为我们的测试特征集。
分类
在获得 TF-IDF 特征之后,我们可以调用 SGDClassifier() 类来训练 SVM 分类器。

sklearn.linear_model.SGDClassifier(loss='hinge')

SGDClassifier 是一个多个分类器的组合,当参数 loss='hinge' 时是一个支持向量机分类器。
加载 SVM 分类器,并调整 loss = 'hinge'。

from sklearn.linear_model import SGDClassifier

svm = SGDClassifier(loss='hinge')

然后我们将之前准备好的样本集和样本标签送进 SVM 分类器进行训练。

svm.fit(tfidf_train_features, train_y)

接下来我们用测试集来测试一下分类器的效果。

predictions = svm.predict(tfidf_test_features)
predictions

为了直观显示分类的结果,我们用 scikit-learn 库中的 accuracy_score 函数来计算一下分类器的准确率 。

sklearn.metrics.accuracy_score(test_l, prediction)

这个函数的作用是为了计算 test_l 中与 prediction 相同的比例。即准确率。
用测试标签和预测结果 计算分类准确率。np.round(X,2) 的作用是 X 四舍五入后保留小数点后 2 位数字。

from sklearn import metrics

accuracy_score = np.round(metrics.accuracy_score(test_y, predictions), 2)
accuracy_score

随机提取一个样本查看其预测结果:

print('邮件类型:', test_y[20])
print('预测邮件类型:', predictions[20])
print('文本:', test_d[20])

你可能感兴趣的:(13自然语言处理基础入门)