🔎大家好,我是Sonhhxg_柒,希望你看完之后,能对你有所帮助,不足请指正!共同学习交流🔎
📝个人主页-Sonhhxg_柒的博客_CSDN博客 📃
🎁欢迎各位→点赞👍 + 收藏⭐️ + 留言📝
📣系列专栏 - 机器学习【ML】 自然语言处理【NLP】 深度学习【DL】
🖍foreword
✔说明⇢本人讲解主要包括Python、机器学习(ML)、深度学习(DL)、自然语言处理(NLP)等内容。
如果你对这个系列感兴趣的话,可以关注订阅哟👋
文章目录
词嵌入是分布语义,类似于我们在前一章讨论的主题模型。与主题模型不同,词嵌入不适用于术语-文档关系。相反,词嵌入适用于较小的上下文,例如句子中的句子或标记的子序列。
词嵌入领域是一组快速发展的技术。最流行的技术Word2vec由 Tomas Mikolov 等人于 2013 年开发。在谷歌。从那时起,有很多研究(和炒作)。这个想法是您使用神经网络来构建语言模型。一旦学习了这个模型,您就可以将网络中的一些中间值作为输入项的表示。
在本章中,我们将看看 Word2vec 在代码中的实现。这将有助于我们清楚地了解这一系列技术的基本原理。我们将在更高的层次上讨论最近的方法,因为它们可能会占用大量资源。
Word2vec
深度学习背后的想法之一是隐藏层是数据的“更高级别”表示。这来自对视觉皮层的分析。随着信息从眼睛通过大脑传播,神经元似乎与更复杂的形状相关联。早期的神经元层只识别明暗点,后面的神经元识别线条和曲线,等等。使用这个假设,如果我们使用神经网络训练语言模型,隐藏层将是单词的“更高级别”表示。
Word2vec 通常有两种实现方式:连续词袋(CBOW)和连续跳过克(通常只是跳过克)。在 CBOW 中,我们构建了一个模型,该模型试图根据附近的词来预测一个词。在skip-gram方法中,一个词被用来预测上下文。
在任何一种方法中,模型都是使用具有一个隐藏层的神经网络进行训练的。假设我们想将单词表示为K
维度向量,假设我们N
的词汇表中有单词。我们学习的权重将成为向量。这背后的直觉是基于神经网络的功能。神经网络学习输入特征的高级表示。这些更高级别的表示是评估神经网络模型时产生的中间值。在经典的 CBOW 中,输入隐藏层的向量是这些更高级别的特征。这意味着我们可以简单地将第一个权重矩阵的行作为我们的词向量。让我们实现 CBOW,这样我们可以得到更清晰的理解。
首先,让我们定义我们的导入并加载我们的数据。
import sparknlp
from nltk.corpus import brown
spark = sparknlp.start()
def detokenize(sentence):
text = ''
for token in sentence:
if text and any(c.isalnum() for c in token):
text += ' '
text += token
return text
texts = []
for fid in brown.fileids():
text = [detokenize(s) for s in brown.sents(fid)]
text = ' '.join(text)
texts.append((text,))
texts = spark.createDataFrame(texts, ['text'])
现在我们有了数据,让我们处理和准备它以构建我们的模型。
from pyspark.ml import Pipeline
from sparknlp import DocumentAssembler, Finisher
from sparknlp.annotator import *
assembler = DocumentAssembler()\
.setInputCol('text')\
.setOutputCol('document')
sentence = SentenceDetector() \
.setInputCols(["document"]) \
.setOutputCol("sentences") \
.setExplodeSentences(True)
tokenizer = Tokenizer()\
.setInputCols(['sentences'])\
.setOutputCol('token')
normalizer = Normalizer()\
.setCleanupPatterns([
'[^a-zA-Z.-]+',
'^[^a-zA-Z]+',
'[^a-zA-Z]+$',
])\
.setInputCols(['token'])\
.setOutputCol('normalized')\
.setLowercase(True)
finisher = Finisher()\
.setInputCols(['normalized'])\
.setOutputCols(['normalized'])\
.setOutputAsArray(True)
pipeline = Pipeline().setStages([
assembler, sentence, tokenizer,
normalizer, finisher
]).fit(texts)
sentences = pipeline.transform(texts)
sentences = sentences.select('normalized').collect()
sentences = [r['normalized'] for r in sentences]
print(len(sentences)) # number of sentences
59091
现在我们已经执行了文本处理,所以让我们构建我们的编码。大多数深度学习库中都有工具可以做到这一点,但让我们自己做吧。
from collections import Counter
import numpy as np
import pandas as pd
UNK = '???'
PAD = '###'
w2i = {PAD: 0, UNK: 1}
df = Counter()
for s in sentences:
df.update(s)
df = pd.Series(df)
df = df[df > 10].sort_values(ascending=False)
for word in df.index:
w2i[word] = len(w2i)
i2w = {ix: w for w, ix in w2i.items()}
vocab_size = len(i2w)
7814
我们包括一个用于填充的标记和一个用于未知单词的标记。我们将平铺我们的句子,创建标记窗口。中间的标记是我们试图预测的,而周围的标记是我们的上下文。我们需要填充我们的句子,否则我们会在句子的开头和结尾丢失单词。
让我们还创建一些实用函数,将标记序列转换为索引序列,并执行相反的操作。
def texts_to_sequences(texts):
return [[w2i.get(w, w2i[UNK]) for w in s] for s in texts]
def sequences_to_texts(seqs):
return [' '.join([i2w.get(ix, UNK) for ix in s]) for s in seqs]
seqs = texts_to_sequences(sentences)
现在让我们构建我们的上下文窗口。我们将检查每个句子并为句子中的每个标记创建一个窗口。
w = 4
windows = []
Y = []
for k, seq in enumerate(seqs):
for i in range(len(seq)):
if seq[i] == w2i[UNK] or len(seq) < 2*w:
continue
window = []
for j in range(-w, w+1):
if i+j < 0:
window.append(w2i[PAD])
elif i+j >= len(seq):
window.append(w2i[PAD])
else:
window.append(seq[i+j])
windows.append(window)
windows = np.array(windows)
我们不能将所有数据都转换为向量,因为这会占用太多内存。所以我们需要实现一个生成器。首先,让我们编写将窗口集合转换为 numpy 数组的函数。这将获取窗口并生成一个包含 one-hot-encoded 单词的矩阵和一个包含 one-hot-encoded 目标单词的矩阵。
def windows_to_batch(batch_windows):
w = batch_windows.shape[1] // 2
X = []
Y = []
for window in batch_windows:
X.append(np.concatenate((window[:w], window[w+1:])))
Y.append(window[w])
X = np.array(X)
Y = ku.to_categorical(Y, vocab_size)
return X, Y
现在我们编写实际生成生成器的函数。训练方法采用 Python 生成器,因此我们需要一个实用函数来创建批次生成器。
def generate_batch(windows, batch_size=100):
while True:
indices = np.arange(windows.shape[0])
indices = np.random.choice(indices, batch_size)
batch_windows = windows[indices, :]
yield windows_to_batch(batch_windows)
现在我们可以实现我们的模型了。让我们定义我们的模型。我们将创建 50 维的词向量。维度的数量应该基于你的语料库的大小。但是,没有硬性规定。
from keras.models import Sequential
from keras.layers import *
import keras.backend as K
import keras.utils as ku
dim = 50
model = Sequential()
model.add(Embedding(vocab_size, dim, input_length=w*2))
model.add(Lambda(lambda x: K.mean(x, axis=1), (dim,)))
model.add(Dense(vocab_size, activation='softmax'))
第一层是我们将要学习的实际嵌入。第二层将上下文折叠成一个向量。最后一层预测窗口中间的单词应该是什么。
print(model.summary())
Model: "sequential_2"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
embedding_2 (Embedding) (None, 8, 50) 390700
_________________________________________________________________
lambda_2 (Lambda) (None, 50) 0
_________________________________________________________________
dense_2 (Dense) (None, 7814) 398514
=================================================================
Total params: 789,214
Trainable params: 789,214
Non-trainable params: 0
_________________________________________________________________
None
model.compile(loss='categorical_crossentropy', optimizer='adam')
这是一个相对简单的 Word2vec 模型,但我们仍然需要学习超过 700,000 个参数。词嵌入模型很快变得复杂。
让我们存储每 50 个 epoch 的权重。我们为每个 epoch 调用 50 次生成器。
batch_size = 1000
steps = 100
generator = generate_batch(windows, batch_size)
mc = ModelCheckpoint('weights{epoch:05d}.h5',
save_weights_only=True,
period=50)
model.fit_generator(generator, steps_per_epoch=steps,
epochs=500, callbacks=[mc])
现在让我们看一下数据。首先,让我们实现一个类来表示嵌入数据。我们将使用余弦相似度来比较向量。
class Word2VecData(object):
def __init__(self, word_vectors, w2i, i2w):
self.word_vectors = word_vectors
self.w2i = w2i
self.i2w = i2w
## 余弦相似度的实现使用
## 归一化向量。这意味着我们可以预先计算
## 词汇表的向量
self.normed_wv = np.divide(
word_vectors.T,
np.linalg.norm(word_vectors, axis=1)
).T
self.all_sims = np.dot(self.normed_wv, self.normed_wv.T)
self.all_sims = np.triu(self.all_sims)
self.all_sims = self.all_sims[self.all_sims > 0]
## 这将一个单词转换为一个向量
def w2v(self, word):
return self.word_vectors[self.w2i[word],:]
## 这计算输入单词与所有单词的余弦相似度
def _get_sims(self, word):
if isinstance(word, str):
v = self.w2v(word)
else:
v = word
v = np.divide(v, np.linalg.norm(v))
return np.dot(self.normed_wv, v)
def nearest_words(self, word, k=10):
sims = self._get_sims(word)
nearest = sims.argsort()[-k:][::-1]
ret = []
for ix in nearest:
ret.append((self.i2w[ix], sims[ix]))
return ret
def compare_words(self, u, v):
if isinstance(u, str):
u = self.w2v(u)
if isinstance(v, str):
v = self.w2v(v)
u = np.divide(u, np.linalg.norm(u))
v = np.divide(v, np.linalg.norm(v))
return np.dot(u, v)
让我们也实现一些东西来输出结果。在查看 Word2vec 时,我们想看看几件事。我们想找出哪些词与其他词相似。如果模型已经学习了有关单词的信息,您应该会看到相关的单词。
还有词类比。Word2vec 的一个有趣用途是“代数”这个词。常见的例子是king – man + woman ~ queen
。这意味着您从向量中减去man
向量king
,然后添加woman
向量。结果近似为queen
向量。这通常只适用于大量不同的词汇。我们的词汇量比较有限,因为我们的数据集很小。
让我们绘制所有单词到单词相似性的直方图。
import matplotlib.pyplot as plt
%matplotlib inline
def display_Word2vec(model, weight_path, words, analogies):
model.load_weights(weight_path)
word_vectors = model.layers[0].get_weights()[0]
W2V = Word2VecData(word_vectors, w2i, i2w)
for word in words:
for w, sim in W2V.nearest_words(word):
print(w, sim)
print()
for w1, w2, w3, w4 in analogies:
v1 = W2V.w2v(w1)
v2 = W2V.w2v(w2)
v3 = W2V.w2v(w3)
v4 = W2V.w2v(w4)
x = v1 - v2 + v3
for w, sim in W2V.nearest_words(x):
print(w, sim)
print()
print(w4, W2V.compare_words(x, v4))
print()
print('{}-{}+{}~{} quantile'.format(w1, w2, w3, w4),
(W2V.all_sims < W2V.compare_words(x, v4)).mean())
print()
plt.hist(W2V.all_sims)
plt.title('Word-to-Word similarity histogram')
plt.show()
让我们看看第 50 个 epoch 的结果。首先,让我们看一下类似于“空间”的词。这是通过余弦相似度列出的最接近“空间”的 10 个单词的列表。
space 0.9999999
shear 0.96719706
section 0.9615698
chapter 0.9592927
output 0.958699
phase 0.9580841
corporate 0.95798546
points 0.9575049
density 0.9573466
institute 0.9545017
现在让我们看一下类似于“多项式”的词。
polynomial 1.0000001
formula 0.9805055
factor 0.9684353
positive 0.96643007
produces 0.9631797
remarkably 0.96068406
equation 0.9601216
assumption 0.95971704
moral 0.9586859
unique 0.95754766
现在让我们看看这个king – man + woman ~ queen
类比。我们将打印出最接近结果向量的单词king – man + woman
。然后我们可以看看结果与 queen
向量的相似度。最后,我们来看看queen的分位数是什么。它越高,类比的效果就越好。
mountains 0.96987706
emperor 0.96913254
crowds 0.9688335
generals 0.9669207
masters 0.9664976
kings 0.9663711
roof 0.9653381
ceiling 0.96467453
ridge 0.96467185
woods 0.96466273
queen 0.9404894
king-man+woman~queen quantile 0.942
向queen
量比 94% 的其他词更接近是一个好兆头,但其他一些顶级结果(例如“天花板”)如此接近,表明我们的数据集可能太小而且可能太专业而无法学习这样的一般关系。
最后,我们看一下直方图,如图 11-1所示。
图 11-1。epoch 50 的单词相似度直方图
大多数相似之处都偏高。这意味着在 epoch 50 时,这些词非常相似。让我们看一下 epoch 100 的直方图,如图 11-2所示。
图 11-2。epoch 100 的单词到单词相似度的直方图
直方图的权重向中间移动。这意味着我们看到我们的词之间有更多的区别。现在让我们看一下 500 个 epoch,如图 11-3所示。
请注意直方图的质量如何向左移动,因此大多数单词彼此不同。这意味着单词在单词向量空间中更加分离。但请记住,这可能意味着我们过度拟合了这个数据集。
Spark NLP 让我们可以整合外部训练的 Word2vec 模型。让我们看看如何在 Spark NLP 中使用这些词嵌入。首先,让我们以 Spark NLP 熟悉的格式将嵌入写入文件。
图 11-3。epoch 500 的单词相似度直方图
model.load_weights('weights00500.h5')
word_vectors = model.layers[0].get_weights()[0]
with open('cbow.csv', 'w') as out:
for ix in range(vocab_size):
word = i2w[ix]
vec = list(word_vectors[ix, :])
line = ['{}'] + ['{:.18e}'] * dim
line = ' '.join(line) + '\n'
line = line.format(word, *vec)
out.write(line)
现在我们可以创建一个嵌入注释器。
Word2vec = WordEmbeddings() \
.setInputCols(['document', 'normalized']) \
.setOutputCol('embeddings') \
.setDimension(dim) \
.setStoragePath('cbow.csv', 'TEXT')
pipeline = Pipeline().setStages([
assembler, sentence, tokenizer,
normalizer, Word2vec
]).fit(texts)
让我们找出模型生成的嵌入。
pipeline.transform(texts).select('embeddings.embeddings') \
.first()['embeddings']
[[0.6336007118225098,
0.7607026696205139,
1.1777857542037964,
...]]
Spark 有一个 skip-gram 方法的实现。让我们看看如何使用它。
from pyspark.ml.feature import Word2Vec
Word2vec = Word2Vec() \
.setInputCol('normalized') \
.setOutputCol('word_vectors') \
.setVectorSize(dim) \
.setMinCount(5)
finisher = Finisher()\
.setInputCols(['normalized'])\
.setOutputCols(['normalized'])\
.setOutputAsArray(True)
pipeline = Pipeline().setStages([
assembler, sentence, tokenizer,
normalizer, finisher, Word2vec
]).fit(texts)
pipeline.transform(texts).select('word_vectors') \
.first()['word_vectors']
DenseVector([0.0433, -0.0003, 0.0281, 0.0791, ...])
GloVe
由斯坦福大学的 Jeffrey Pennington、Richard Socher 和 Christopher Manning 创建的GloVe(全局向量)实际上更类似于我们在上一章中介绍的技术,例如 LSI。GloVe 没有使用神经网络来构建语言模型,而是尝试学习单词的共现统计。它在词类比任务上优于许多常见的 Word2vec 模型。
GloVe 的一个好处是它是直接建模关系的结果,而不是将它们作为训练语言模型的副作用。
让我们看看如何在 Spark NLP 中使用 GloVe:
glove = WordEmbeddingsModel.pretrained(name='glove_100d') \
.setInputCols(['document', 'normalized']) \
.setOutputCol('embeddings') \
pipeline = Pipeline().setStages([
assembler, sentence, tokenizer,
normalizer, glove
]).fit(texts)
pipeline.transform(texts).select('embeddings.embeddings') \
.first()['embeddings']
[[-0.03819400072097778,
-0.24487000703811646,
0.7281200289726257,
...]]
fastText
2015 年,Facebook 研究开发了一个名为fastText的 Word2vec 扩展。Word2vec 的一个常见问题是它处理不在训练语料库词汇中的单词的方式。对于某些问题,简单地删除这些词可能是有意义的,假设它们太罕见而不会对下游流程的结果产生重大影响。在具有专业词汇的语料库(如临床语料库)中,找到一个对文档很重要但在训练数据中可能找不到的词并不少见。这种词汇外的问题也使得迁移学习变得困难。迁移学习是您在一个数据集和任务上训练的部分或整个模型,并将其用于不同的数据集甚至不同的任务。事实上,Word2vec 本身就是迁移学习。您构建一个模型来解决语言建模问题,通常是人为的,并在其他一些与 NLP 相关的任务中使用该模型的一部分。
fastText 通过学习字符级信息使使用词嵌入的迁移学习更容易。因此,它不是学习更高级别的标记表示,而是学习更高级别的字符序列表示。一旦学习了这些字符序列,我们将构成单词的字符序列向量的总和作为单词的向量。
Transformers
2017 年,谷歌的研究人员创建了一种新的注意力建模方法。注意力是序列建模的一个概念。没有固定上下文的序列模型必须了解将序列中较早的信息保留多长时间。能够更好地捕捉远距离关系对于自动机器翻译非常重要。大多数单词都有多种含义或含义,要澄清需要更广泛的上下文。在语言学中,具有多种意义的属性被称为多义或同音异义。
多义是指感觉不同但相关,而同义是指感觉不相关。例如,让我们看一下“摇滚”这个词。当用作名词时,“岩石”是指一块石头。这与动词“摇滚”的另一个含义完全无关。动词“摇滚”是指来回运动,也可以表示演奏或欣赏摇滚音乐。所以“摇滚”(一块石头)是“摇滚”(来回移动)的谐音,是多义词,也有表演或欣赏摇滚音乐的意思。
消除同音异义词和多义词歧义的线索通常来自上下文中的其他词。论文中给出的示例将Transformer
(Vaswani 等人)定义为“银行”,它有两个含义。第一个是金融机构,第二个是河边。这种同名关系不翻译。例如,在西班牙语中,机构是“banco”,河流的边缘是“orilla”。因此,如果您正在使用神经网络进行翻译,那么以不同方式表示两个单词将是有利的。为此,您必须根据上下文对您的单词进行编码。
先前方法的词向量表示这些不同含义的聚合。这允许更丰富的文本表示。然而,它付出了沉重的代价。这些模型在计算上的训练和使用强度要大得多。事实上,在 2019 年撰写本文时,当前的大多数方法在不使用 GPU 甚至更专业的硬件的情况下都不可行。
ELMo、BERT 和 XLNet
较新的嵌入技术基于以上下文相关的方式表示单词的想法。这意味着需要一个完整的神经网络模型来使用嵌入,这与静态嵌入不同,其中只有一个查找。
语言模型嵌入 (ELMo)是艾伦研究所于 2018 年开发的模型。正在学习的语言模型是双向的。这意味着该模型正在学习根据它之前和之后的单词来预测一个单词。该模型在字符级别学习,但嵌入本身实际上是基于单词的。
2018 年发布的来自 Transformers (BERT) 的双向编码器表示正在做一些与 ELMo 非常相似的事情,但它使用的是谷歌的 s——Transformer
因此得名。目的是该模型可以进行微调。这是通过在数据集上构建通用的预训练模型来完成的。还有其他方法可以进行微调,但 BERT 论文的作者指出,这些方法要么是单向方法,要么是更专业的双向方法。BERT 旨在通过构建一个尝试识别随机掩码单词的模型来解决需要选择的问题。
BERT 通过在许多基准测试中获得高分而变得非常受欢迎。大约一年后,XLNet 发布了。XLNet旨在学习一个没有 BERT 所需掩码的模型。这个想法是,掩蔽会在 BERT 模型在训练时看到的内容和它在使用时看到的内容之间造成差异。然后 XLNet 继续达到更高的基准。
让我们看看如何在 Spark NLP 中使用 BERT 嵌入。
bert = BertEmbeddings.pretrained() \
.setInputCols(["sentences", "normalized"]) \
.setOutputCol("bert")
pipeline = Pipeline().setStages([
assembler, sentence, tokenizer,
normalizer, bert
]).fit(texts)
pipeline.transform(texts).select('bert.embeddings') \
.first()['embeddings']
[[-0.43669646978378296,
0.5360171794891357,
-0.051413312554359436,
...]]
对那些对这些技术感兴趣的人需要注意的是:在评估这种新的和复杂的方法时,必须始终回到第一原则。首先,始终考虑您的产品实际需要什么。你的产品是否类似于 BERT 和 XLNet 获得高分的任务之一?与您愿意在开发人员时间、培训时间和硬件上花费的数量相比,您需要的准确度是多少?仅仅因为这些技术在 NLP 领域的人们中非常流行,并不意味着它们对每个应用程序都是最好的。
事实上,这些技术有可能以难以检测的方式过度拟合。台湾国立成功大学的研究人员为名为 Argument Reasoning Comprehension Task 的问答任务创建了一个对抗性数据集。在这里,模型必须接受一段文本,提出一些论点并得出结论。BERT 在这项任务上取得了高于人类分数的分数。研究人员用相互矛盾的例子修改了数据集。BERT 模型在这个新的对抗性数据集上进行了评估,它的表现比人类差,比用更旧、更简单的技术构建的模型好不了多少。模型应该能够仅根据输入文本得出结论;也就是说,它不应该在其他示例中使用语句。
doc2vec
Doc2vec是一组让我们将文档转换为向量的技术。通常,我们希望使用嵌入作为其他任务的稀疏特征,例如分类。我们如何将这些单词级特征组合成文档级特征?一种常见的方法是简单地平均单词级向量。这很直观,因为我们希望向量空间代表一个模糊的含义概念。因此,如果我们对文档中的所有向量进行平均,我们应该得到文档的“平均”含义。
当我们考虑单词的稀有性时,这种方法的问题就出现了。回顾我们在 TF.IDF 上的对话,通常情况下,不重要的词出现频率很高。例如,考虑一个临床记录。我们可能会发现大量对所有音符都通用的通用词。这可能会带来问题,因为这些词会将我们所有的文档拉向向量空间中的少数几个地方。模型仍然可以将它们分开,但收敛速度会更慢。更糟糕的是,如果语料库中存在自然集群,换句话说,来自不同部门的临床记录,我们可能会有许多紧密打包的文档集群。我们想要的是能够通过对该独特文档最重要的单词来表征文档。有几种方法可以做到这一点。
您可以使用 IDF 值作为权重对词向量执行加权平均。这将有助于减少更常见的单词对文档向量的影响。这种方法的好处是易于实施。实际上,如果您使用静态嵌入技术,您可以简单地通过 IDF 值缩放向量。在评估时不需要计算这个。缺点是这是一种词袋方法,没有考虑词之间的关系。
另一种方法称为分布式内存段落向量。这本质上是 CBOW,但需要学习一组额外的权重。在 CBOW 中,我们从上下文中预测一个单词,因此我们将表示上下文的向量作为输入。对于分布式内存段落向量,我们将文档 ID 的 one-hot 编码连接到输入。这允许我们用与单词相同的维度来表示文档。
doc2vec 的第三种方法与 skip-gram 方法类似,即分布式词袋段落向量。回想一下,skipgrams 从一个单词中预测上下文。在分布式词袋段落向量中,您学习根据文档 ID 预测文档的上下文。
最后两种方法的好处是可以学习共现词之间的关系。如果您可以实现 Word2vec,它们的实现也相对简单。他们的缺点是他们只学习你手头的文件。如果您获得一个新文档,您将无法为其生成矢量。所以这些方法只能用于离线流程。
在谈论 doc2vec(有时也称为paragraph2vec)时,重要的是要记住它可以应用于不同大小的文本,从短语到整个文档。但是,如果您对将短语转换为向量感兴趣,您可能还需要考虑将其合并到您的标记化中。您可以生成短语作为标记,然后学习前面讨论的单词级嵌入之一。
练习
让我们看看这些技术如何解决第 9 章中的分类问题。
这一次,编写代码取决于您。尝试 Spark 的 skip-gram 实现、Spark NLP 的预训练 GloVe 模型和 Spark NLP 的 BERT 模型。