词嵌入与循环神经网络入门

前言

在写论文的过程中,发现自己对词嵌入和循环神经网络这一方面的知识了解的并不深。针对这一现象,我使用keras。在这个框架上面实现了一些词嵌入和循环神经网络的技术。

词嵌入技术

之前,我们已经学习过了one hot编码技术,通过将数字进行二进制10表示该数字的编码,在其对应的坐标下。数字为一,其他的全部为零。但是这种编码方式主要的问题是他无法表现词汇之间的相似度。例如我们希望动物之间的词汇和人类之间的词汇,它们之间有一定区别。但是相同词汇之间有一定的相同。为了克服这种局限性,提出了一种信息检索的相关技术,词嵌入技术不同于信息检索技术,是在两千年发展而来的,并且他表现的文本向量化更为自然。最著名的词嵌入模型就是word2vec和GloVe。下面将分别介绍这两个方法。

word2vec

该方法是由谷歌公司提出的。这个模型是五监督模型,它以大型的文本语料作为输入,并生成词汇的向量空间。该方法生成的向量空间维度通常低于one hot纬度,向量空间更紧密一些。他有连续词袋模型和skip grammar两种方法。在连续词袋模型中,模型是通过周围的词预测当前的词,另外上下文词汇的顺序不会影响预测的结果。Cape grammar结构中模型通过中心的词汇预测周围的词汇。该方法更有效一些。但总的来说这两个都是浅层的神经网络。可以放到深度学习模型中一起训练,也可以使用迁移学习加载预训练的嵌入模型。
skip grammar原理举个例子: I like green apple and banana。我们假设窗口大小为三,这句话按照从中间找两边词原则可以被分为以下n组。

([I,green],like)
([green,and],apple)
([apple,banana],and)

skip grammar。模型是通过给定中间词预测周围词。我们可以把前面数据集转化成一个输入输出的数据组。也就是给定一个输入的词,我们希望这个模型可以预测输出的词为
(like,I),(like,green),(green,apple),(apple,and) …
我们也可以把每个输入词和字典当中的某个随机词组合一下构成负样本。例如,
(like,you),(apple,thing)…
接下来我们将正样本和负样本打上相应的标签。然后放入网络中去训练。训练好的产物就是此嵌入层的一个权重。该权重可以将输入的词匹配正确的相关词。

以上就是该方法的原理。另外一个方法是根据两边找中间。其原理和这个相类似。我们重点实现下面一个方法GloVe.

Glove

该方法和之前那个方法不同的是,word2wec是一个预测模型。而Glove是一个基于计数的模型。第一步是构造一个训练语料中出现共同的(word,context)矩阵,矩阵中的每个元素代表了词与周围内容共同出现的频率。需要注意的一点是,它与前面的方法思路上是一致的。都是构造了一个向量空间。其中词的位置会被它相邻词的内容所影响。在使用过程中,我们一般使用预训练好的词向量。因为该词向量收集了很多的词汇进行训练。除非你自己使用了大量的生僻文本,这时候你才需要从头训练。例如谷歌的word2vec是在超过10亿词汇上训练好的模型。这个模型呢词典大小约为三百万,输出向量维度为300。
在介绍之前需要了解一下keras的embedding:

Embedding(200, 32, input_length=50)

其中的embedded层就是词嵌入层,定义一个词汇量为200的嵌入层(例如,从0到199(包括整数)的整数编码单词),将词嵌入到32维的向量空间中,以及每次输入50个单词的句子。
下面我们将通过从头开始学习词向量或者使用预训练模型去预测单词的好坏评价。
从头开始学习词向量模型去预测

from keras.preprocessing.text import one_hot
from keras.preprocessing.sequence import pad_sequences
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import Flatten
from keras.layers.embeddings import Embedding
# define documents
docs = ['Well done!',
		'Good work',
		'Great effort',
		'nice work',
		'Excellent!',
		'Weak',
		'Poor effort!',
		'not good',
		'poor work',
		'Could have done better.']
