第一章:文本预处理

1.1 认识文本预处理


学习目标


文本预处理 - 图1

文本预处理及其作用

  • 文本语料在输送给模型前一般需要一系列的预处理工作, 才能符合模型输入的要求, 如: 将文本转化成模型需要的张量, 规范张量的尺寸等, 而且科学的文本预处理环节还将有效指导模型超参数的选择, 提升模型的评估指标.

文本预处理中包含的主要环节

  • 文本处理的基本方法
  • 文本张量表示方法
  • 文本语料的数据分析
  • 文本特征处理
  • 数据增强方法

文本处理的基本方法

  • 分词
  • 词性标注
  • 命名实体识别

文本张量表示方法

  • one-hot编码
  • Word2vec
  • Word Embedding

文本语料的数据分析

  • 标签数量分布
  • 句子长度分布
  • 词频统计与关键词词云

文本特征处理

  • 添加n-gram特征
  • 文本长度规范

数据增强方法

  • 回译数据增强法

重要说明

  • 在实际生产应用中, 我们最常使用的两种语言是中文和英文, 因此, 文本预处理部分的内容都将针对这两种语言进行讲解.

1.2 文本处理的基本方法


学习目标

  • 了解什么是分词, 词性标注, 命名实体识别及其它们的作用.
  • 掌握分词, 词性标注, 命名实体识别流行工具的使用方法.

什么是分词

  • 分词就是将连续的字序列按照一定的规范重新组合成词序列的过程。我们知道,在英文的行文中,单词之间是以空格作为自然分界符的,而中文只是字、句和段能通过明显的分界符来简单划界,唯独词没有一个形式上的分界符, 分词过程就是找到这样分界符的过程.

  • 举个栗子:


工信处女干事每月经过下属科室都要亲口交代24口交换机等技术性器件的安装工作

==>

[‘工信处’, ‘女干事’, ‘每月’, ‘经过’, ‘下属’, ‘科室’, ‘都’, ‘要’, ‘亲口’, ‘交代’, ‘24’, ‘口’, ‘交换机’, ‘等’, ‘技术性’, ‘器件’, ‘的’, ‘安装’, ‘工作’]


  • 分词的作用:
    • 词作为语言语义理解的最小单元, 是人类理解文本语言的基础. 因此也是AI解决NLP领域高阶任务, 如自动问答, 机器翻译, 文本生成的重要基础环节.

  • 流行中文分词工具jieba:
    • 愿景: “结巴”中文分词, 做最好的 Python 中文分词组件.

  • jieba的特性:

    • 支持多种分词模式

      • 精确模式
      • 全模式
      • 搜索引擎模式
    • 支持中文繁体分词

    • 支持用户自定义词典

  • jieba的安装:


pip install jieba


  • jieba的使用:
    • 精确模式分词:
    • 试图将句子最精确地切开,适合文本分析.


>>> import jieba
>>> content = “工信处女干事每月经过下属科室都要亲口交代24口交换机等技术性器件的安装工作”
>>> jieba.cut(content, cut_all=False) # cut_all默认为False

将返回一个生成器对象

若需直接返回列表内容, 使用jieba.lcut即可
>>> jieba.lcut(content, cut_all=False)
[‘工信处’, ‘女干事’, ‘每月’, ‘经过’, ‘下属’, ‘科室’, ‘都’, ‘要’, ‘亲口’, ‘交代’, ‘24’, ‘口’, ‘交换机’, ‘等’, ‘技术性’, ‘器件’, ‘的’, ‘安装’, ‘工作’]

  • 全模式分词:
  • 把句子中所有的可以成词的词语都扫描出来, 速度非常快,但是不能消除 歧义.


>>> import jieba
>>> content = “工信处女干事每月经过下属科室都要亲口交代24口交换机等技术性器件的安装工作”
>>> jieba.cut(content, cut_all=True) # cut_all默认为False

将返回一个生成器对象

若需直接返回列表内容, 使用jieba.lcut即可
>>> jieba.lcut(content, cut_all=True)
[‘工信处’, ‘处女’, ‘女干事’, ‘干事’, ‘每月’, ‘月经’, ‘经过’, ‘下属’, ‘科室’, ‘都’, ‘要’, ‘亲口’, ‘口交’, ‘交代’, ‘24’, ‘口交’, ‘交换’, ‘交换机’, ‘换机’, ‘等’, ‘技术’, ‘技术性’, ‘性器’, ‘器件’, ‘的’, ‘安装’, ‘安装工’, ‘装工’, ‘工作’]


  • 搜索引擎模式分词:
  • 在精确模式的基础上,对长词再次切分,提高召回率,适合用于搜索引擎分词.


>>> import jieba
>>> content = “工信处女干事每月经过下属科室都要亲口交代24口交换机等技术性器件的安装工作”
>>> jieba.cut_for_search(content)

将返回一个生成器对象

若需直接返回列表内容, 使用jieba.lcut_for_search即可
>>> jieba.lcut_for_search(content)
[‘工信处’, ‘干事’, ‘女干事’, ‘每月’, ‘经过’, ‘下属’, ‘科室’, ‘都’, ‘要’, ‘亲口’, ‘交代’, ‘24’, ‘口’, ‘交换’, ‘换机’, ‘交换机’, ‘等’, ‘技术’, ‘技术性’, ‘器件’, ‘的’, ‘安装’, ‘工作’]

对’女干事’, ‘交换机’等较长词汇都进行了再次分词.


  • 中文繁体分词:
  • 针对中国香港, 台湾地区的繁体文本进行分词.


>>> import jieba
>>> content = “煩惱即是菩提,我暫且不提”
>>> jieba.lcut(content)
[‘煩惱’, ‘即’, ‘是’, ‘菩提’, ‘,’, ‘我’, ‘暫且’, ‘不’, ‘提’]


  • 使用用户自定义词典:
  • 添加自定义词典后, jieba能够准确识别词典中出现的词汇,提升整体的识别准确率.
  • 词典格式: 每一行分三部分:词语、词频(可省略)、词性(可省略),用空格隔开,顺序不可颠倒.
  • 词典样式如下, 具体词性含义请参照附录: jieba词性对照表, 将该词典存为userdict.txt, 方便之后加载使用.


云计算 5 n
李小福 2 nr
easy_install 3 eng
好用 300
韩玉赏鉴 3 nz
八一双鹿 3 nz



>>> import jieba
>>> jieba.lcut(“八一双鹿更名为八一南昌篮球队!”)
# 没有使用用户自定义词典前的结果:
>>> [‘八’, ‘一双’, ‘鹿’, ‘更名’, ‘为’, ‘八一’, ‘南昌’, ‘篮球队’, ‘!’]

jieba.load_userdict(“./userdict.txt”)
# 使用了用户自定义词典后的结果:
[‘八一双鹿’, ‘更名’, ‘为’, ‘八一’, ‘南昌’, ‘篮球队’, ‘!’]


  • 流行中英文分词工具hanlp:
    • 中英文NLP处理工具包, 基于tensorflow2.0, 使用在学术界和行业中推广最先进的深度学习技术.

  • hanlp的安装:


# 使用pip进行安装
pip install hanlp


  • 使用hanlp进行中文分词:


>>> import hanlp
# 加载CTB_CONVSEG预训练模型进行分词任务
>>> tokenizer = hanlp.load(‘CTB6_CONVSEG’)
>>> tokenizer(“工信处女干事每月经过下属科室都要亲口交代24口交换机等技术性器件的安装工作”)
[‘工信处’, ‘女’, ‘干事’, ‘每’, ‘月’, ‘经过’, ‘下’, ‘属’, ‘科室’, ‘都’, ‘要’, ‘亲口’, ‘交代’, ‘24口’, ‘交换机’, ‘等’, ‘技术性’, ‘器件’, ‘的’, ‘安装’, ‘工作’]

  • 使用hanlp进行英文分词:


# 进行英文分词, 英文分词只需要使用规则即可
>>> tokenizer = hanlp.utils.rules.tokenize_english
>>> tokenizer(‘Mr. Hankcs bought hankcs.com for 1.5 thousand dollars.’)
[‘Mr.’, ‘Hankcs’, ‘bought’, ‘hankcs.com’, ‘for’, ‘1.5’, ‘thousand’, ‘dollars’, ‘.’]


什么是命名实体识别

  • 命名实体: 通常我们将人名, 地名, 机构名等专有名词统称命名实体. 如: 周杰伦, 黑山县, 孔子学院, 24辊方钢矫直机.
  • 顾名思义, 命名实体识别(Named Entity Recognition,简称NER)就是识别出一段文本中可能存在的命名实体.

  • 举个栗子:


鲁迅, 浙江绍兴人, 五四新文化运动的重要参与者, 代表作朝花夕拾.

==>

鲁迅(人名) / 浙江绍兴(地名)人 / 五四新文化运动(专有名词) / 重要参与者 / 代表作 / 朝花夕拾(专有名词)


  • 命名实体识别的作用:
    • 同词汇一样, 命名实体也是人类理解文本的基础单元, 因此也是AI解决NLP领域高阶任务的重要基础环节.

  • 使用hanlp进行中文命名实体识别:


