RNN模型与NLP应用笔记(2):文本处理与词嵌入详解及完整代码实现(Word Embedding)

一、写在前面

紧接着上一节,现在来讲文本处理的常见方式。

本文大部分内容参考了王树森老师的视频内容,再次感谢王树森老师和李沐老师的讲解视频。

目录

一、写在前面

二、引入

三、文本处理基本步骤详解

四、关键部分代码详解

五、结语

六、代码实现

七、参考资料


二、引入

此小节具体讲文本处理与Word Embedding(词嵌入)。

1)数据集介绍:

这节以及接下来的两节,我们都会IMDB电影评论的数据,并且搭建机器学习模型来分析电影评论。IMDB是最有名的电影评论网站,用户可以在IMDB上给电影打分,1分是非常讨厌,10分是非常喜欢,如果用户乐意,还可以写一段电影评论。

数据集下载地址:

• http://ai.stanford.edu/~amaas/data/sentiment/

• http://s3.amazonaws.com/text-datasets/aclImdb.zip

2)背景

假设我们不给你看分数,只给你看评论,你(人们)大概能猜到用户打的分数,但你的猜测可能不太准确,如果换种方式让你判断电影评论是正面还是负面的,你应该会有很高的准确率。

有人从IMDB上爬了5万条电影评论,这些电影评论都是很极端的,都是强烈的喜欢或者强烈反感,这个二分类问题对人来说很简单,人读一下电影评论就能轻易知道这是正面评价还是负面评价,人应该能有百分之百的准确率。

如图,这个数据即分成两半,25000条作为训练数据,另外25000条作为测试数据,在做Word Embedding和搭机器学习模型之前, 我们要做文本处理,把文本变成Sequence。

文本处理是模型进行训练前的关键步骤,文本处理的好坏直接影响积极学习模型的准确率。

三、文本处理基本步骤详解

回顾上一节,我们可以知道:

文本处理的第一步是tokenization把文本分割成很多tokens 这里我们把文本分割成单词 一个token就是一个单词。假如你把文本分割成字符,那么一个token就是一个字符。

做完tokenization后,一个很长的字符串就被分割成了一个单词的列表,tokenization看似简单,但是讲究很多,具体例子如下图所示。

例子1:是否应该把大写变成小写呢。通常应该把大写变成小写,因为大小写单词通常是一个意思,但有时候会混淆单词。比如:又如大小写apple并不是相同的单词,大写的Apple是苹果公司,小写的apple是水果

例子2:有些应用会去掉stop words(停词),比如stop words是the、a、off等最高频的单词,这些单词出现在几乎所有的句子里,这些词对二分类是没有任何帮助的。

【PS:停用词是指在信息检索中,为节省存储空间和提高搜索效率,在处理自然语言数据(或文本)之前或之后会自动过滤掉某些字或词,这些字或词即被称为Stop Words(停用词)。】

例子3:应该怎么样做拼写纠错呢?用户发twitter或者写电影评论的时候并不会仔细检查,所以写的东西难免会有拼写错误,做Typo correction(文本纠错)通常是有帮助的,这里我们只是举了几个例子。

实际做tokenization的时候需要做大量的工作。tokenization看似简单但实际上并不容易。

第二步是建立一个字典,可以首先统计词频,去掉低频词,然后让每个单词对应一个正整数 比如,让the对应数字1,让cat对应数字2,让set对应数字3。(如图所示)

有了字典,就可以把每个单词映射到一个整数,这样一来一句话就可以用一个正整数的列表表示这个列表被称为sequence即序列

如图,如果有必要的话,还得进一步做One-hot Encoding把单词表示成One-hot向量。

在电影评论的例子里,数据是5万条电影评论,每条电影评论可以表示成一个字符串,tokenizationOne-hot Encoding

条电影评论都会被转换成一个sequence,也就是一个正整数的列表。

因为电影评论有长有短,如下图,有人只写几句话的评论,有人的洋洋洒洒写几千个词,所以得到这些sequence的长度也各不相同。

具体举例:下图这两条sequence长度分别是52和90,这就造成一个问题,训练数据没有对齐。

如下图,每条sequence都有不同的长度,做机器学习的时候,我们得把数据存储在矩阵或者张量里,这就要求把序列对齐,每条序列都得有相同的长度

第四步为对齐序列

解决方案是这样的,我们可以固定长度为w,(设置词数阈值为w)