# define class labels
labels = [1,1,1,1,1,0,0,0,0,0]  #打标签。
# integer encode the documents
vocab_size = 50
encoded_docs = [one_hot(d, vocab_size) for d in docs]#将上面的文档单词进行编码。
print(encoded_docs)
# pad documents to a max length of 4 words
max_length = 4    
padded_docs = pad_sequences(encoded_docs, maxlen=max_length, padding='post')#由于文档中的单词长度不一致,我们希望将长度修改成相同的长度话可以使用这个函数。
print(padded_docs)#  打印经过程度统一和编码之后的词向量
# define the model
model = Sequential()
model.add(Embedding(vocab_size, 8, input_length=max_length))#这是重点。我们将输入的长度为四的语句进行词嵌入。输出的结果是四个向量,每个向量是具有八个维度。也就是每个单词8个维度,需要注意。我们在之前已经统一单词的长度为四个。
model.add(Flatten())
model.add(Dense(1, activation='sigmoid'))
# compile the model
model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['acc'])
# summarize the model
print(model.summary())
# fit the model
model.fit(padded_docs, labels, epochs=50, verbose=0)
# evaluate the model
loss, accuracy = model.evaluate(padded_docs, labels, verbose=0)
print('Accuracy: %f' % (accuracy*100))

上面的代码我们是实现了。通过给一些词10的一个标签。来训练该词的一个结果。我们希望它可以识别文字当中好的单词和坏的单词。其结果如下,我们可以将学习之后的权重从嵌入层保存到文件当中,已备其他模型的使用。
在这里插入图片描述
使用预训练模型去预测
可以从网上找到一些预训练的模型。当然这些模型都是非常大的,他们基本上都在10万以上的数据量进行训练。最后输出的维度有20,100,200等不同的维度可以进行选择。具体的步骤如下。

扫描二维码关注公众号,回复: 11158563 查看本文章
from numpy import asarray
from numpy import zeros
from keras.preprocessing.text import Tokenizer
from keras.preprocessing.sequence import pad_sequences
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import Flatten
from keras.layers import Embedding
# define documents
docs = ['Well done!',
		'Good work',
		'Great effort',
		'nice work',
		'Excellent!',
		'Weak',
		'Poor effort!',
		'not good',
		'poor work',
		'Could have done better.']
# define class labels
labels = [1,1,1,1,1,0,0,0,0,0]
# prepare tokenizer
t = Tokenizer()
t.fit_on_texts(docs)
vocab_size = len(t.word_index) + 1
# integer encode the documents
encoded_docs = t.texts_to_sequences(docs)
print(encoded_docs)
# pad documents to a max length of 4 words
max_length = 4
padded_docs = pad_sequences(encoded_docs, maxlen=max_length, padding='post')
print(padded_docs)
# load the whole embedding into memory
embeddings_index = dict()
f = open('../glove_data/glove.6B/glove.6B.100d.txt')
for line in f:
	values = line.split()
	word = values[0]
	coefs = asarray(values[1:], dtype='float32')
	embeddings_index[word] = coefs
f.close()
print('Loaded %s word vectors.' % len(embeddings_index))
# create a weight matrix for words in training docs
embedding_matrix = zeros((vocab_size, 100))
for word, i in t.word_index.items():
	embedding_vector = embeddings_index.get(word)
	if embedding_vector is not None:
		embedding_matrix[i] = embedding_vector
# define model
model = Sequential()
e = Embedding(vocab_size, 100, weights=[embedding_matrix], input_length=4, trainable=False)
model.add(e)
model.add(Flatten())
model.add(Dense(1, activation='sigmoid'))
# compile the model
model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['acc'])
# summarize the model
print(model.summary())
# fit the model
model.fit(padded_docs, labels, epochs=50, verbose=0)
# evaluate the model
loss, accuracy = model.evaluate(padded_docs, labels, verbose=0)
print('Accuracy: %f' % (accuracy*100))

预先训练模型前面的步骤和从头开始的是一样的。但是我们并没有采用one hot编码。代码开头我们采用了Tokenizer(),其实原理是一样的,该方法可以配合训练数据,可以将文本转换为序列,通过调用texts_to_sequences()的方法标记生成器的类,并提供访问字的字典映射到整数在word_index属性。之后我们打开了输出为100维度的预训练模型权重文档。下载地址添加链接描述该文件里面储存了单词的预训练权重。我们通过一个循环将这些权重提取进来,放入了embedding层,输入还剩四个维度。但是我们的输出是修改成了100。因为我们采用的是预训练模型。其他的地方一致,如果你还有不清楚的地方,可以查看这篇博客添加链接描述专门介绍词嵌入的方法。

循环神经网络RNN

循环神经网络是用来解决时序问题的,比如我们在说一段话的时候,话中的当前单词肯定与之前的每一个单词有一定的关系。这种关系在卷积神经网络中是无法捕捉到的。准确网络只是用来捕捉空间上的特征,我们通过简单循环神经网络可以记录该节点与之前节点的关系,从而可以识别时序问题。但是由于该网络的输入过长的时候,他并不知道。哪一个时间节点是重点,哪一个时间节点是不重要的地方,只是一味的去记录之前的数据会导致时间间隔比较大的信息会丢失。因此我们提出了长短时记忆网络和门控循环单元网络。这两个网络可以很好的解决长序列问题。
RNN结构如下:

