文本的预处理
1、概述
文本语料在输送给模型前一般需要一系列的预处理工作, 才能符合模型输入的要求, 如: 将文本转化成模型需要的张量, 规范张量的尺寸等, 而且科学的文本预处理环节还将有效指导模型超参数的选择, 提升模型的评估指标.
2、文本处理的基本方法
(一)、分词
(1)、概述
分词就是将连续的字序列,按照一定的规范,重新组合成词序列的过程
一般实现模型训练的时候,模型接受的文本基本最小单位是词语,因此我们需要对文本进行分词
词语是语意理解的基本单元
英文具有天然的空格分隔符,而中文分词的目的:寻找一个合适的分词边界,进行准确分词
(2)、jieba分词
'''
前面放句子参数
cut_all:默认是Flase
'''
jieba.cut(句子, cut_all) # 返回生成器
jieba.lcut(句子, cut_all) # 返回列表
<一>、精准模式(默认首选)
按照人类擅长的表达词汇的习惯来分词,试图将句子最精确地切开,适合文本分析
import jieba
content = "我喜欢学习"
jieba.cut(content, cut_all=False) # 返回生成器,cut_all=False默认
jieba.lcut(content, cut_all=False) # 返回列表
<二>、全模式分词
将尽可能成词的词汇分割出来,速度非常快,但是不能消除歧义
import jieba
content = "我喜欢学习"
jieba.cut(content, cut_all=True) # 返回生成器
jieba.lcut(content, cut_all=True) # 返回列表
<三>、搜索引擎模式
在精确模式分词的基础上,将长粒度的词再次切分,提高召回率,适合用于搜索引擎分词
import jieba
content = "我喜欢学习"
jieba.cut_for_search(content) # 返回生成器
jieba.lcut_for_search(content) # 返回列表结果
<四>、自定义词典
自定义词典格式:
可以根据自定义词典,修改jieba分词方式,优先考虑词典里面的词来切分
格式:词语 词频(可省略) 词性(可省略)
例如:
'''
userdict:
整点薯条
去码头
'''
jieba.load_userdict('userdict.txt')
res5 = jieba.lcut(data, cut_all=False)
print(f'自定义词典分词结果:{res5}')
<五>、支持中文繁体分词
import jieba
content = "煩惱即是菩提,我暫且不提"
jieba.lcut(content)
# ['煩惱', '即', '是', '菩提', ',', '我', '暫且', '不', '提']
<六>、支持文言文
import jieba
content = '庭有枇杷树,吾妻死之年所手植也,今已亭亭如盖矣'
res3 = jieba.lcut(content)
print(content)
# ['庭有', '枇杷树', ',', '吾妻', '死', '之', '年', '所', '手植', '也', ',', '今', '已', '亭亭', '如盖', '矣']
(二)、通用信息抽取技术
(1)、命名实体识别(NER)
命名实体识别(NER):从文本中识别并分类命名实体,如人名、地名、组织名、时间、日期、货币、百分比。
import sys
sys.path.append(r'D:\python\code\NLP\NLP_proctice\uie_pytorch')
from uie_predictor import UIEPredictor
from pprint import pprint
data_3 = ['时间', '人物', '地点']
ie = UIEPredictor(model='uie-base', schema=data_3)
pprint(ie('我们将要去往何方?去码头整点薯条!'))
(2)、关系抽取(RE)
关系抽取(RE):识别文本中实体之间的关系,如“张三工作于阿里云”中的“工作于”关系。
(3)、事件抽取(EE)
事件抽取(EE):从文本中识别特定类型的事件,如灾害/意外、公司上市等。
(4)、属性抽取
属性抽取:提取实体的特定属性,如产品的价格、颜色等。
(三)、词性标注(POS)
(1)、概述
对每个词语进行词性的标注:动词、名词、形容词等。词性标注以分词为基础, 是对文本语言的另一个角度的理解, 因此也常常成为AI解决NLP领域高阶任务的重要基础环节。
(2)、实现方式
import jieba.posseg as pseg
# 词性标注
print(pseg.lcut("我们将要去往何方?去码头整点薯条!"))
(四)、总结
- 分词:
- 分词就是将连续的字序列按照一定的规范重新组合成词序列的过程。我们知道,在英文的行文中,单词之间是以空格作为自然分界符的,而中文只是字、句和段能通过明显的分界符来简单划界,唯独词没有一个形式上的分界符, 分词过程就是找到这样分界符的过程.
- 分词的作用:
- 词作为语言语义理解的最小单元, 是人类理解文本语言的基础. 因此也是AI解决NLP领域高阶任务, 如自动问答, 机器翻译, 文本生成的重要基础环节.
- 中文分词工具jieba:
- 支持多种分词模式: 精确模式, 全模式, 搜索引擎模式
- 支持中文繁体分词
- 支持用户自定义词典
- 命名实体识别:
- 命名实体: 通常我们将人名, 地名, 机构名等专有名词统称命名实体. 如: 周杰伦, 黑山县, 孔子学院, 24辊方钢矫直机.
- 顾名思义, 命名实体识别(Named Entity Recognition,简称NER)就是识别出一段文本中可能存在的命名实体.
- 命名实体识别的作用:
- 同词汇一样, 命名实体也是人类理解文本的基础单元, 因此也是AI解决NLP领域高阶任务的重要基础环节.
- 词性标注:
- 词性: 语言中对词的一种分类方法,以语法特征为主要依据、兼顾词汇意义对词进行划分的结果, 常见的词性有14种, 如: 名词, 动词, 形容词等.
- 顾名思义, 词性标注(Part-Of-Speech tagging, 简称POS)就是标注出一段文本中每个词汇的词性.
- 词性标注的作用:
- 词性标注以分词为基础, 是对文本语言的另一个角度的理解, 因此也常常成为AI解决NLP领域高阶任务的重要基础环节.
3、文本张量表示方法
(一)、文本张量表示
(1)、概述
将一段文本使用张量进行表示,其中一般将词汇表示成向量,称作词向量,再由各个词向量按顺序组成矩阵形成文本表示。
(2)、作用
将文本表示成张量(矩阵)形式,能够使语言文本可以作为计算机处理程序的输入,进行接下来一系列的解析工作。
(二)、one-hot词向量表示
(1)、概述
独热编码,将每个词表示成具有n个元素的向量,这个词向量中只有一个元素是1,其他元素都是0,不同词汇元素为0的位置不同,其中n的大小是整个语料中不同词汇的总数。
(2)、实现进行编码
def onehot_get(sent):
# 准备材料
vocabs = list(set(jieba.lcut(sent)))
# 实例化Tokenizer
mytokenizer = Tokenizer()
mytokenizer.fit_on_texts(vocabs)
# 查询词的index,赋值zero_list,生成one-hot,记得要索引-1,因为Tokenizer()是从1开始的
for vocab in vocabs:
zero_list = [0] * len(vocabs)
idx = mytokenizer.word_index[vocab] - 1
zero_list[idx] = 1
print(vocab, '的onehot编码为', zero_list)
# 使用joblib保存模型
mypath = 'data/onehot_model.pkl'
joblib.dump(mytokenizer, mypath)
print('保存成功')
# 打印词->序号和序号->词的映射关系
print('词->序号的映射关系:', mytokenizer.word_index)
print('序号->词的映射关系:', mytokenizer.index_word)
if __name__ == '__main__':
sent = '庭有枇杷树,吾妻死之年所手植也,今已亭亭如盖矣'
onehot_get(sent)
结果:
手植 的onehot编码为 [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
也 的onehot编码为 [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
今 的onehot编码为 [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
矣 的onehot编码为 [0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
亭亭 的onehot编码为 [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
枇杷树 的onehot编码为 [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]
吾妻 的onehot编码为 [0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0]
死 的onehot编码为 [0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0]
年 的onehot编码为 [0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0]
所 的onehot编码为 [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0]
, 的onehot编码为 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0]
庭有 的onehot编码为 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0]
已 的onehot编码为 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0]
如盖 的onehot编码为 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0]
之 的onehot编码为 [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]
词->序号的映射关系: {'手植': 1, '也': 2, '今': 3, '矣': 4, '亭亭': 5, '枇杷树': 6, '吾妻': 7, '死': 8, '年': 9, '所': 10, ',': 11, '庭有': 12, '已': 13, '如盖': 14, '之': 15}
序号->词的映射关系: {1: '手植', 2: '也', 3: '今', 4: '矣', 5: '亭亭', 6: '枇杷树', 7: '吾妻', 8: '死', 9: '年', 10: '所', 11: ',', 12: '庭有', 13: '已', 14: '如盖', 15: '之'}
(3)、实现编码器的使用
def onehot_use(word):
# 加载词汇映射器
path = 'data/onehot_model.pkl'
mytokenizer = joblib.load(path)
# 查询单词的idx,如果查不到就说明不在
try:
idx = mytokenizer.word_index[word] - 1
except:
print(f'“{word}”不在词汇表中')
return
# 将索引赋值给zero_list,获得独热编码
zero_list = [0] * len(mytokenizer.word_index)
zero_list[idx] = 1
print(word, '的onehot编码为', zero_list)
if __name__ == '__main__':
onehot_use('枇杷树')
onehot_use('派大星')
结果:
枇杷树 的onehot编码为 [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]
“派大星”不在词汇表中
(4)、小结
- 优势:操作简单,容易理解.
- 劣势:完全割裂了词与词之间的联系,而且在大语料集下,每个向量的长度过大,占据大量内存.
- 正因为one-hot编码明显的劣势,这种编码方式被应用的地方越来越少,取而代之的是接下来我们要学习的稠密向量的表示方法word2vec和word embedding
(三)、word2vec模型
(1)、概述
word2vec是一种流行的将词汇表示成向量的无监督训练方法, 该过程将构建神经网络模型, 将网络参数作为词汇的向量表示, 它包含CBOW和skipgram两种训练模式
- CBOW(Continuous bag of words)模式:俗称连续词带模式。给定一段用于训练的文本语料, 再选定某段长度(窗口)作为研究对象, 使用上下文词汇预测目标词汇。也可以理解为使用目标词汇对上下文词向量进行更新。
- skipgram模式:俗称跳词模式。给定一段用于训练的文本语料, 再选定某段长度(窗口)作为研究对象, 使用目标词汇预测上下文词汇。也可以理解为使用上下文对目标词向量进行更新。
(2)、CBOW模式
先对数据源进行分词,例如“去码头整点薯条。”,可以被分成“去”、“码头”、“整点”、“薯条”、“。”,对每一个token进行编号{去:0,码头:1,整点:2,薯条:3,。:4},获得相对应的独热编码:去->[1,0,0,0,0],一直到。->[0,0,0,0,1],本次我们选取窗口为3,规定embedding_dim=4,也就是一个长度为4的词向量。第一个样本为【去,码头,整点】,第二个样本为【码头,整点,薯条】,第三个样本为【整点,薯条,。】。
然后就对数据做上图处理:
- 先获得上下文token的独热编码,乘上一个权重矩阵(也就相当于第一层线性层,而且此权重矩阵最终的列就是对应的词向量),获得到对应上下文token的词向量矩阵
- 这两个词向量相加取均值,获得一个向量,该向量再次乘以一个权重矩阵(相当于第二层线性层),将其转化为一个和目标独热编码向量同维度的向量
- 对该向量做softmax概率值处理,获得到最终的概率向量,与目标独热编码做交叉熵求出损失值
- 利用该损失值,反向传播修正上面两层权重矩阵的值,使得最终损失值较小
(3)、skip-gram模式
同理,该模式也需要先对数据源进行分词,例如“去码头整点薯条。”,可以被分成“去”、“码头”、“整点”、“薯条”、“。”,对每一个token进行编号{去:0,码头:1,整点:2,薯条:3,。:4},获得相对应的独热编码:去->[1,0,0,0,0],一直到。->[0,0,0,0,1],本次我们选取窗口为3,规定embedding_dim=4,也就是一个长度为4的词向量。第一个样本为【去,码头,整点】,第二个样本为【码头,整点,薯条】,第三个样本为【整点,薯条,。】。
然后对数据做上图处理(只表示了对前面一个token的修正,后一个token同理):
-
先获得目标token的独热编码,乘上一个权重矩阵(也就相当于第一层线性层,而且此权重矩阵最终的列就是对应的词向量),获得到目标token的词向量矩阵
-
将该词向量再次乘上一个权重矩阵(相当于第二层线性层),将其转化为一个和上下文独热编码向量同维度的向量
-
对该向量做softmax概率值处理,获得到最终的概率向量,与目标独热编码做交叉熵求出损失值
-
利用该损失值,反向传播修正上面两层权重矩阵的值,使得最终损失值较小
(4)、实现
<一>、获取训练数据
数据来源:http://mattmahoney.net/dc/enwik9.zip
原数据处理:
# 使用wikifil.pl文件处理脚本来清除XML/HTML格式的内容
# perl wikifil.pl data/enwik9 > data/fil9 #该命令已经执行
<二>、训练词向量
'''
# 训练词向量工具库的安装
# 方法1 简洁版
pip install fasttext
# 方法2:源码安装(推荐)
# 以linux安装为例: 目录切换到虚拟开发环境目录下,再执行git clone 操作
git clone https://github.com/facebookresearch/fastText.git
cd fastText
# 使用pip安装python中的fasttext工具包
sudo pip install .
'''
import fasttext
def fasttext_train():
my_model = fasttext.train_unsupervised('data/fil9')
print('训练词向量ok')
<三>、模型超参数设定
# 在训练词向量过程中, 我们可以设定很多常用超参数来调节我们的模型效果, 如:
# 无监督训练模式: 'skipgram' 或者 'cbow', 默认为'skipgram', 在实践中,skipgram模式在利用子词方面比cbow更好.
# 词嵌入维度dim: 默认为100, 但随着语料库的增大, 词嵌入的维度往往也要更大.
# 数据循环次数epoch: 默认为5, 但当你的数据集足够大, 可能不需要那么多次.
# 学习率lr: 默认为0.05, 根据经验, 建议选择[0.01,1]范围内.
# 使用的线程数thread: 默认为12个线程, 一般建议和你的cpu核数相同.
def fasttext_train_args():
model = fasttext.train_unsupervised('data/fil9', "cbow", dim=300, epoch=1, lr=0.1, thread=8)
<四>、模型效果检验
# 通过get_word_vector方法来获得指定词汇的词向量, 默认词向量训练出来是1个单词100特征
def fasttext_get_word_vector():
my_model = fasttext.load_model('data/fil9.bin')
result = my_model.get_word_vector('cat')
print(f'cat的词向量为:{result}')
print(f'cat的词向量类型为:{type(result)}')
print(f'cat的词向量形状为:{result.shape}')
# 检查单词向量质量的一种简单方法就是查看其邻近单词, 通过我们主观来判断这些邻近单词是否与目标单词相关来粗略评定模型效果好坏
def fasttest_get_nearest_neighbors():
my_model = fasttext.load_model('data/fil9.bin')
result = my_model.get_nearest_neighbors('cat')
print(f'cat的近义词为{result}')
result01 = my_model.get_nearest_neighbors('dog')
print(f'dog的近义词为{result01}')
<五>、模型的保存与重加载
def fasttext_save_load():
my_model.save_model('data/fil9.bin')
print('保存模型ok')
my_model = fasttext.load_model('data/fil9.bin')
print('加载模型ok')
(5)、小结
<一>、CBOW模式
- 优点:
- 训练速度快:因为 CBOW 是基于上下文预测目标词,所以它的训练速度通常比 Skip-Gram 快。
- 对低频词的处理较好:由于 CBOW 使用多个上下文词来预测目标词,因此在处理低频词时表现更好。
- 缺点:
- 难以捕捉词序信息:CBOW 将上下文词视为一个“袋子”,不考虑词的顺序,因此难以捕捉到词序信息。
- 对高频词敏感:CBOW 对高频词的依赖较大,可能会导致模型对高频词过拟合。
<二>、skip-gram模式
-
优点:
- 能更好地捕捉词序信息:Skip-Gram 是基于目标词预测上下文词,因此能够更好地捕捉词序信息。
- 对低频词的表示更准确:Skip-Gram 在处理低频词时通常能生成更好的词向量表示。
-
缺点:
- 训练速度慢:由于 Skip-Gram 需要为每个目标词生成多个上下文词的预测,因此训练速度通常比 CBOW 慢。
- 计算复杂度高:Skip-Gram 的计算复杂度较高,尤其是在大规模语料库上训练时。
<三>、区别
- 目标和输入不同:
CBOW:输入是上下文词,目标是预测目标词。
Skip-Gram:输入是目标词,目标是预测上下文词。 - 处理词序的方式不同:
CBOW:将上下文词视为一个“袋子”,不考虑词的顺序。
Skip-Gram:考虑词的顺序,能够更好地捕捉词序信息。 - 适用场景不同:
CBOW:适用于需要快速训练且对低频词有较好处理的场景。
Skip-Gram:适用于需要更准确的词向量表示且对词序信息有较高要求的场景。 - 模型复杂度不同:
CBOW:模型相对简单,训练速度快。
Skip-Gram:模型复杂度高,训练速度慢。
(四)、word_encodding
(1)、概述
- 通过一定的方式将词汇映射到指定维度(一般是更高维度)的空间.
- 广义的word embedding包括所有密集词汇向量的表示方法,如之前学习的word2vec, 即可认为是word embedding的一种.
- 狭义的word embedding是指在神经网络中加入的embedding层, 对整个网络进行训练的同时产生的embedding矩阵(embedding层的参数), 这个embedding矩阵就是训练过程中所有输入词汇的向量表示组成的矩阵
(2)、可视化实现
import jieba
import torch.nn as nn
from tensorflow.keras.preprocessing.text import Tokenizer
from torch.utils.tensorboard import SummaryWriter
import torch
def embadding_show():
# 获取语料
sent1 = '我们将要飞往何方'
sent2 = '我准备过会去码头整点薯条'
sents = [sent1, sent2]
# 分词获得word_list,未去重
word_list = []
for s in sents:
word_list.append(jieba.lcut(s))
print(f'word_list:{word_list}')
# 实例化 Tokenizer
my_tokenizer = Tokenizer()
my_tokenizer.fit_on_texts(word_list)
print(f'my_tokenizer.word_index:{my_tokenizer.word_index}')
print(f'my_tokenizer.index_word:{my_tokenizer.index_word}')
# 获得词表
my_token_list = my_tokenizer.index_word.values()
print(f'my_token_list:<