假如一个序列太长,超过了w个词,就砍掉前面的词,只保留最后w个词,当然如果想保留最前面w个词也同样可以(根据不同场景进行调整)。

如图,假如一个序列太短,不到w个词,那么就做zero pading操作用0来补齐把长度增加到w,

如图,通过设置阈值,我们调整了序列的长度,所有序列的长度都是w,于是都可以存储在一个矩阵里,文本处理已经完成了,现在每一个词都用一个正整数来表示。

第五步是Word Embedding操作:把单词进一步表示成一个低维向量,现在每一个单词都用一个数字来表示。

如图,该怎么样把这些类别特征(用一个整数来表示)变成数值向量呢?

我们显然可以One-hot Encoding操作。根据上一章节的知识,我们可以用一个One-hot向量来表示一个单词,比如单词good的index是2,于是就用标准正交积e2来表示,它的第二个元素是1,其余元素全都是0。

如图,假如vocabulary等于v,也就说字典里共有v个单词,那么就需要维度等于v的One-hot向量。

要是字典里有10000个单词,那么这些One-hot向量就都是10000维度的,这样的向量维度太高了,之后说到RNN的时候,我们会看到RNN的参数数量正比于输入向量的维度,我们肯定不想让输入的向量是10000维的,否则一层RNN(循环神经网络)至少有好几十万个参数,于是我们要做Word Embedding操作把这些高维度的One-hot向量映射到低维度的向量。

如图,具体做法是把One-hot向量ei乘到参数矩阵P的转至矩阵上,P转至的矩阵大小是d×v,d是词向量的维度,由用户自己决定,v是vocabulary(字典)里单词的数量。

矩阵乘法的结果记作向量xi,此处的xi就是一个词向量,如下图,维度d如果One-hot向量e的第三个元素是1,那么xi就是P转制矩阵的第三列,可以看出P的转制矩阵每一列都是一个词向量。(此处的参数矩阵是由训练数据产生的)

同理,如下图,这个参数矩阵P的每一行是一个词向量,这个矩阵的函数v,也就是vocabulary 每一行对应一个单词,矩阵的列数d,d是用户决定的,d的大小会影响机器学习模型的表现,应该用Cross Validation(交叉验证)来选择一个比较好的d。

又如下图,字典里的第一个词是movie,那么参数矩阵P的第一行就是movie的词向量,字典里第二个词是good,那么第二行就是good的词向量。

我们的任务是判断电影的评论是正面的还是负面的,这个参数矩阵是从训练数据中学习出来的,所以学出的词向量会带有感情色彩。

如下图,假如这些词向量都是二维的,我们就可以在平面坐标系中标出这些词向量,fantastic、good放这些词都带有正面的情感,所以这三个词学出来的词向量应该比较接近

同理,poor、boring、mediocre这些磁带有负面情感,所以学出来的词向量也应该比较接近,但是这些词的词向量应该远离正面色彩的词向量

同时,像movie这样的中性词没有感情色彩,应该在二维坐标轴的中间

此处相当于,我们一般只关注目标,即只需要关注用户评论中有关情感信息的词,则可直接用One hot向量和词向量矩阵的乘积筛选出该评论中对应的包含情感信息的词,得到Xi以此来进行判断。(此处的P参数矩阵相当于我们的关键词,可根据这些关键词判断语义信息,ei则是代表的评论经过文本处理后生成的向量)

四、关键部分代码详解

1)文本处理

此处直接列出对应的代码实现,具体看代码注释(具体第七部分)

                                                                                                            

2Word Embedding与线性回归模型代码实现

在编码实现过程中,Keras提供Embedding层,用户需要指定vocabulary的大小v,词向量的维度d,以及每一个sequence的长度,我们处理数据的时候, 设置词汇量v= 10000以及每个电影评论用最后20个词,此处设置词向量的维度d=8。

如下图,d是通过Cross Validation(交叉验证)选出来的, Embedding层的输出是一个20×8的矩阵,每个电影评论中有20个单词,每个单词用8维的词向量来表示。

同时我们可以发现,Embedding层的参数数量等于80000,这是这么算出来的呢?

实际上Embedding层中有一个参数矩阵P,矩阵的行数等于vocabulary,我使用10000个常用词,所以矩阵有10000行,矩阵的列数d是词向量的维度,此处设置d=8,所以这个矩阵的大小就是10000×8=80000,Embedding层一共有80000个参数。