在这里插入图片描述
我们在t时刻的输入为xt,他相比较之前的网络中间多了一个W,这个w代表之前所有状态的合,注意这里千万不要以为只与前一时刻有关,要知道前一时刻的输入输出还与前前时刻有关。从这方面理解就可以理解到循环神经网络的含义。当t时刻隐藏层的神经元激活函数为st=f(uxt+wst-1+b1),输出的激活函数为ot=f(v*st+b2),训练过程中我们要学习w v,u和两个偏差。循环神经网络的有多种方式,不同的方式应用的领域也不同,例如多输入多输出的网络,既可以预测文本中下一个词值也可以用来文本翻译,预测文本中下一个单词。输入的是当前单词的序列输出的是下一个单词的序列。文本翻译的结构中,他们会出现有的单元没有输出,而有的单元是没有输入。

单输入多输出的网络可以实现用文字描述图片输入,多输入单输出可以用来做情感分析。参照下面这张图。
在这里插入图片描述
下面我们用一个简单的例子,利用keras实现Simplernn生成文本。也就是给一段文本生成后续固定长度的文本。实验中用到的文本数据是《爱丽丝梦游仙境》本文。爱丽丝,这个模型通过输入十个字符,预测下一个字符,因为他的字典数量比较小,训练了更快。但是概念和词的语言模型是一致的。具体步骤如下:
1.导入需要的模块。

from __future__ import print_function
from keras.layers import Dense, Activation
from keras.layers.recurrent import SimpleRNN
from keras.models import Sequential
import numpy as np

读取文件,排除空格等符号,将文件的内容解码成ASCII码。

# 读取文件,并将文件内容解码为ASCII码
fin = open('alice_in_wonderland.txt', 'rb')
lines = []		#
for line in fin:		# 遍历每行数据
	line = line.strip().lower()		# 去除每行两端空格
	line = line.decode('ascii', 'ignore')		# 解码为ASCII码
	if len(line) == 0:
		continue
	lines.append(line)
fin.close()
text = ' '.join(lines)

3.构建字符到所以之间的映射关系,创建标签和输入文本并将标签文本向量化。

chars = set([c for c in text])		# 获取字符串中不同字符组成的集合
nb_chars = len(chars)		# 获取集合中不同元素数量
char2index = dict((c, i) for i, c in enumerate(chars))		# 创建字符到索引的字典
index2char = dict((i, c) for i, c in enumerate(chars))		# 创建索引到字符的字典
SEQLEN = 10		# 超参数,输入字符串长度
STEP = 1		# 输出字符串长度
input_chars = []		# 输入字符串列表
label_chars = []		# 标签列表
for i in range(0, len(text) - SEQLEN, STEP):
	input_chars.append(text[i: i + SEQLEN])
	label_chars.append(text[i + SEQLEN])
# 将输入文本和标签文本向量化: one-hot编码
X = np.zeros((len(input_chars), SEQLEN, nb_chars), dtype=np.bool)		# 输入文本张量
Y = np.zeros((len(input_chars), nb_chars), dtype=np.bool)		# 标签文本张量
for i, input_char in enumerate(input_chars):		# 遍历所有输入样本
	for j, ch in enumerate(input_char):		# 对于每个输入样本
		X[i, j, char2index[ch]] = 1
	Y[i, char2index[label_chars[i]]] = 1


4.构建网络去训练。

# 构建模型
HIDDEN_SIZE = 128
BATCH_SIZE = 128
NUM_ITERATIONS = 25
NUM_EPOCH_PER_ITERATIONS = 1
NUM_PREDS_PER_EPOCH = 100

model = Sequential()
model.add(SimpleRNN(HIDDEN_SIZE, return_sequences=False, input_shape=(SEQLEN, nb_chars), unroll=True))
model.add(Dense(nb_chars))
model.add(Activation('softmax'))
# 编译模型
model.compile(loss='categorical_crossentropy', optimizer='rmsprop')