>>> import hanlp
# 加载中文命名实体识别的预训练模型MSRA_NER_BERT_BASE_ZH
>>> recognizer = hanlp.load(hanlp.pretrained.ner.MSRA_NER_BERT_BASE_ZH)
# 这里注意它的输入是对句子进行字符分割的列表, 因此在句子前加入了list()
# >>> list(‘上海华安工业(集团)公司董事长谭旭光和秘书张晚霞来到美 国纽约现代艺术博物馆参观。’)
# [‘上’, ‘海’, ‘华’, ‘安’, ‘工’, ‘业’, ‘(’, ‘集’, ‘团’, ‘)’, ‘公’, ‘司’, ‘董’, ‘事’, ‘长’, ‘谭’, ‘旭’, ‘光’, ‘和’, ‘秘’, ‘书’, ‘张’, ‘晚’, ‘霞’, ‘来’, ‘到’, ‘美’, ‘国’, ‘纽’, ‘约’, ‘现’, ‘代’, ‘艺’, ‘术’, ‘博’, ‘物’, ‘馆’, ‘参’, ‘观’, ‘。’]
>>> recognizer(list(‘上海华安工业(集团)公司董事长谭旭光和秘书张晚霞来到美国纽约现代艺术博物馆参观。’))
[(‘上海华安工业(集团)公司’, ‘NT’, 0, 12), (‘谭旭光’, ‘NR’, 15, 18), (‘张晚霞’, ‘NR’, 21, 24), (‘美国’, ‘NS’, 26, 28), (‘纽约现代艺术博物馆’, ‘NS’, 28, 37)]

返回结果是一个装有n个元组的列表, 每个元组代表一个命名实体, 元组中的每一项分别代表具体的命名实体, 如: ‘上海华安工业(集团)公司’; 命名实体的类型, 如: ‘NT’-机构名; 命名实体的开始索引和结束索引, 如: 0, 12.


  • 使用hanlp进行英文命名实体识别:


>>> import hanlp
# 加载英文命名实体识别的预训练模型CONLL03_NER_BERT_BASE_UNCASED_EN
>>> recognizer = hanlp.load(hanlp.pretrained.ner.CONLL03_NER_BERT_BASE_UNCASED_EN))
# 这里注意它的输入是对句子进行分词后的结果, 是列表形式.
>>> recognizer([“President”, “Obama”, “is”, “speaking”, “at”, “the”, “White”, “House”])
[(‘Obama’, ‘PER’, 1, 2), (‘White House’, ‘LOC’, 6, 8)]
# 返回结果是一个装有n个元组的列表, 每个元组代表一个命名实体, 元组中的每一项分别代>表具体的命名实体, 如: ‘Obama’, 如: ‘PER’-人名; 命名实体的开始索引和结束索引, 如: 1, 2.


什么是词性标注

  • 词性: 语言中对词的一种分类方法,以语法特征为主要依据、兼顾词汇意义对词进行划分的结果, 常见的词性有14种, 如: 名词, 动词, 形容词等.
  • 顾名思义, 词性标注(Part-Of-Speech tagging, 简称POS)就是标注出一段文本中每个词汇的词性.

  • 举个栗子:


我爱自然语言处理

==>

我/rr, 爱/v, 自然语言/n, 处理/vn

rr: 人称代词
v: 动词
n: 名词
vn: 动名词


  • 词性标注的作用:
    • 词性标注以分词为基础, 是对文本语言的另一个角度的理解, 因此也常常成为AI解决NLP领域高阶任务的重要基础环节.

  • 使用jieba进行中文词性标注:


>>> import jieba.posseg as pseg
>>> pseg.lcut(“我爱北京天安门”)
[pair(‘我’, ‘r’), pair(‘爱’, ‘v’), pair(‘北京’, ‘ns’), pair(‘天安门’, ‘ns’)]

结果返回一个装有pair元组的列表, 每个pair元组中分别是词汇及其对应的词性, 具体词性含义请参照附录: jieba词性对照表


  • 使用hanlp进行中文词性标注:


>>> import hanlp
# 加载中文命名实体识别的预训练模型CTB5_POS_RNN_FASTTEXT_ZH
>>> tagger = hanlp.load(hanlp.pretrained.pos.CTB5_POS_RNN_FASTTEXT_ZH)
# 输入是分词结果列表
>>> tagger([‘我’, ‘的’, ‘希望’, ‘是’, ‘希望’, ‘和平’])
# 结果返回对应的词性
[‘PN’, ‘DEG’, ‘NN’, ‘VC’, ‘VV’, ‘NN’]

  • 使用hanlp进行英文词性标注:


>>> import hanlp
# 加载英文命名实体识别的预训练模型PTB_POS_RNN_FASTTEXT_EN
>>> tagger = hanlp.load(hanlp.pretrained.pos.PTB_POS_RNN_FASTTEXT_EN)
# 输入是分词结果列表
>>> tagger([‘I’, ‘banked’, ‘2’, ‘dollars’, ‘in’, ‘a’, ‘bank’, ‘.’])
[‘PRP’, ‘VBD’, ‘CD’, ‘NNS’, ‘IN’, ‘DT’, ‘NN’, ‘.’]


小节总结

  • 学习了什么是分词:
    • 分词就是将连续的字序列按照一定的规范重新组合成词序列的过程。我们知道,在英文的行文中,单词之间是以空格作为自然分界符的,而中文只是字、句和段能通过明显的分界符来简单划界,唯独词没有一个形式上的分界符, 分词过程就是找到这样分界符的过程.

  • 学习了分词的作用:
    • 词作为语言语义理解的最小单元, 是人类理解文本语言的基础. 因此也是AI解决NLP领域高阶任务, 如自动问答, 机器翻译, 文本生成的重要基础环节.

  • 学习了流行中文分词工具jieba:
    • 支持多种分词模式: 精确模式, 全模式, 搜索引擎模式
    • 支持中文繁体分词
    • 支持用户自定义词典

  • 学习了jieba工具的安装和分词使用.

  • 学习了流行中英文分词工具hanlp及其安装和分词使用.

  • 学习了什么是命名实体识别:
    • 命名实体: 通常我们将人名, 地名, 机构名等专有名词统称命名实体. 如: 周杰伦, 黑山县, 孔子学院, 24辊方钢矫直机.
    • 顾名思义, 命名实体识别(Named Entity Recognition,简称NER)就是识别出一段文本中可能存在的命名实体.

  • 命名实体识别的作用:
    • 同词汇一样, 命名实体也是人类理解文本的基础单元, 因此也是AI解决NLP领域高阶任务的重要基础环节.

  • 学习了使用hanlp进行命名实体识别.

  • 学习了什么是词性标注:
    • 词性: 语言中对词的一种分类方法,以语法特征为主要依据、兼顾词汇意义对词进行划分的结果, 常见的词性有14种, 如: 名词, 动词, 形容词等.
    • 顾名思义, 词性标注(Part-Of-Speech tagging, 简称POS)就是标注出一段文本中每个词汇的词性.

  • 学习了词性标注的作用:
    • 词性标注以分词为基础, 是对文本语言的另一个角度的理解, 因此也常常成为AI解决NLP领域高阶任务的重要基础环节.

  • 学习了使用jieba和hanlp进行词性标注.

1.3 文本张量表示方法

学习目标

  • 了解什么是文本张量表示及其作用.
  • 掌握文本张量表示的几种方法及其实现.

什么是文本张量表示

  • 将一段文本使用张量进行表示,其中一般将词汇为表示成向量,称作词向量,再由各个词向量按顺序组成矩阵形成文本表示.

  • 举个栗子:


["人生", "该", "如何", "起头"]

==>

每个词对应矩阵中的一个向量
[[1.32, 4,32, 0,32, 5.2],
[3.1, 5.43, 0.34, 3.2],
[3.21, 5.32, 2, 4.32],
[2.54, 7.32, 5.12, 9.54]]


  • 文本张量表示的作用:
    • 将文本表示成张量(矩阵)形式,能够使语言文本可以作为计算机处理程序的输入,进行接下来一系列的解析工作.

  • 文本张量表示的方法:
    • one-hot编码
    • Word2vec
    • Word Embedding

什么是one-hot词向量表示

  • 又称独热编码,将每个词表示成具有n个元素的向量,这个词向量中只有一个元素是1,其他元素都是0,不同词汇元素为0的位置不同,其中n的大小是整个语料中不同词汇的总数.

  • 举个栗子:


`[“改变”, “要”, “如何”, “起手”]``
==>

[[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1]]


  • onehot编码实现:
    • 进行onehot编码:


# 导入用于对象保存与加载的joblib
from sklearn.externals import joblib
# 导入keras中的词汇映射器Tokenizer
from keras.preprocessing.text import Tokenizer
# 假定vocab为语料集所有不同词汇集合
vocab = {“周杰伦”, “陈奕迅”, “王力宏”, “李宗盛”, “吴亦凡”, “鹿晗”}
# 实例化一个词汇映射器对象
t = Tokenizer(num_words=None, char_level=False)
# 使用映射器拟合现有文本数据
t.fit_on_texts(vocab)

for token in vocab:
zero_list = [0]*len(vocab)
# 使用映射器转化现有文本数据, 每个词汇对应从1开始的自然数
# 返回样式如: [[2]], 取出其中的数字需要使用[0][0]
token_index = t.texts_to_sequences([token])[0][0] - 1
zero_list[token_index] = 1
print(token, “的one-hot编码为:”, zero_list)

使用joblib工具保存映射器, 以便之后使用
tokenizer_path = “./Tokenizer”
joblib.dump(t, tokenizer_path)


  • 输出效果:


鹿晗 的one-hot编码为: [1, 0, 0, 0, 0, 0]
王力宏 的one-hot编码为: [0, 1, 0, 0, 0, 0]
李宗盛 的one-hot编码为: [0, 0, 1, 0, 0, 0]
陈奕迅 的one-hot编码为: [0, 0, 0, 1, 0, 0]
周杰伦 的one-hot编码为: [0, 0, 0, 0, 1, 0]
吴亦凡 的one-hot编码为: [0, 0, 0, 0, 0, 1]

同时在当前目录生成Tokenizer文件, 以便之后使用


  • onehot编码器的使用:


# 导入用于对象保存与加载的joblib
# from sklearn.externals import joblib
# 加载之前保存的Tokenizer, 实例化一个t对象
t = joblib.load(tokenizer_path)

编码token为”李宗盛”
token = “李宗盛”
# 使用t获得token_index
token_index = t.texts_to_sequences([token])[0][0] - 1
# 初始化一个zero_list
zero_list = [0]*len(vocab)
# 令zero_List的对应索引为1
zero_list[token_index] = 1
print(token, “的one-hot编码为:”, zero_list)


  • 输出效果:


李宗盛 的one-hot编码为: [1, 0, 0, 0, 0, 0]


  • one-hot编码的优劣势:
    • 优势:操作简单,容易理解.
    • 劣势:完全割裂了词与词之间的联系,而且在大语料集下,每个向量的长度过大,占据大量内存.

  • 说明:
    • 正因为one-hot编码明显的劣势,这种编码方式被应用的地方越来越少,取而代之的是接下来我们要学习的稠密向量的表示方法word2vec和word embedding.

什么是word2vec

  • 是一种流行的将词汇表示成向量的无监督训练方法, 该过程将构建神经网络模型, 将网络参数作为词汇的向量表示, 它包含CBOW和skipgram两种训练模式.

  • CBOW(Continuous bag of words)模式:
    • 给定一段用于训练的文本语料, 再选定某段长度(窗口)作为研究对象, 使用上下文词汇预测目标词汇.

文本预处理 - 图2

  • 分析:
    • 图中窗口大小为9, 使用前后4个词汇对目标词汇进行预测.

  • CBOW模式下的word2vec过程说明:
    • 假设我们给定的训练语料只有一句话: Hope can set you free (愿你自由成长),窗口大小为3,因此模型的第一个训练样本来自Hope can set,因为是CBOW模式,所以将使用Hope和set作为输入,can作为输出,在模型训练时, Hope,can,set等词汇都使用它们的one-hot编码. 如图所示: 每个one-hot编码的单词与各自的变换矩阵(即参数矩阵3x5, 这里的3是指最后得到的词向量维度)相乘之后再相加, 得到上下文表示矩阵(3x1).

文本预处理 - 图3

  • 接着, 将上下文表示矩阵与变换矩阵(参数矩阵5x3, 所有的变换矩阵共享参数)相乘, 得到5x1的结果矩阵, 它将与我们真正的目标矩阵即can的one-hot编码矩阵(5x1)进行损失的计算, 然后更新网络参数完成一次模型迭代.

文本预处理 - 图4

  • 最后窗口按序向后移动,重新更新参数,直到所有语料被遍历完成,得到最终的变换矩阵(3x5),这个变换矩阵与每个词汇的one-hot编码(5x1)相乘,得到的3x1的矩阵就是该词汇的word2vec张量表示.

  • skipgram模式:
    • 给定一段用于训练的文本语料, 再选定某段长度(窗口)作为研究对象, 使用目标词汇预测上下文词汇.

文本预处理 - 图5

  • 分析:
    • 图中窗口大小为9, 使用目标词汇对前后四个词汇进行预测.

  • skipgram模式下的word2vec过程说明:
    • 假设我们给定的训练语料只有一句话: Hope can set you free (愿你自由成长),窗口大小为3,因此模型的第一个训练样本来自Hope can set,因为是skipgram模式,所以将使用can作为输入 ,Hope和set作为输出,在模型训练时, Hope,can,set等词汇都使用它们的one-hot编码. 如图所示: 将can的one-hot编码与变换矩阵(即参数矩阵3x5, 这里的3是指最后得到的词向量维度)相乘, 得到目标词汇表示矩阵(3x1).
    • 接着, 将目标词汇表示矩阵与多个变换矩阵(参数矩阵5x3)相乘, 得到多个5x1的结果矩阵, 它将与我们Hope和set对应的one-hot编码矩阵(5x1)进行损失的计算, 然后更新网络参数完成一次模 型迭代.

文本预处理 - 图6

  • 最后窗口按序向后移动,重新更新参数,直到所有语料被遍历完成,得到最终的变换矩阵即参数矩阵(3x5),这个变换矩阵与每个词汇的one-hot编码(5x1)相乘,得到的3x1的矩阵就是该词汇的word2vec张量表示.

使用fasttext工具实现word2vec的训练和使用

  • 第一步: 获取训练数据
  • 第二步: 训练词向量
  • 第三步: 模型超参数设定
  • 第四步: 模型效果检验
  • 第五步: 模型的保存与重加载

  • 第一步: 获取训练数据


# 在这里, 我们将研究英语维基百科的部分网页信息, 它的大小在300M左右
# 这些语料已经被准备好, 我们可以通过Matt Mahoney的网站下载.
# 首先创建一个存储数据的文件夹data
$ mkdir data
# 使用wget下载数据的zip压缩包, 它将存储在data目录中
$ wget -c http://mattmahoney.net/dc/enwik9.zip -P data
# 使用unzip解压, 如果你的服务器中还没有unzip命令, 请使用: yum install unzip -y
# 解压后在data目录下会出现enwik9的文件夹
$ unzip data/enwik9.zip -d data

  • 查看原始数据:


$ head -10 data/enwik9

原始数据将输出很多包含XML/HTML格式的内容, 这些内容并不是我们需要的


Wikipedia
http://en.wikipedia.org/wiki/Main_Page
MediaWiki 1.6alpha
first-letter

Media
Special

  • 原始数据处理:


# 使用wikifil.pl文件处理脚本来清除XML/HTML格式的内容
# 注: wikifil.pl文件已为大家提供
$ perl wikifil.pl data/enwik9 > data/fil9

  • 查看预处理后的数据:


# 查看前80个字符
head -c 80 data/fil9

输出结果为由空格分割的单词
anarchism originated as a term of abuse first used against early working class


  • 第二步: 训练词向量


# 代码运行在python解释器中
# 导入fasttext
>>> import fasttext
# 使用fasttext的train_unsupervised(无监督训练方法)进行词向量的训练
# 它的参数是数据集的持久化文件路径’data/fil9’
>>> model = fasttext.train_unsupervised(‘data/fil9’)

有效训练词汇量为124M, 共218316个单词
Read 124M words
Number of words: 218316
Number of labels: 0
Progress: 100.0% words/sec/thread: 53996 lr: 0.000000 loss: 0.734999 ETA: 0h 0m


  • 查看单词对应的词向量:


# 通过get_word_vector方法来获得指定词汇的词向量
>>> model.get_word_vector(“the”)

array([-0.03087516, 0.09221972, 0.17660329, 0.17308897, 0.12863874,
0.13912526, -0.09851588, 0.00739991, 0.37038437, -0.00845221,

-0.21184735, -0.05048715, -0.34571868, 0.23765688, 0.23726143],
dtype=float32)


  • 第三步: 模型超参数设定


# 在训练词向量过程中, 我们可以设定很多常用超参数来调节我们的模型效果, 如:
# 无监督训练模式: ‘skipgram’ 或者 ‘cbow’, 默认为’skipgram’, 在实践中,skipgram模式在利用子词方面比cbow更好.
# 词嵌入维度dim: 默认为100, 但随着语料库的增大, 词嵌入的维度往往也要更大.
# 数据循环次数epoch: 默认为5, 但当你的数据集足够大, 可能不需要那么多次.
# 学习率lr: 默认为0.05, 根据经验, 建议选择[0.01,1]范围内.
# 使用的线程数thread: 默认为12个线程, 一般建议和你的cpu核数相同.

model = fasttext.train_unsupervised(‘data/fil9’, “cbow”, dim=300, epoch=1, lr=0.1, thread=8)

Read 124M words
Number of words: 218316
Number of labels: 0
Progress: 100.0% words/sec/thread: 49523 lr: 0.000000 avg.loss: 1.777205 ETA: 0h 0m 0s


  • 第四步: 模型效果检验


# 检查单词向量质量的一种简单方法就是查看其邻近单词, 通过我们主观来判断这些邻近单词是否与目标单词相关来粗略评定模型效果好坏.

查找”运动”的邻近单词, 我们可以发现”体育网”, “运动汽车”, “运动服”等.
>>> model.get_nearest_neighbors(‘sports’)

[(0.8414610624313354, ‘sportsnet’), (0.8134572505950928, ‘sport’), (0.8100415468215942, ‘sportscars’), (0.8021156787872314, ‘sportsground’), (0.7889881134033203, ‘sportswomen’), (0.7863013744354248, ‘sportsplex’), (0.7786710262298584, ‘sporty’), (0.7696356177330017, ‘sportscar’), (0.7619683146476746, ‘sportswear’), (0.7600985765457153, ‘sportin’)]

查找”音乐”的邻近单词, 我们可以发现与音乐有关的词汇.
>>> model.get_nearest_neighbors(‘music’)

[(0.8908010125160217, ‘emusic’), (0.8464668393135071, ‘musicmoz’), (0.8444250822067261, ‘musics’), (0.8113634586334229, ‘allmusic’), (0.8106718063354492, ‘musices’), (0.8049437999725342, ‘musicam’), (0.8004694581031799, ‘musicom’), (0.7952923774719238, ‘muchmusic’), (0.7852965593338013, ‘musicweb’), (0.7767147421836853, ‘musico’)]

查找”小狗”的邻近单词, 我们可以发现与小狗有关的词汇.
>>> model.get_nearest_neighbors(‘dog’)

[(0.8456876873970032, ‘catdog’), (0.7480780482292175, ‘dogcow’), (0.7289096117019653, ‘sleddog’), (0.7269964218139648, ‘hotdog’), (0.7114801406860352, ‘sheepdog’), (0.6947550773620605, ‘dogo’), (0.6897546648979187, ‘bodog’), (0.6621081829071045, ‘maddog’), (0.6605004072189331, ‘dogs’), (0.6398137211799622, ‘dogpile’)]


  • 第五步: 模型的保存与重加载


# 使用save_model保存模型
>>> model.save_model(“fil9.bin”)

使用fasttext.load_model加载模型
>>> model = fasttext.load_model(“fil9.bin”)
>>> model.get_word_vector(“the”)

array([-0.03087516, 0.09221972, 0.17660329, 0.17308897, 0.12863874,
0.13912526, -0.09851588, 0.00739991, 0.37038437, -0.00845221,

-0.21184735, -0.05048715, -0.34571868, 0.23765688, 0.23726143],
dtype=float32)


什么是word embedding(词嵌入)

  • 通过一定的方式将词汇映射到指定维度(一般是更高维度)的空间.
  • 广义的word embedding包括所有密集词汇向量的表示方法,如之前学习的word2vec, 即可认为是word embedding的一种.
  • 狭义的word embedding是指在神经网络中加入的embedding层, 对整个网络进行训练的同时产生的embedding矩阵(embedding层的参数), 这个embedding矩阵就是训练过程中所有输入词汇的向量表示组成的矩阵.

以一个案例演示狭义的word_embedding的生成过程


  • word embedding的可视化分析:
    • 通过使用tensorboard可视化嵌入的词向量.


# 导入torch和tensorboard的摘要写入方法
import torch
import json
import fileinput
from torch.utils.tensorboard import SummaryWriter
# 实例化一个摘要写入对象
writer = SummaryWriter()

随机初始化一个100x50的矩阵, 认为它是我们已经得到的词嵌入矩阵
# 代表100个词汇, 每个词汇被表示成50维的向量
embedded = torch.randn(100, 50)

导入事先准备好的100个中文词汇文件, 形成meta列表原始词汇
meta = list(map(lambda x: x.strip(), fileinput.FileInput(“./vocab100.csv”)))
writer.add_embedding(embedded, metadata=meta)
writer.close()


  • 在终端启动tensorboard服务:


$ tensorboard --logdir runs --host 0.0.0.0

通过http://0.0.0.0:6006访问浏览器可视化页面


  • 浏览器展示并可以使用右侧近邻词汇功能检验效果:

文本预处理 - 图7


小节总结

  • 学习了什么是文本张量表示:
    • 将一段文本使用张量进行表示,其中一般将词汇为表示成向量,称作词向量,再由各个词向量按顺序组成矩阵形成文本表示.

  • 学习了文本张量表示的作用:
    • 将文本表示成张量(矩阵)形式,能够使语言文本可以作为计算机处理程序的输入,进行接下来一系列的解析工作.

  • 学习了文本张量表示的方法:
    • one-hot编码
    • Word2vec
    • Word Embedding

  • 什么是one-hot词向量表示:
    • 又称独热编码,将每个词表示成具有n个元素的向量,这个词向量中只有一个元素是1,其他元素都是0,不同词汇元素为0的位置不同,其中n的大小是整个语料中不同词汇的总数.

  • 学习了onehot编码实现.

  • 学习了one-hot编码的优劣势:
    • 优势:操作简单,容易理解.
    • 劣势:完全割裂了词与词之间的联系,而且在大语料集下,每个向量的长度过大,占据大量内存.

  • 学习了什么是word2vec:
    • 是一种流行的将词汇表示成向量的无监督训练方法, 该过程将构建神经网络模型, 将网络参数作为词汇的向量表示, 它包含CBOW和skipgram两种训练模式.

  • 学习了CBOW(Continuous bag of words)模式:
    • 给定一段用于训练的文本语料, 再选定某段长度(窗口)作为研究对象, 使用上下文词汇预测目标词汇.

  • 学习了CBOW模式下的word2vec过程说明:
    • 假设我们给定的训练语料只有一句话: Hope can set you free (愿你自由成长),窗口大小为3,因此模型的第一个训练样本来自Hope you set,因为是CBOW模式,所以将使用Hope和set作为输入,you作为输出,在模型训练时, Hope,set,you等词汇都使用它们的one-hot编码. 如图所示: 每个one-hot编码的单词与各自的变换矩阵(即参数矩阵3x5, 这里的3是指最后得到的词向量维度)相乘之后再相加, 得到上下文表示矩阵(3x1).
    • 接着, 将上下文表示矩阵与变换矩阵(参数矩阵5x3, 所有的变换矩阵共享参数)相乘, 得到5x1的结果矩阵, 它将与我们真正的目标矩阵即you的one-hot编码矩阵(5x1)进行损失的计算, 然后更新网络参数完成一次模型迭代.
    • 最后窗口按序向后移动,重新更新参数,直到所有语料被遍历完成,得到最终的变换矩阵(3x5),这个变换矩阵与每个词汇的one-hot编码(5x1)相乘,得到的3x1的矩阵就是该词汇的word2vec张量表示.

  • 学习了skipgram模式:
    • 给定一段用于训练的文本语料, 再选定某段长度(窗口)作为研究对象, 使用目标词汇预测上下文词汇.

  • 学习了skipgram模式下的word2vec过程说明:
    • 假设我们给定的训练语料只有一句话: Hope can set you free (愿你自由成长),窗口大小为3,因此模型的第一个训练样本来自Hope you set,因为是skipgram模式,所以将使用you作为输入 ,hope和set作为输出,在模型训练时, Hope,set,you等词汇都使用它们的one-hot编码. 如图所示: 将you的one-hot编码与变换矩阵(即参数矩阵3x5, 这里的3是指最后得到的词向量维度)相乘, 得到目标词汇表示矩阵(3x1).
    • 接着, 将目标词汇表示矩阵与多个变换矩阵(参数矩阵5x3)相乘, 得到多个5x1的结果矩阵, 它将与我们hope和set对应的one-hot编码矩阵(5x1)进行损失的计算, 然后更新网络参数完成一次模 型迭代.
    • 最后窗口按序向后移动,重新更新参数,直到所有语料被遍历完成,得到最终的变换矩阵即参数矩阵(3x5),这个变换矩阵与每个词汇的one-hot编码(5x1)相乘,得到的3x1的矩阵就是该词汇的word2vec张量表示.

  • 学习了使用fasttext工具实现word2vec的训练和使用:
    • 第一步: 获取训练数据
    • 第二步: 训练词向量
    • 第三步: 模型超参数设定
    • 第四步: 模型效果检验
    • 第五步: 模型的保存与重加载

  • 学习了什么是word embedding(词嵌入):
    • 通过一定的方式将词汇映射到指定维度(一般是更高维度)的空间.
    • 广义的word embedding包括所有密集词汇向量的表示方法,如之前学习的word2vec, 即可认为是word embedding的一种.
    • 狭义的word embedding是指在神经网络中加入的embedding层, 对整个网络进行训练的同时产生的embedding矩阵(embedding层的参数), 这个embedding矩阵就是训练过程中所有输入词汇的向量表示组成的矩阵.

  • 通过一个案例: 新闻主题分类任务, 演示狭义的word_embedding的生成过程.

  • 学习了word embedding的可视化分析:
    • 通过使用tensorboard可视化嵌入的词向量.
    • 在终端启动tensorboard服务.
    • 浏览器展示并可以使用右侧近邻词汇功能检验效果.

1.4 文本数据分析


学习目标

  • 了解文本数据分析的作用.
  • 掌握常用的几种文本数据分析方法.

  • 文本数据分析的作用:
    • 文本数据分析能够有效帮助我们理解数据语料, 快速检查出语料可能存在的问题, 并指导之后模型训练过程中一些超参数的选择.

  • 常用的几种文本数据分析方法:
    • 标签数量分布
    • 句子长度分布
    • 词频统计与关键词词云

  • 说明:
    • 我们将基于真实的中文酒店评论语料来讲解常用的几种文本数据分析方法.

  • 中文酒店评论语料:
    • 属于二分类的中文情感分析语料, 该语料存放在”./cn_data”目录下.
    • 其中train.tsv代表训练集, dev.tsv代表验证集, 二者数据样式相同.

  • train.tsv数据样式:


sentence label
早餐不好,服务不到位,晚餐无西餐,早餐晚餐相同,房间条件不好,餐厅不分吸烟区.房间不分有无烟房. 0
去的时候 ,酒店大厅和餐厅在装修,感觉大厅有点挤.由于餐厅装修本来该享受的早饭,也没有享受(他们是8点开始每个房间送,但是我时间来不及了)不过前台服务员态度好! 1
有很长时间没有在西藏大厦住了,以前去北京在这里住的较多。这次住进来发现换了液晶电视,但网络不是很好,他们自己说是收费的原因造成的。其它还好。 1
非常好的地理位置,住的是豪华海景房,打开窗户就可以看见栈桥和海景。记得很早以前也住过,现在重新装修了。总的来说比较满意,以后还会住 1
交通很方便,房间小了一点,但是干净整洁,很有香港的特色,性价比较高,推荐一下哦 1
酒店的装修比较陈旧,房间的隔音,主要是卫生间的隔音非常差,只能算是一般的 0
酒店有点旧,房间比较小,但酒店的位子不错,就在海边,可以直接去游泳。8楼的海景打开窗户就是海。如果想住在热闹的地带,这里不是一个很好的选择,不过威海城市真的比较小,打车还是相当便宜的。晚上酒店门口出租车比较少。 1
位置很好,走路到文庙、清凉寺5分钟都用不了,周边公交车很多很方便,就是出租车不太爱去(老城区路窄爱堵车),因为是老宾馆所以设施要陈旧些, 1
酒店设备一般,套房里卧室的不能上网,要到客厅去。 0


  • train.tsv数据样式说明:
    • train.tsv中的数据内容共分为2列, 第一列数据代表具有感情色彩的评论文本; 第二列数据, 0或1, 代表每条文本数据是积极或者消极的评论, 0代表消极, 1代表积极.

获得训练集和验证集的标签数量分布


# 导入必备工具包
import seaborn as sns
import pandas as pd
import matplotlib.pyplot as plt
# 设置显示风格
plt.style.use(‘fivethirtyeight’)

分别读取训练tsv和验证tsv
train_data = pd.read_csv(“./cn_data/train.tsv”, sep=”\t”)
valid_data = pd.read_csv(“./cn_data/dev.tsv”, sep=”\t”)

获得训练数据标签数量分布
sns.countplot(“label”, data=train_data)
plt.title(“train_data”)
plt.show()

获取验证数据标签数量分布
sns.countplot(“label”, data=valid_data)
plt.title(“valid_data”)
plt.show()


  • 训练集标签数量分布:

文本预处理 - 图8


  • 验证集标签数量分布:

文本预处理 - 图9


  • 分析:
    • 在深度学习模型评估中, 我们一般使用ACC作为评估指标, 若想将ACC的基线定义在50%左右, 则需要我们的正负样本比例维持在1:1左右, 否则就要进行必要的数据增强或数据删减. 上图中训练和验证集正负样本都稍有不均衡, 可以进行一些数据增强.

获取训练集和验证集的句子长度分布


# 在训练数据中添加新的句子长度列, 每个元素的值都是对应的句子列的长度
train_data[“sentence_length”] = list(map(lambda x: len(x), train_data[“sentence”]))

绘制句子长度列的数量分布图
sns.countplot(“sentence_length”, data=train_data)
# 主要关注count长度分布的纵坐标, 不需要绘制横坐标, 横坐标范围通过dist图进行查看
plt.xticks([])
plt.show()

绘制dist长度分布图
sns.distplot(train_data[“sentence_length”])

主要关注dist长度分布横坐标, 不需要绘制纵坐标
plt.yticks([])
plt.show()

在验证数据中添加新的句子长度列, 每个元素的值都是对应的句子列的长度
valid_data[“sentence_length”] = list(map(lambda x: len(x), valid_data[“sentence”]))

绘制句子长度列的数量分布图
sns.countplot(“sentence_length”, data=valid_data)

主要关注count长度分布的纵坐标, 不需要绘制横坐标, 横坐标范围通过dist图进行查看
plt.xticks([])
plt.show()

绘制dist长度分布图
sns.distplot(valid_data[“sentence_length”])

主要关注dist长度分布横坐标, 不需要绘制纵坐标
plt.yticks([])
plt.show()


  • 训练集句子长度分布:

文本预处理 - 图10文本预处理 - 图11


  • 验证集句子长度分布:

文本预处理 - 图12文本预处理 - 图13


  • 分析:
    • 通过绘制句子长度分布图, 可以得知我们的语料中大部分句子长度的分布范围, 因为模型的输入要求为固定尺寸的张量,合理的长度范围对之后进行句子截断补齐(规范长度)起到关键的指导作用. 上图中大部分句子长度的范围大致为20-250之间.

获取训练集和验证集的正负样本长度散点分布


# 绘制训练集长度分布的散点图
sns.stripplot(y=’sentence_length’,x=’label’,data=train_data)
plt.show()

绘制验证集长度分布的散点图
sns.stripplot(y=’sentence_length’,x=’label’,data=valid_data)
plt.show()


  • 训练集上正负样本的长度散点分布:

文本预处理 - 图14


  • 验证集上正负样本的长度散点分布:

文本预处理 - 图15


  • 分析:
    • 通过查看正负样本长度散点图, 可以有效定位异常点的出现位置, 帮助我们更准确进行人工语料审查. 上图中在训练集正样本中出现了异常点, 它的句子长度近3500左右, 需要我们人工审查.

获得训练集与验证集不同词汇总数统计


# 导入jieba用于分词
# 导入chain方法用于扁平化列表
import jieba
from itertools import chain

进行训练集的句子进行分词, 并统计出不同词汇的总数
train_vocab = set(chain(*map(lambda x: jieba.lcut(x), train_data[“sentence”])))
print(“训练集共包含不同词汇总数为:”, len(train_vocab))

进行验证集的句子进行分词, 并统计出不同词汇的总数
valid_vocab = set(chain(*map(lambda x: jieba.lcut(x), valid_data[“sentence”])))
print(“训练集共包含不同词汇总数为:”, len(valid_vocab))


  • 输出效果:


训练集共包含不同词汇总数为: 12147
训练集共包含不同词汇总数为: 6857


获得训练集上正负的样本的高频形容词词云


# 使用jieba中的词性标注功能
import jieba.posseg as pseg

def get_a_list(text):
“””用于获取形容词列表”””
# 使用jieba的词性标注方法切分文本,获得具有词性属性flag和词汇属性word的对象,
# 从而判断flag是否为形容词,来返回对应的词汇
r = []
for g in pseg.lcut(text):
if g.flag == “a”:
r.append(g.word)
return r

导入绘制词云的工具包
from wordcloud import WordCloud

def get_word_cloud(keywords_list):
# 实例化绘制词云的类, 其中参数font_path是字体路径, 为了能够显示中文,
# max_words指词云图像最多显示多少个词, background_color为背景颜色
wordcloud = WordCloud(font_path=”./SimHei.ttf”, max_words=100, background_color=”white”)
# 将传入的列表转化成词云生成器需要的字符串形式
keywords_string = “ “.join(keywords_list)
# 生成词云
wordcloud.generate(keywords_string)

  1. # 绘制图像并显示<br /> plt.figure()<br /> plt.imshow(wordcloud, interpolation="bilinear")<br /> plt.axis("off")<br /> plt.show()

获得训练集上正样本
p_train_data = train_data[train_data[“label”]==1][“sentence”]

对正样本的每个句子的形容词
train_p_a_vocab = chain(*map(lambda x: get_a_list(x), p_train_data))
#print(train_p_n_vocab)

获得训练集上负样本
n_train_data = train_data[train_data[“label”]==0][“sentence”]

获取负样本的每个句子的形容词
train_n_a_vocab = chain(*map(lambda x: get_a_list(x), n_train_data))

调用绘制词云函数
get_word_cloud(train_p_a_vocab)
get_word_cloud(train_n_a_vocab)


  • 训练集正样本形容词词云:

文本预处理 - 图16


  • 训练集负样本形容词词云:

文本预处理 - 图17


获得验证集上正负的样本的形容词词云


# 获得验证集上正样本
p_valid_data = valid_data[valid_data[“label”]==1][“sentence”]

对正样本的每个句子的形容词
valid_p_a_vocab = chain(*map(lambda x: get_a_list(x), p_valid_data))
#print(train_p_n_vocab)

获得验证集上负样本
n_valid_data = valid_data[valid_data[“label”]==0][“sentence”]

获取负样本的每个句子的形容词
valid_n_a_vocab = chain(*map(lambda x: get_a_list(x), n_valid_data))

调用绘制词云函数
get_word_cloud(valid_p_a_vocab)
get_word_cloud(valid_n_a_vocab)


  • 验证集正样本形容词词云:

文本预处理 - 图18


  • 验证集负样本形容词词云:

文本预处理 - 图19


  • 分析:
    • 根据高频形容词词云显示, 我们可以对当前语料质量进行简单评估, 同时对违反语料标签含义的词汇进行人工审查和修正, 来保证绝大多数语料符合训练标准. 上图中的正样本大多数是褒义词, 而负样本大多数是贬义词, 基本符合要求, 但是负样本词云中也存在”便利”这样的褒义词, 因此可以人工进行审查.

小节总结

  • 学习了文本数据分析的作用:
    • 文本数据分析能够有效帮助我们理解数据语料, 快速检查出语料可能存在的问题, 并指导之后模型训练过程中一些超参数的选择.

  • 学习了常用的几种文本数据分析方法:
    • 标签数量分布
    • 句子长度分布
    • 词频统计与关键词词云

  • 学习了基于真实的中文酒店评论语料进行几种文本数据分析方法.
    • 获得训练集和验证集的标签数量分布
    • 获取训练集和验证集的句子长度分布
    • 获取训练集和验证集的正负样本长度散点分布
    • 获得训练集与验证集不同词汇总数统计
    • 获得训练集上正负的样本的高频形容词词云

1.5 文本特征处理


学习目标

  • 了解文本特征处理的作用.
  • 掌握实现常见的文本特征处理的具体方法.

  • 文本特征处理的作用:
    • 文本特征处理包括为语料添加具有普适性的文本特征, 如:n-gram特征, 以及对加入特征之后的文本语料进行必要的处理, 如: 长度规范. 这些特征处理工作能够有效的将重要的文本特征加入模型训练中, 增强模型评估指标.

  • 常见的文本特征处理方法:
    • 添加n-gram特征
    • 文本长度规范

什么是n-gram特征

  • 给定一段文本序列, 其中n个词或字的相邻共现特征即n-gram特征, 常用的n-gram特征是bi-gram和tri-gram特征, 分别对应n为2和3.

  • 举个栗子:


假设给定分词列表: ["是谁", "敲动", "我心"]

对应的数值映射列表为: [1, 34, 21]

我们可以认为数值映射列表中的每个数字是词汇特征.

除此之外, 我们还可以把”是谁”和”敲动”两个词共同出现且相邻也作为一种特征加入到序列列表中,

假设1000就代表”是谁”和”敲动”共同出现且相邻

此时数值映射列表就变成了包含2-gram特征的特征列表: [1, 34, 21, 1000]

这里的”是谁”和”敲动”共同出现且相邻就是bi-gram特征中的一个.

“敲动”和”我心”也是共现且相邻的两个词汇, 因此它们也是bi-gram特征.

假设1001代表”敲动”和”我心”共同出现且相邻

那么, 最后原始的数值映射列表 [1, 34, 21] 添加了bi-gram特征之后就变成了 [1, 34, 21, 1000, 1001]


  • 提取n-gram特征:


# 一般n-gram中的n取2或者3, 这里取2为例
ngram_range = 2

def create_ngram_set(input_list):
“””
description: 从数值列表中提取所有的n-gram特征
:param input_list: 输入的数值列表, 可以看作是词汇映射后的列表,
里面每个数字的取值范围为[1, 25000]
:return: n-gram特征组成的集合

  1. eg:<br /> >>> create_ngram_set([1, 4, 9, 4, 1, 4])<br /> {(4, 9), (4, 1), (1, 4), (9, 4)}<br /> """<br /> return set(zip(*[input_list[i:] for i in range(ngram_range)]))
  • 调用:


input_list = [1, 3, 2, 1, 5, 3]
res = create_ngram_set(input_list)
print(res)


  • 输出效果:


# 该输入列表的所有bi-gram特征
{(3, 2), (1, 3), (2, 1), (1, 5), (5, 3)}


文本长度规范及其作用

  • 一般模型的输入需要等尺寸大小的矩阵, 因此在进入模型前需要对每条文本数值映射后的长度进行规范, 此时将根据句子长度分布分析出覆盖绝大多数文本的合理长度, 对超长文本进行截断, 对不足文本进行补齐(一般使用数字0), 这个过程就是文本长度规范.

  • 文本长度规范的实现:


from keras.preprocessing import sequence

cutlen根据数据分析中句子长度分布,覆盖90%左右语料的最短长度.
# 这里假定cutlen为10
cutlen = 10

def padding(x_train):
“””
description: 对输入文本张量进行长度规范
:param x_train: 文本的张量表示, 形如: [[1, 32, 32, 61], [2, 54, 21, 7, 19]]
:return: 进行截断补齐后的文本张量表示
“””
# 使用sequence.pad_sequences即可完成
return sequence.pad_sequences(x_train, cutlen)


  • 调用:


# 假定x_train里面有两条文本, 一条长度大于10, 一天小于10
x_train = [[1, 23, 5, 32, 55, 63, 2, 21, 78, 32, 23, 1],
[2, 32, 1, 23, 1]]

res = padding(x_train)
print(res)


  • 输出效果:


[[ 5 32 55 63 2 21 78 32 23 1]
[ 0 0 0 0 0 2 32 1 23 1]]


小节总结

  • 学习了文本特征处理的作用:
    • 文本特征处理包括为语料添加具有普适性的文本特征, 如:n-gram特征, 以及对加入特征之后的文本语料进行必要的处理, 如: 长度规范. 这些特征处理工作能够有效的将重要的文本特征加入模型训练中, 增强模型评估指标.

  • 学习了常见的文本特征处理方法:
    • 添加n-gram特征
    • 文本长度规范

  • 学习了什么是n-gram特征:
    • 给定一段文本序列, 其中n个词或字的相邻共现特征即n-gram特征, 常用的n-gram特征是bi-gram和tri-gram特征, 分别对应n为2和3.

  • 学习了提取n-gram特征的函数: create_ngram_set

  • 学习了文本长度规范及其作用:
    • 一般模型的输入需要等尺寸大小的矩阵, 因此在进入模型前需要对每条文本数值映射后的长度进行规范, 此时将根据句子长度分布分析出覆盖绝大多数文本的合理长度, 对超长文本进行截断, 对不足文本进行补齐(一般使用数字0), 这个过程就是文本长度规范.

  • 学习了文本长度规范的实现函数: padding

1.6 文本数据增强


学习目标

  • 了解文本数据增强的作用.
  • 掌握实现常见的文本数据增强的具体方法.

  • 常见的文本数据增强方法:
    • 回译数据增强法

什么是回译数据增强法

  • 回译数据增强目前是文本数据增强方面效果较好的增强方法, 一般基于google翻译接口, 将文本数据翻译成另外一种语言(一般选择小语种),之后再翻译回原语言, 即可认为得到与与原语料同标签的新语料, 新语料加入到原数据集中即可认为是对原数据集数据增强.

  • 回译数据增强优势:
    • 操作简便, 获得新语料质量高.

  • 回译数据增强存在的问题:
    • 在短文本回译过程中, 新语料与原语料可能存在很高的重复率, 并不能有效增大样本的特征空间.

  • 高重复率解决办法:
    • 进行连续的多语言翻译, 如: 中文—>韩文—>日语—>英文—>中文, 根据经验, 最多只采用3次连续翻译, 更多的翻译次数将产生效率低下, 语义失真等问题.

  • 回译数据增强实现:


# 假设取两条已经存在的正样本和两条负样本
# 将基于这四条样本产生新的同标签的四条样本
p_sample1 = “酒店设施非常不错”
p_sample2 = “这家价格很便宜”
n_sample1 = “拖鞋都发霉了, 太差了”
n_sample2 = “电视不好用, 没有看到足球”

导入google翻译接口工具
from googletrans import Translator
# 实例化翻译对象
translator = Translator()
# 进行第一次批量翻译, 翻译目标是韩语
translations = translator.translate([p_sample1, p_sample2, n_sample1, n_sample2], dest=’ko’)
# 获得翻译后的结果
ko_res = list(map(lambda x: x.text, translations))
# 打印结果
print(“中间翻译结果:”)
print(ko_res)

最后在翻译回中文, 完成回译全部流程
translations = translator.translate(ko_res, dest=’zh-cn’)
cn_res = list(map(lambda x: x.text, translations))
print(“回译得到的增强数据:”)
print(cn_res)


  • 输出效果:


中间翻译结果:
[‘호텔 시설은 아주 좋다’, ‘이 가격은 매우 저렴합니다’, ‘슬리퍼 곰팡이가 핀이다, 나쁜’, ‘TV가 잘 작동하지 않습니다, 나는 축구를 볼 수 없습니다’]
回译得到的增强数据:
[‘酒店设施都非常好’, ‘这个价格是非常实惠’, ‘拖鞋都发霉了,坏’, ‘电视不工作,我不能去看足球’]


小节总结

  • 学习了常见的文本数据增强方法:
    • 回译数据增强法

  • 学习了什么是回译数据增强法:
    • 回译数据增强目前是文本数据增强方面效果较好的增强方法, 一般基于google翻译接口, 将文本数据翻译成另外一种语言(一般选择小语种),之后再翻译回原语言, 即可认为得到与与原语料同标签的新语料, 新语料加入到原数据集中即可认为是对原数据集数据增强.

  • 学习了回译数据增强优势:
    • 操作简便, 获得新语料质量高.

  • 学习了回译数据增强存在的问题:
    • 在短文本回译过程中, 新语料与原语料可能存在很高的重复率, 并不能有效增大样本的特征空间.

  • 学习了高重复率解决办法:
    • 进行连续的多语言翻译, 如: 中文—>韩文—>日语—>英文—>中文, 根据经验, 最多只采用3次连续翻译, 更多的翻译次数将产生效率低下, 语义失真等问题.

  • 学习了回译数据增强实现.

附录

  • jieba词性对照表:


- a 形容词
- ad 副形词
- ag 形容词性语素
- an 名形词
- b 区别词
- c 连词
- d 副词
- df
- dg 副语素
- e 叹词
- f 方位词
- g 语素
- h 前接成分
- i 成语
- j 简称略称
- k 后接成分
- l 习用语
- m 数词
- mg
- mq 数量词
- n 名词
- ng 名词性语素
- nr 人名
- nrfg
- nrt
- ns 地名
- nt 机构团体名
- nz 其他专名
- o 拟声词
- p 介词
- q 量词
- r 代词
- rg 代词性语素
- rr 人称代词
- rz 指示代词
- s 处所词
- t 时间词
- tg 时语素
- u 助词
- ud 结构助词 得
- ug 时态助词
- uj 结构助词 的
- ul 时态助词 了
- uv 结构助词 地
- uz 时态助词 着
- v 动词
- vd 副动词
- vg 动词性语素
- vi 不及物动词
- vn 名动词
- vq
- x 非语素词
- y 语气词
- z 状态词
- zg


  • hanlp词性对照表:


【Proper Noun——NR,专有名词】

【Temporal Noun——NT,时间名词】

【Localizer——LC,定位词】如“内”,“左右”

【Pronoun——PN,代词】

【Determiner——DT,限定词】如“这”,“全体”

【Cardinal Number——CD,量词】

【Ordinal Number——OD,次序词】如“第三十一”

【Measure word——M,单位词】如“杯”

【Verb:VA,VC,VE,VV,动词】

【Adverb:AD,副词】如“近”,“极大”

【Preposition:P,介词】如“随着”

【Subordinating conjunctions:CS,从属连词】

【Conjuctions:CC,连词】如“和”

【Particle:DEC,DEG,DEV,DER,AS,SP,ETC,MSP,小品词】如“的话”

【Interjections:IJ,感叹词】如“哈”

【onomatopoeia:ON,拟声词】如“哗啦啦”

【Other Noun-modifier:JJ】如“发稿/JJ 时间/NN”

【Punctuation:PU,标点符号】

【Foreign word:FW,外国词语】如“OK

第二章:新闻主题分类任务

2.1 新闻主题分类任务


学习目标

  • 了解有关新闻主题分类和有关数据.
  • 掌握使用浅层网络构建新闻主题分类器的实现过程.

  • 关于新闻主题分类任务:
    • 以一段新闻报道中的文本描述内容为输入, 使用模型帮助我们判断它最有可能属于哪一种类型的新闻, 这是典型的文本分类问题, 我们这里假定每种类型是互斥的, 即文本描述有且只有一种类型.

  • 新闻主题分类数据:
    • 通过torchtext获取数据:


# 导入相关的torch工具包
import torch
import torchtext
# 导入torchtext.datasets中的文本分类任务
from torchtext.datasets import text_classification
import os

定义数据下载路径, 当前路径的data文件夹
load_data_path = “./data”
# 如果不存在该路径, 则创建这个路径
if not os.path.isdir(load_data_path):
os.mkdir(load_data_path)

选取torchtext中的文本分类数据集’AG_NEWS’即新闻主题分类数据, 保存在指定目录下
# 并将数值映射后的训练和验证数据加载到内存中
train_dataset, test_dataset = text_classification.DATASETS‘AG_NEWS’


  • 数据文件预览:


- data/
- ag_news_csv.tar.gz
- ag_news_csv/
classes.txt
readme.txt
test.csv
train.csv

  • 文件说明:
    • train.csv表示训练数据, 共12万条数据; test.csv表示验证数据, 共7600条数据; classes.txt是标签(新闻主题)含义文件, 里面有四个单词’World’, ‘Sports’, ‘Business’, ‘Sci/Tech’代表新闻的四个主题, readme.txt是该数据集的英文说明.
  • train.csv预览:


"3","Wall St. Bears Claw Back Into the Black (Reuters)","Reuters - Short-sellers, Wall Street's dwindling\band of ultra-cynics, are seeing green again."
“3”,”Carlyle Looks Toward Commercial Aerospace (Reuters)”,”Reuters - Private investment firm Carlyle Group,\which has a reputation for making well-timed and occasionally\controversial plays in the defense industry, has quietly placed\its bets on another part of the market.”
“3”,”Oil and Economy Cloud Stocks’ Outlook (Reuters)”,”Reuters - Soaring crude prices plus worries\about the economy and the outlook for earnings are expected to\hang over the stock market next week during the depth of the\summer doldrums.”
“3”,”Iraq Halts Oil Exports from Main Southern Pipeline (Reuters)”,”Reuters - Authorities have halted oil export\flows from the main pipeline in southern Iraq after\intelligence showed a rebel militia could strike\infrastructure, an oil official said on Saturday.”
“3”,”Oil prices soar to all-time record, posing new menace to US economy (AFP)”,”AFP - Tearaway world oil prices, toppling records and straining wallets, present a new economic menace barely three months before the US presidential elections.”
“3”,”Stocks End Up, But Near Year Lows (Reuters)”,”Reuters - Stocks ended slightly higher on Friday\but stayed near lows for the year as oil prices surged past #36;46\a barrel, offsetting a positive outlook from computer maker\Dell Inc. (DELL.O)”
“3”,”Money Funds Fell in Latest Week (AP)”,”AP - Assets of the nation’s retail money market mutual funds fell by #36;1.17 billion in the latest week to #36;849.98 trillion, the Investment Company Institute said Thursday.”
“3”,”Fed minutes show dissent over inflation (USATODAY.com)”,”USATODAY.com - Retail sales bounced back a bit in July, and new claims for jobless benefits fell last week, the government said Thursday, indicating the economy is improving from a midsummer slump.”
“3”,”Safety Net (Forbes.com)”,”Forbes.com - After earning a PH.D. in Sociology, Danny Bazil Riley started to work as the general manager at a commercial real estate firm at an annual base salary of #36;70,000. Soon after, a financial planner stopped by his desk to drop off brochures about insurance benefits available through his employer. But, at 32, “”buying insurance was the furthest thing from my mind,”” says Riley.”
“3”,”Wall St. Bears Claw Back Into the Black”,” NEW YORK (Reuters) - Short-sellers, Wall Street’s dwindling band of ultra-cynics, are seeing green again.”


  • 文件内容说明:
    • train.csv共由3列组成, 使用’,’进行分隔, 分别代表: 标签, 新闻标题, 新闻简述; 其中标签用”1”, “2”, “3”, “4”表示, 依次对应classes中的内容.
    • test.csv与train.csv内容格式与含义相同.

整个案例的实现可分为以下五个步骤

  • 第一步: 构建带有Embedding层的文本分类模型.
  • 第二步: 对数据进行batch处理.
  • 第三步: 构建训练与验证函数.
  • 第四步: 进行模型训练和验证.
  • 第五步: 查看embedding层嵌入的词向量.

第一步: 构建带有Embedding层的文本分类模型


# 导入必备的torch模型构建工具
import torch.nn as nn
import torch.nn.functional as F

指定BATCH_SIZE的大小
BATCH_SIZE = 16

进行可用设备检测, 有GPU的话将优先使用GPU
device = torch.device(“cuda” if torch.cuda.is_available() else “cpu”)

class TextSentiment(nn.Module):
“””文本分类模型”””
def init(self, vocabsize, embeddim, num_class):
“””
description: 类的初始化函数
:param vocab_size: 整个语料包含的不同词汇总数
:param embed_dim: 指定词嵌入的维度
:param num_class: 文本分类的类别总数
“””
super().__init
()
# 实例化embedding层, sparse=True代表每次对该层求解梯度时, 只更新部分权重.
self.embedding = nn.Embedding(vocab_size, embed_dim, sparse=True)
# 实例化线性层, 参数分别是embed_dim和num_class.
self.fc = nn.Linear(embed_dim, num_class)
# 为各层初始化权重
self.init_weights()

  1. def init_weights(self):<br /> """初始化权重函数"""<br /> # 指定初始权重的取值范围数<br /> initrange = 0.5<br /> # 各层的权重参数都是初始化为均匀分布<br /> self.embedding.weight.data.uniform_(-initrange, initrange)<br /> self.fc.weight.data.uniform_(-initrange, initrange)<br /> # 偏置初始化为0<br /> self.fc.bias.data.zero_()
  2. def forward(self, text):<br /> """<br /> :param text: 文本数值映射后的结果<br /> :return: 与类别数尺寸相同的张量, 用以判断文本类别<br /> """<br /> # 获得embedding的结果embedded<br /> # >>> embedded.shape<br /> # (m, 32) 其中m是BATCH_SIZE大小的数据中词汇总数<br /> embedded = self.embedding(text)<br /> # 接下来我们需要将(m, 32)转化成(BATCH_SIZE, 32)<br /> # 以便通过fc层后能计算相应的损失<br /> # 首先, 我们已知m的值远大于BATCH_SIZE=16,<br /> # 用m整除BATCH_SIZE, 获得m中共包含c个BATCH_SIZE<br /> c = embedded.size(0) // BATCH_SIZE<br /> # 之后再从embedded中取c*BATCH_SIZE个向量得到新的embedded<br /> # 这个新的embedded中的向量个数可以整除BATCH_SIZE<br /> embedded = embedded[:BATCH_SIZE*c]<br /> # 因为我们想利用平均池化的方法求embedded中指定行数的列的平均数,<br /> # 但平均池化方法是作用在行上的, 并且需要3维输入<br /> # 因此我们对新的embedded进行转置并拓展维度<br /> embedded = embedded.transpose(1, 0).unsqueeze(0)<br /> # 然后就是调用平均池化的方法, 并且核的大小为c<br /> # 即取每c的元素计算一次均值作为结果<br /> embedded = F.avg_pool1d(embedded, kernel_size=c)<br /> # 最后,还需要减去新增的维度, 然后转置回去输送给fc层<br /> return self.fc(embedded[0].transpose(1, 0))

  • 实例化模型:


# 获得整个语料包含的不同词汇总数
VOCAB_SIZE = len(train_dataset.get_vocab())
# 指定词嵌入维度
EMBED_DIM = 32
# 获得类别总数
NUN_CLASS = len(train_dataset.get_labels())
# 实例化模型
model = TextSentiment(VOCAB_SIZE, EMBED_DIM, NUN_CLASS).to(device)


第二步: 对数据进行batch处理


def generate_batch(batch):
“””
description: 生成batch数据函数
:param batch: 由样本张量和对应标签的元组组成的batch_size大小的列表
形如:
[(label1, sample1), (lable2, sample2), …, (labelN, sampleN)]
return: 样本张量和标签各自的列表形式(张量)
形如:
text = tensor([sample1, sample2, …, sampleN])
label = tensor([label1, label2, …, labelN])
“””
# 从batch中获得标签张量
label = torch.tensor([entry[0] for entry in batch])
# 从batch中获得样本张量
text = [entry[1] for entry in batch]
text = torch.cat(text)
# 返回结果
return text, label


  • 调用:


# 假设一个输入:
batch = [(1, torch.tensor([3, 23, 2, 8])), (0, torch.tensor([3, 45, 21, 6]))]
res = generate_batch(batch)
print(res)


  • 输出效果:


# 对应输入的两条数据进行了相应的拼接
(tensor([ 3, 23, 2, 8, 3, 45, 21, 6]), tensor([1, 0]))


第三步: 构建训练与验证函数


# 导入torch中的数据加载器方法
from torch.utils.data import DataLoader

def train(train_data):
“””模型训练函数”””
# 初始化训练损失和准确率为0
train_loss = 0
train_acc = 0

  1. # 使用数据加载器生成BATCH_SIZE大小的数据进行批次训练<br /> # data就是N多个generate_batch函数处理后的BATCH_SIZE大小的数据生成器<br /> data = DataLoader(train_data, batch_size=BATCH_SIZE, shuffle=True,<br /> collate_fn=generate_batch)
  2. # 对data进行循环遍历, 使用每个batch的数据进行参数更新<br /> for i, (text, cls) in enumerate(data):<br /> # 设置优化器初始梯度为0<br /> optimizer.zero_grad()<br /> # 模型输入一个批次数据, 获得输出<br /> output = model(text)<br /> # 根据真实标签与模型输出计算损失<br /> loss = criterion(output, cls)<br /> # 将该批次的损失加到总损失中<br /> train_loss += loss.item()<br /> # 误差反向传播<br /> loss.backward()<br /> # 参数进行更新<br /> optimizer.step()<br /> # 将该批次的准确率加到总准确率中<br /> train_acc += (output.argmax(1) == cls).sum().item()
  3. # 调整优化器学习率 <br /> scheduler.step()
  4. # 返回本轮训练的平均损失和平均准确率<br /> return train_loss / len(train_data), train_acc / len(train_data)

def valid(valid_data):
“””模型验证函数”””
# 初始化验证损失和准确率为0
loss = 0
acc = 0

  1. # 和训练相同, 使用DataLoader获得训练数据生成器<br /> data = DataLoader(valid_data, batch_size=BATCH_SIZE, collate_fn=generate_batch)<br /> # 按批次取出数据验证<br /> for text, cls in data:<br /> # 验证阶段, 不再求解梯度<br /> with torch.no_grad():<br /> # 使用模型获得输出<br /> output = model(text)<br /> # 计算损失<br /> loss = criterion(output, cls)<br /> # 将损失和准确率加到总损失和准确率中<br /> loss += loss.item()<br /> acc += (output.argmax(1) == cls).sum().item()
  2. # 返回本轮验证的平均损失和平均准确率<br /> return loss / len(valid_data), acc / len(valid_data)

第四步: 进行模型训练和验证


# 导入时间工具包
import time

导入数据随机划分方法工具
from torch.utils.data.dataset import random_split

指定训练轮数
N_EPOCHS = 10

定义初始的验证损失
min_valid_loss = float(‘inf’)

选择损失函数, 这里选择预定义的交叉熵损失函数
criterion = torch.nn.CrossEntropyLoss().to(device)
# 选择随机梯度下降优化器
optimizer = torch.optim.SGD(model.parameters(), lr=4.0)
# 选择优化器步长调节方法StepLR, 用来衰减学习率
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, 1, gamma=0.9)

从train_dataset取出0.95作为训练集, 先取其长度
train_len = int(len(train_dataset) * 0.95)

然后使用randomsplit进行乱序划分, 得到对应的训练集和验证集
sub_train
, subvalid = \
random_split(train_dataset, [train_len, len(train_dataset) - train_len])

开始每一轮训练
for epoch in range(NEPOCHS):
# 记录概论训练的开始时间
start_time = time.time()
# 调用train和valid函数得到训练和验证的平均损失, 平均准确率
train_loss, train_acc = train(sub_train
)
validloss, valid_acc = valid(sub_valid)

  1. # 计算训练和验证的总耗时(秒)<br /> secs = int(time.time() - start_time)<br /> # 用分钟和秒表示<br /> mins = secs / 60<br /> secs = secs % 60
  2. # 打印训练和验证耗时,平均损失,平均准确率<br /> print('Epoch: %d' %(epoch + 1), " | time in %d minutes, %d seconds" %(mins, secs))<br /> print(f'\tLoss: {train_loss:.4f}(train)\t|\tAcc: {train_acc * 100:.1f}%(train)')<br /> print(f'\tLoss: {valid_loss:.4f}(valid)\t|\tAcc: {valid_acc * 100:.1f}%(valid)')

  • 输出效果:


120000lines [00:06, 17834.17lines/s]
120000lines [00:11, 10071.77lines/s]
7600lines [00:00, 10432.95lines/s]

Epoch: 1 | time in 0 minutes, 36 seconds
Loss: 0.0592(train) | Acc: 63.9%(train)
Loss: 0.0005(valid) | Acc: 69.2%(valid)
Epoch: 2 | time in 0 minutes, 37 seconds
Loss: 0.0507(train) | Acc: 71.3%(train)
Loss: 0.0005(valid) | Acc: 70.7%(valid)
Epoch: 3 | time in 0 minutes, 36 seconds
Loss: 0.0484(train) | Acc: 72.8%(train)
Loss: 0.0005(valid) | Acc: 71.4%(valid)
Epoch: 4 | time in 0 minutes, 36 seconds
Loss: 0.0474(train) | Acc: 73.4%(train)
Loss: 0.0004(valid) | Acc: 72.0%(valid)
Epoch: 5 | time in 0 minutes, 36 seconds
Loss: 0.0455(train) | Acc: 74.8%(train)
Loss: 0.0004(valid) | Acc: 72.5%(valid)
Epoch: 6 | time in 0 minutes, 36 seconds
Loss: 0.0451(train) | Acc: 74.9%(train)
Loss: 0.0004(valid) | Acc: 72.3%(valid)
Epoch: 7 | time in 0 minutes, 36 seconds
Loss: 0.0446(train) | Acc: 75.3%(train)
Loss: 0.0004(valid) | Acc: 72.0%(valid)
Epoch: 8 | time in 0 minutes, 36 seconds
Loss: 0.0437(train) | Acc: 75.9%(train)
Loss: 0.0004(valid) | Acc: 71.4%(valid)
Epoch: 9 | time in 0 minutes, 36 seconds
Loss: 0.0431(train) | Acc: 76.2%(train)
Loss: 0.0004(valid) | Acc: 72.7%(valid)
Epoch: 10 | time in 0 minutes, 36 seconds
Loss: 0.0426(train) | Acc: 76.6%(train)
Loss: 0.0004(valid) | Acc: 72.6%(valid)


第五步: 查看embedding层嵌入的词向量


# 打印从模型的状态字典中获得的Embedding矩阵
print(model.state_dict()[‘embedding.weight’])


  • 输出效果:


tensor([[ 0.4401, -0.4177, -0.4161, ..., 0.2497, -0.4657, -0.1861],
[-0.2574, -0.1952, 0.1443, …, -0.4687, -0.0742, 0.2606],
[-0.1926, -0.1153, -0.0167, …, -0.0954, 0.0134, -0.0632],
…,
[-0.0780, -0.2331, -0.3656, …, -0.1899, 0.4083, 0.3002],
[-0.0696, 0.4396, -0.1350, …, 0.1019, 0.2792, -0.4749],
[-0.2978, 0.1872, -0.1994, …, 0.3435, 0.4729, -0.2608]])

小节总结

  • 学习了关于新闻主题分类任务:
    • 以一段新闻报道中的文本描述内容为输入, 使用模型帮助我们判断它最有可能属于哪一种类型的新闻, 这是典型的文本分类问题, 我们这里假定每种类型是互斥的, 即文本描述有且只有一种类型.

  • 学习了新闻主题分类数据的获取和样式.

  • 学习了整个案例的实现的五个步骤:
    • 第一步: 构建带有Embedding层的文本分类模型.
    • 第二步: 对数据进行batch处理.
    • 第三步: 构建训练与验证函数.
    • 第四步: 进行模型训练和验证.
    • 第五步: 查看embedding层嵌入的词向量.