到这一步 我们已经完成了文本处理和Word Embedding 保留每个电影评论最后20个单词, 每个单词用一个八维的词向量来表示,现在我们用Logistic Regression做二分类,以判断电影评论是正面的还是负面的,用这几行Keras代码就能实现一个分类器

如上图,首先,从Keras的库里面导入Sequential模型,Sequential的意思是把神经网络的层按顺序搭起来, 然后从Keras到Layers里面导入flatten、dense以及Embedding这三种层, 设置词向量的维度d=8,现在开始搭网络,调用Sequential返回Model这个对象,接下来往Model里面依次添加各种层。

第一层是Embedding层 Embedding层的输出是个20×8的矩阵,每条电影评论中有20个单词,每个单词用8维的向量来表示

第二层是Flatten层, 相当于把20×8的矩阵压扁(压缩成一维向量),此处变成160维的向量。
最后一层是dance,全连接层输出是一维的,用sigmoid的激活函数,这一层的输出是介于0-1之间的数,0代表负面评价,1代表正面评价,这样一个全连接层其实就是老Logistic Regression(线性分类)

用summary函数可以打印出模型的概要,模型的概要显示出每一层的名字,输出的大小以及参数的数量。

如下图,接下来编译模型,用model.compile这个函数指定优化算法是Rimsprob,损失函数是crossentropy,评价标准是accuracy,然后用训练数据来拟合模型,我把训练数据随机划分成了train和validation两部分,训练数据x_train与y_train,其中包含2万条电影评论。validation数据 x_valid和y_valid,包含5000条电影评论,x_train是个20000×20的矩阵,此处20的意思是每条电影评论中有20个单词,每个单词用一个正整数表示 。

如下图,运行model.fit函数Keras就开始训练模型了,让训练进行50个epochs,此处epoch=50的意思是这样的,把训练数据全都扫50遍,运行model.fit函数,每完成一个epoch,Keras就会输出一条信息,里面包括train_loss和train_accuracy以及validations_loss和validations_accuracy

问题来了,为什么要设置epoch=50,而不是20或者1000呢?

如下图,左右两张图里分别画出accuracy和loss,左边图里蓝色的曲线是训练准确率一直增长85%,而且还有上升的趋势,红色曲线是validation准确率增长到74%就不再增长了,这就是为什么我让训练进行50个epoch,如果我用20个epoch,validation准确率还没有达到最优,如果我用1000个epoch,validation准确率也不会更好,只会白白浪费计算时间,完成训练之后要在测试挤上检验模型的表现。用model.evaluate函数把测试数据作为输入函数 会返回loss和accuracy,测试的accuracy大约在75%。

五、结语

如图,假如做随机猜测,分类准确率是50%,这个简单的模型的准确率是75% ,比盲猜要好不少 这说明模型学到的东西,然而,75%还远不如人的表现,人可以达到100%的准确率,但75%也不是太差,毕竟只用了每条电影评论最后20个单词

总结:

  1. 文本处理。

如图:给我们一条电影评论,我们首先做tokenization 把电影评论分割成很多单词,然后把每个单词编码成一个数字,这样一来一条电影评论,就可以用一个正整数的序列来表示,这个正整数的序列叫做sequence,sequence就是神经网络中引Embedding层的输入。

由于电影评论的长短不一得到的sequence长短也不一样,没办法存储在一个矩阵里,解决方案是Alignment对齐,假如一条评论长度大于20就只保留最后20个单词,假如长度不到20就用0补齐,把长度增加到20。这样一来,每个sequence长度都是20。

  1. Word Embedding以及用神经网络对电影评论进行分类

Embedding层把每个单词映射到一个低维的词向量,所以每个长度为20的电影评论就用一个20×8的矩阵来表示。接下来,用一个flatten层把20×9的矩阵压扁,变成160位的向量,最后用线性回归分类器输出一个0-1之间的数,0代表负面评价,1代表正面评价。

我们来数一下模型参数Embedding层有一个参数矩阵, 大小是10000×8,10000是字典里单词的个数,也就是vocabulary,8是词向量的维度,每个单词被映射成一个八维的词向量,这个线性分类器有161个参数,分类器的输入是160维的向量,所以分类器有一个160维的参数向量,分类器还有一个interception 也叫bias 或者偏移量,所以分类器一共有161个参数