for iteration in range(NUM_ITERATIONS):
	print('=' * 50)
	print('Iteration #: %d' % (iteration))		# 打印迭代次数
	# 训练模型
	model.fit(X, Y, batch_size=BATCH_SIZE, epochs=NUM_EPOCH_PER_ITERATIONS)		# 每次迭代训练一个周期
	# 使用模型进行预测
	test_idx = np.random.randint(len(input_chars))		# 随机抽取样本
	test_chars = input_chars[test_idx]
	print('Generating from seed: %s' % (test_chars))
	print(test_chars, end='')		# 不换行输出
	for i in range(NUM_PREDS_PER_EPOCH):		# 评估每一次迭代后的结果
		Xtest = np.zeros((1, SEQLEN, nb_chars))
		for i, ch in enumerate(test_chars):
			Xtest[0, i, char2index[ch]] = 1		# 样本张量
		# pred = model.predict(Xtest, verbose=0)[0]
		# ypred = index2char[np.argmax(pred)]		# 找对类别标签对应的字符
		ypred = index2char[model.predict_classes(Xtest)[0]]
		print(ypred, end='')
		# 使用test_chars + ypred继续
		test_chars = test_chars[1:] + ypred
print()

在这里插入图片描述
测试的时候,我们把一个周期作为一次测试,持续25次epoch.我们发现有意义的输出就停止,因而我们可以高效地将模型训练到满意为止。我们的测试是首先对给定一个随机输入,从模型中生成下一个字符。然后把第一个字符从输入中丢弃,把预测的字符加在之后再进行预测,持续了100次之后。打印输出的结果,这个字符串就表明了模型的质量。运行结果中可以看出模型开始的时候预测的毫无意义。但是随着次数的加深,它可以进行很好的拼写。我们本身是对一个字符做预测。但是经过训练之后我们可以发现。这个字符已经可以完成了单词的拼写。至于这些单词看起来好像和原文中差不多。实际上每个单词最后一个字符都很可能是砍掉的。

长短期记忆网络LSTM

长短期记忆网络是循环神经网络的一个变体。他能够学习长期依赖。我们可以看到在之前的rnn中tanh层使用前一时间状态和当前输入来实现递归。长短期记忆网络也是用类似的方法实现了递归,但是并没有那么简单,它是通过输入门i遗忘门f输出门o三个门来控制持续信息。网络模型如下。
在这里插入图片描述

在这里插入图片描述
该网络看起来有点复杂,让我们逐个来查看组件。图中最上层的是单元状态ct。他表示单元的内部记忆。图下ht表示隐藏状态。三个门虽然他们用的公式和参数是一致的,但是他们内部的参数w和b是各不相同的。每个门中的参数都是针对它特定的功能学习的。sigmod层让这些萌的输出。映射到零和一之间。因此产生的输出向量可以和单元状态ct-1等定最后的输出ht,并更新其内部的w和b。。遗忘门定义了你希望前一个状态ht-1多少可以通过。输出门是定义了你想把当前状态的多少部分留给下一层。内部隐藏状态g基于当前输入xt和前一状态ht-1计算的。注意g对的等式和前一小节的simplernn单元相同。但在这里我们是通过输入门i产生的输出来进行调节。该网络中,如果将一些门进行关闭可以设置为零。例如我们将遗忘门设置为零,表示忽略所有的旧记忆。输入门为零的情况下表示忽略新计算出的状态。在使用过程中,我们可以放心的用长短期记忆网络代替掉普通的循环神经网络。
下面我们利用长短期记忆网络进行情感分析。keras提供一个LSTM层,用它来构造和训练一个多对一的RNN。我们的网络吸收一个序列(词序列)并输出一个情感分析值(正或负)。
训练集源自于kaggle上情感分类竞赛,包含7000个短句 UMICH SI650
每个句子有一个值为1或0的分别用来代替正负情感的标签,这个标签就是我们将要学习预测的。

from keras.layers.core import Activation, Dense, Dropout, SpatialDropout1D
from keras.layers.embeddings import Embedding
from keras.layers.recurrent import LSTM
from keras.models import Sequential
from keras.preprocessing import sequence
from sklearn.model_selection import train_test_split
import collections
import matplotlib.pyplot as plt
import nltk
import numpy as np
import os
# Read training data and generate vocabulary
maxlen = 0
word_freqs = collections.Counter()
num_recs = 0
ftrain = open(os.path.join(./data, "umich-sentiment-train.txt"), 'rb')
for line in ftrain:
    label, sentence = line.strip().split("\t")
    words = nltk.word_tokenize(sentence.decode("ascii", "ignore").lower())
    if len(words) > maxlen:
        maxlen = len(words)
    for word in words:
        word_freqs[word] += 1
    num_recs += 1
ftrain.close()
'''
首先导入了所需的库。之后我们将我们这里面的每个句子进行统计。经过统计,我们可以得到语料的值
maxlen: 42
len(word_freqs): 2313
我们将单词总数量设为固定值,并把所有其他词看作字典外的词,这些词全部用伪词unk(unknown)替换,预测时候将未见的词进行替换
句子包含的单词数(maxlen)让我们可以设置一个固定的序列长度,并且用0进行补足短句,把更长的句子截短至合适的长度。
把VOCABULARY_SIZE设置为2002,即源于字典的2000个词,加上伪词UNK和填充词PAD(用来补足句子到固定长度的词)
这里把句子最大长度MAX_SENTENCE_LENGTH定为40

'''