下一节可以正式开始循环神经网络RNN了

六、代码实现

以下代码实现均已跑通,分别从Imdb自带API的方式和从零开始实现的方式进行实现。

如下为Glove相关数据下载地址:

GloVe: Global Vectors for Word Representation (stanford.edu)

from keras.preprocessing.text import Tokenizer
import pandas as pd
import os
from keras.datasets import imdb
from keras import preprocessing

'举例操作:将词典和index对应'
samples = ['He is an engineer.', 'He uses PC to work.']

tokenizer = Tokenizer(num_words=1000)  # 创建分词器,设置为只考虑前1000个最常见的单词
# num_words设置成 vocabulary,最后返回的是最常见的、出现频率最高的num_words个字词
# 需要保留的最大词数,基于词频。只有最常出现的 num_words 词会被保留,
tokenizer.fit_on_texts(samples)  # 构建单词索引,为每个单词分配一个整数(根据词典标签)
# 只有前1000个词频最高的词才会被保留,相当于根据samples的内容,分配索引(每个词对应一个整数)
# fit_on_texts使用一系列文档来生成token词典,texts为list类,每个元素为一个文档。
# #num_words:处理的最大单词数

sequences = tokenizer.texts_to_sequences(samples)  # 将字符串转换为整数索引组成的列表
# 也就是得到词索引列表
#texts_to_sequences(texts) 将多个文档转换为word下标的向量形式,
# shape为[len(texts),len(text)] -- (文档数,每条文档的长度)

one_hot_results = tokenizer.texts_to_matrix(samples, mode='binary')
# 不仅能使用二进制,也支持one-hot外的向量化编码模式,矩阵化=one_hot编码操作

word_index = tokenizer.word_index  # 查看单词索引,此处为给samples标记的内容
print('独立标签数:', len(word_index))
print(word_index)
print(one_hot_results)

'嵌入层(Embeding)实现'
#1.利用Embedding层学习词嵌入
#imdb数据集,使用keras的embedding层处理文字数据
##将评论限制为10000个常见单词,评论长度限制为20个单词,每个单词学习一个8维嵌入
from keras.datasets import imdb
from keras import preprocessing

max_features = 10000 #作为特征的单词个数
maxlen = 20 #在20个单词后截断文本,这些单词都属于上一行的最常见单词

(x_train, y_train), (x_test, y_test) = imdb.load_data(num_words=max_features)
#x_train表示的是训练数据(评论列表,每条评论由数字组成,对应单词在词典中出现的位置下标),
# y_train表示的是训练的标签,标签0表示负面评论,1表示正面评论
#num_words定义的是大于该词频的单词会被读取,词频越高的单词,其在单词表中的位置越靠前

#将整数序列进行对齐,也就是对列表进行填充或者裁切
#imdb数据集中,单词的索引是从1开始的,不是从0开始的,所以可以使用0填充序列
x_train = preprocessing.sequence.pad_sequences(x_train, maxlen = maxlen)
x_test = preprocessing.sequence.pad_sequences(x_test, maxlen = maxlen)
#只使用了前20个单词作为判断依据,之前imdb的划分是基于全部评论

'利用Embedding层学习词嵌入(定义模型)'
from keras.models import Sequential# Sequential为将神经网络的层按顺序搭起来
from keras.layers import Dense,Embedding,Flatten

model = Sequential()
#第一层是Embedding层,设定字典里10000个单词,Embedding层的输出是个20×8的矩阵,
#只考虑每条电影评论中最后的20个单词,每个单词用8维的向量来表示
#参数矩阵在此的维度是80000,矩阵的参数根据设定的每个单词表示的向量(8)*字典词个数10000得到
model.add(Embedding(10000,8,input_length=maxlen))
model.add(Flatten())##将三维的嵌入张量展平为形状为(samples,maxlen*8)的二维张量
model.add(Dense(1, activation='sigmoid')) #添加分类器
model.compile(
    optimizer='rmsprop',
    loss='binary_crossentropy',
    metrics=['accuracy']
)

history = model.fit(x_train, y_train,
          epochs = 10,
          batch_size = 32,
          validation_split = 0.2)


'从零开始动手实现'
#从IMDB的原始文本开始学习,而不使用keras自带的imdb数据库。
'处理IMDB原始数据的内容和标签对应'
imdb_dir = r'E:\python\data\aclImdb\aclImdb'  # 根据自己的情况改进
train_dir = os.path.join(imdb_dir, 'train')  # 因为训练数据集分为train和test,所以需要进行分类

# train中又分为积极pos和消极neg两类型的评价,因此需要进行区分
labels = []
texts = []

for label_type in ['neg', 'pos']:  # 划分评论的正反倾向,并存到不同的文件
    # 遍历两个类别的数据集
    dir_name = os.path.join(train_dir, label_type)
    for fname in os.listdir(dir_name):
        # os.listdir() 方法用于返回指定的文件夹包含的文件或文件夹的名字的列表(返回文件名)
        if fname[-4:] == '.txt':  # 判断后四位是不是.txt
            f = open(os.path.join(dir_name, fname), encoding='utf-8')
            texts.append(f.read())
            f.close()
            if label_type == 'neg':  # 此处进行标签的工作(对应txt附上不同标签)
                labels.append(0)
            else:
                labels.append(1)
                # 1表示积极评论,0表示消极评论

# 对imdb原始数据的文本进行分类
from keras.preprocessing.text import Tokenizer #分词器
from keras.preprocessing.sequence import pad_sequences
import numpy as np

max_len = 500 #100个单词后截断评论,这个参数非常影响准确度
training_samples = 2000 #在2000个样本中训练
validation_samples = 10000 #在10000个样本上验证
max_words = 10000 #只考虑数据集中前10000个最常见的单词

tokenizer = Tokenizer(num_words = max_words)
tokenizer.fit_on_texts(texts)
sequences = tokenizer.texts_to_sequences(texts)

word_index = tokenizer.word_index

data = pad_sequences(sequences, maxlen = max_len)

labels = np.asarray(labels) #结构数据转化为ndarray
print('shape of data tensor:', data.shape)
print('shape of label tensor:', labels.shape)

#将数据划分为训练集和验证集
#注意要打乱数据,因为原始数据是分类排好序的
indices = np.arange(data.shape[0])
np.random.shuffle(indices)
data = data[indices]
labels = labels[indices]

x_train = data[:training_samples]
y_train = labels[:training_samples]
x_val = data[training_samples:training_samples+validation_samples]
y_val = labels[training_samples:training_samples+validation_samples]


#解析glove词嵌入文件,构建一个将单词(字符格式)映射为其向量表示(数值向量)的索引

glov_dir = r'\data\imdb'

embeddings_index = {}
f = open(os.path.join(glov_dir, 'glove.6B.100d.txt'), encoding= 'utf8')
for line in f:
    values = line.split()
    word = values[0]
    coefs = np.asarray(values[1:], dtype='float32')
    embeddings_index[word] = coefs
f.close()

print('find %s word vectors' %len(embeddings_index))

'准备GloVe词嵌入矩阵:'

#构造可以加载到embedding层中的嵌入矩阵
embedding_dim = 100
embedding_matrix = np.zeros((max_words, embedding_dim))
for word, i in word_index.items():
    if i < max_words:
        embedding_vector = embeddings_index.get(word)
        if embedding_vector is not None:
            embedding_matrix[i] = embedding_vector

'定义模型:'
from keras.models import Sequential
from keras.layers import Embedding, Dense, Flatten

model = Sequential()

model.add(Embedding(max_words, embedding_dim, input_length=max_len)) #100维
model.add(Flatten())
model.add(Dense(32, activation='relu'))
model.add(Dense(1, activation='sigmoid'))
model.summary()
##将准备好的glove矩阵加载到Embedding层,同时冻结Embedding层,道理和预处理方法都是一样的
model.layers[0].set_weights([embedding_matrix])
model.layers[0].trainable = False #冻结embedding层

'编译模型'
model.compile(
    optimizer='rmsprop',
    loss = 'binary_crossentropy',
    metrics = ['acc']
)

history = model.fit(
    x_train, y_train,
    epochs = 10,
    batch_size = 32,
    validation_data = (x_val, y_val)
)
model.save_weights('pre_trained_glove_model.h5')

七、参考资料

https://github.com/wangshusen/DeepLearning

tf.keras.preprocessing.sequence.pad_sequences()用法_我家空空的博客-CSDN博客

Keras---text.Tokenizer:文本与序列预处理_图特摩斯科技的博客-CSDN博客_text.tokenizer

https://zhuanlan.zhihu.com/p/59257654

  • 17
    点赞
  • 42
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值