MAX_FEATURES = 2000
MAX_SENTENCE_LENGTH = 40

'''
下一步我们需要两个查询表,RNN的每一个输入行都是一个词序列索引,索引按训练集中词的使用频度从高到低排序。这两张查询表允许我们通过给定的词来查找索引以及通过给定的索引来查找词。
'''
# 1 is UNK, 0 is PAD
# We take MAX_FEATURES-1 featurs to accound for PAD
vocab_size = min(MAX_FEATURES, len(word_freqs)) + 2
word2index = {x[0]: i+2 for i, x in 
                enumerate(word_freqs.most_common(MAX_FEATURES))}
word2index["PAD"] = 0
word2index["UNK"] = 1
index2word = {v:k for k, v in word2index.items()}

'''
接着我们将序列转换成词索引序列
补足MAX_SENTENCE_LENGTH定义的词的长度
因为我们的输出标签是二分类(正负情感)
'''
X = np.empty((num_recs, ), dtype=list)
y = np.zeros((num_recs, ))
i = 0
ftrain = open(os.path.join(DATA_DIR, "umich-sentiment-train.txt"), 'rb')
for line in ftrain:
    label, sentence = line.strip().split("\t")
    words = nltk.word_tokenize(sentence.decode("ascii", "ignore").lower())
    seqs = []
    for word in words:
        if word2index.has_key(word):
            seqs.append(word2index[word])
        else:
            seqs.append(word2index["UNK"])
    X[i] = seqs
    y[i] = int(label)
    i += 1
ftrain.close()


'''
划分测试集与训练集
训练模型
最后我们在测试集上评估模型并打印出评分和准确率
'''
# Pad the sequences (left padded with zeros)
X = sequence.pad_sequences(X, maxlen=MAX_SENTENCE_LENGTH)
# Split input into training and test
Xtrain, Xtest, ytrain, ytest = train_test_split(X, y, test_size=0.2, 
                                                random_state=42)
print(Xtrain.shape, Xtest.shape, ytrain.shape, ytest.shape)
EMBEDDING_SIZE = 128
HIDDEN_LAYER_SIZE = 64
# 批大小32
BATCH_SIZE = 32
# 网络训练10轮
NUM_EPOCHS = 10

# Build model
model = Sequential()
model.add(Embedding(vocab_size, EMBEDDING_SIZE, 
                    input_length=MAX_SENTENCE_LENGTH))
model.add(SpatialDropout1D(Dropout(0.2)))
model.add(LSTM(HIDDEN_LAYER_SIZE, dropout=0.2, recurrent_dropout=0.2))
model.add(Dense(1))
model.add(Activation("sigmoid"))

model.compile(loss="binary_crossentropy", optimizer="adam", 
              metrics=["accuracy"])

history = model.fit(Xtrain, ytrain, batch_size=BATCH_SIZE, 
                    epochs=NUM_EPOCHS,
                    validation_data=(Xtest, ytest))
# evaluate
score, acc = model.evaluate(Xtest, ytest, batch_size=BATCH_SIZE)
print("Test score: %.3f, accuracy: %.3f" % (score, acc))

for i in range(5):
    idx = np.random.randint(len(Xtest))
    xtest = Xtest[idx].reshape(1,40)
    ylabel = ytest[idx]
    ypred = model.predict(xtest)[0][0]
    sent = " ".join([index2word[x] for x in xtest[0].tolist() if x != 0])
    print("%.0f\t%d\t%s" % (ypred, ylabel, sent))

该网络架构如图:

在这里插入图片描述

结果如图:
在这里插入图片描述

该实验小节参照keras深度学习实战(图书)和博客

除了上述最主要的两个循环神经网络之外,还有一些变体。例如gru它针对长短期记忆网络进行了一个优化。其内部的结构更为简单,更新隐藏状态时需要的计算也更少。还有双向循环神经网络,它不仅取决于之前的输入还取决于未来的输出。有状态循环神经网络,它能够在训练中维护跨批次的状态信息,一般默认他是关闭的,如果我们的批处理是能够反映数据的周期性。那么有状态循环神经网络。会大大的减少训练的时间并且让网络更小。

结束!

原创文章 68 获赞 134 访问量 5万+

猜你喜欢

转载自blog.csdn.net/liupeng19970119/article/details/104905376