代码示例 / 自然语言处理 / 使用预训练的词嵌入

使用预训练的词嵌入

作者: fchollet
创建日期: 2020/05/05
最后修改: 2020/05/05
描述: 使用预训练的 GloVe 词嵌入对 Newsgroup20 数据集进行文本分类。

在 Colab 中查看 GitHub 源码


设置

import os

# 只有 TensorFlow 后端支持字符串输入。
os.environ["KERAS_BACKEND"] = "tensorflow"

import pathlib
import numpy as np
import tensorflow.data as tf_data
import keras
from keras import layers

介绍

在本示例中,我们展示了如何训练一个使用预训练词嵌入的文本分类模型。

我们将使用 Newsgroup20 数据集,这是一个包含 20,000 条消息板消息的集合,属于 20 个不同的主题类别。

对于预训练词嵌入,我们将使用 GloVe 嵌入


下载 Newsgroup20 数据

data_path = keras.utils.get_file(
    "news20.tar.gz",
    "http://www.cs.cmu.edu/afs/cs.cmu.edu/project/theo-20/www/data/news20.tar.gz",
    untar=True,
)

让我们看看数据

data_dir = pathlib.Path(data_path).parent / "20_newsgroup"
dirnames = os.listdir(data_dir)
print("目录数量:", len(dirnames))
print("目录名称:", dirnames)

fnames = os.listdir(data_dir / "comp.graphics")
print("comp.graphics 中的文件数量:", len(fnames))
print("一些示例文件名:", fnames[:5])
目录数量: 20
目录名称: ['comp.sys.ibm.pc.hardware', 'comp.os.ms-windows.misc', 'comp.windows.x', 'sci.space', 'sci.crypt', 'sci.med', 'alt.atheism', 'rec.autos', 'rec.sport.hockey', 'talk.politics.misc', 'talk.politics.mideast', 'rec.motorcycles', 'talk.politics.guns', 'misc.forsale', 'sci.electronics', 'talk.religion.misc', 'comp.graphics', 'soc.religion.christian', 'comp.sys.mac.hardware', 'rec.sport.baseball']
comp.graphics 中的文件数量: 1000
一些示例文件名: ['39638', '38747', '38242', '39057', '39031']

这是一个文件内容的示例:

print(open(data_dir / "comp.graphics" / "38987").read())
Newsgroups: comp.graphics
Path: cantaloupe.srv.cs.cmu.edu!das-news.harvard.edu!noc.near.net!howland.reston.ans.net!agate!dog.ee.lbl.gov!network.ucsd.edu!usc!rpi!nason110.its.rpi.edu!mabusj
From: mabusj@nason110.its.rpi.edu (Jasen M. Mabus)
Subject: Looking for Brain in CAD
Message-ID: <c285m+p@rpi.edu>
Nntp-Posting-Host: nason110.its.rpi.edu
Reply-To: mabusj@rpi.edu
Organization: Rensselaer Polytechnic Institute, Troy, NY.
Date: Thu, 29 Apr 1993 23:27:20 GMT
Lines: 7
Jasen Mabus
RPI student
我正在寻找任何CAD格式(.dxf, .cad, .iges, .cgm等)或图片格式(.gif, .jpg, .ras等)的人脑,以用于动画演示。如果有人有或知道位置,请通过电子邮件回复mabusj@rpi.edu。
提前谢谢你,
Jasen Mabus  

正如你所看到的,有标题行泄露了文件的类别,或者是显式的(第一行字面上就是类别名称),或者是隐式的,例如通过Organization字段。让我们去掉这些标题:

samples = []
labels = []
class_names = []
class_index = 0
for dirname in sorted(os.listdir(data_dir)):
    class_names.append(dirname)
    dirpath = data_dir / dirname
    fnames = os.listdir(dirpath)
    print("正在处理 %s,发现 %d 个文件" % (dirname, len(fnames)))
    for fname in fnames:
        fpath = dirpath / fname
        f = open(fpath, encoding="latin-1")
        content = f.read()
        lines = content.split("\n")
        lines = lines[10:]
        content = "\n".join(lines)
        samples.append(content)
        labels.append(class_index)
    class_index += 1

print("类别:", class_names)
print("样本数量:", len(samples))
处理 alt.atheism,找到 1000 个文件  
处理 comp.graphics,找到 1000 个文件  
处理 comp.os.ms-windows.misc,找到 1000 个文件  
处理 comp.sys.ibm.pc.hardware,找到 1000 个文件  
处理 comp.sys.mac.hardware,找到 1000 个文件  
处理 comp.windows.x,找到 1000 个文件  
处理 misc.forsale,找到 1000 个文件  
处理 rec.autos,找到 1000 个文件  
处理 rec.motorcycles,找到 1000 个文件  
处理 rec.sport.baseball,找到 1000 个文件  
处理 rec.sport.hockey,找到 1000 个文件  
处理 sci.crypt,找到 1000 个文件  
处理 sci.electronics,找到 1000 个文件  
处理 sci.med,找到 1000 个文件  
处理 sci.space,找到 1000 个文件  
处理 soc.religion.christian,找到 997 个文件  
处理 talk.politics.guns,找到 1000 个文件  
处理 talk.politics.mideast,找到 1000 个文件  
处理 talk.politics.misc,找到 1000 个文件  
处理 talk.religion.misc,找到 1000 个文件  
类别: ['alt.atheism', 'comp.graphics', 'comp.os.ms-windows.misc', 'comp.sys.ibm.pc.hardware', 'comp.sys.mac.hardware', 'comp.windows.x', 'misc.forsale', 'rec.autos', 'rec.motorcycles', 'rec.sport.baseball', 'rec.sport.hockey', 'sci.crypt', 'sci.electronics', 'sci.med', 'sci.space', 'soc.religion.christian', 'talk.politics.guns', 'talk.politics.mideast', 'talk.politics.misc', 'talk.religion.misc']  
样本数量: 19997
</div>
实际上有一个类别没有预期的文件数,但差异足够小,问题仍然是一个平衡的分类问题。

---
## 打乱数据并分割为训练集和验证集


```python
# 打乱数据
seed = 1337
rng = np.random.RandomState(seed)
rng.shuffle(samples)
rng = np.random.RandomState(seed)
rng.shuffle(labels)

# 提取训练和验证集的分割
validation_split = 0.2
num_validation_samples = int(validation_split * len(samples))
train_samples = samples[:-num_validation_samples]
val_samples = samples[-num_validation_samples:]
train_labels = labels[:-num_validation_samples]
val_labels = labels[-num_validation_samples:]
--- ## 创建词汇索引 让我们使用 `TextVectorization` 来索引数据集中找到的词汇。 稍后,我们将使用同一层实例来向量化样本。 我们的层只会考虑前 20,000 个单词,并将序列截断或填充为实际 200 个标记长。
vectorizer = layers.TextVectorization(max_tokens=20000, output_sequence_length=200)
text_ds = tf_data.Dataset.from_tensor_slices(train_samples).batch(128)
vectorizer.adapt(text_ds)
您可以通过 `vectorizer.get_vocabulary()` 检索计算出的词汇。让我们打印前 5 个单词:
vectorizer.get_vocabulary()[:5]
['', '[UNK]', 'the', 'to', 'of']
让我们向量化一个测试句子:
output = vectorizer([["the cat sat on the mat"]])
output.numpy()[0, :6]
array([   2, 3480, 1818,   15,    2, 5830])
正如您所看到的,“the”被表示为“2”。为什么不是 0,考虑到“the”是词汇中的第一个单词?这是因为索引 0 是为填充保留的,索引 1 是为“超出词汇”标记保留的。 这是一个将单词映射到其索引的字典:
voc = vectorizer.get_vocabulary()
word_index = dict(zip(voc, range(len(voc))))
如您所见,我们为测试句子获得了与上面相同的编码:
test = ["the", "cat", "sat", "on", "the", "mat"]
[word_index[w] for w in test]
[2, 3480, 1818, 15, 2, 5830]
--- ## 加载预训练的词嵌入 让我们下载预训练的 GloVe 嵌入(一个 822M 的 zip 文件)。 您需要运行以下命令:
!wget https://downloads.cs.stanford.edu/nlp/data/glove.6B.zip
!unzip -q glove.6B.zip
--2023-11-19 22:45:27--  https://downloads.cs.stanford.edu/nlp/data/glove.6B.zip
正在解析 downloads.cs.stanford.edu (downloads.cs.stanford.edu)... 171.64.64.22
正在连接 downloads.cs.stanford.edu (downloads.cs.stanford.edu)|171.64.64.22|:443... 已连接.
发送 HTTP 请求,等待响应... 200 OK
长度:862182613 (822M) [application/zip]
保存到 : ‘glove.6B.zip’
glove.6B.zip        100%[===================>] 822.24M  5.05MB/s    用时 2m 39s  
2023-11-19 22:48:06 (5.19 MB/s) - ‘glove.6B.zip’ 已保存 [862182613/862182613]
该归档文件包含多种尺寸的文本编码向量:50维、100维、200维、300维。我们将使用100D的。 让我们制作一个将单词(字符串)映射到其 NumPy 向量表示的字典:
path_to_glove_file = "glove.6B.100d.txt"

embeddings_index = {}
with open(path_to_glove_file) as f:
    for line in f:
        word, coefs = line.split(maxsplit=1)
        coefs = np.fromstring(coefs, "f", sep=" ")
        embeddings_index[word] = coefs

print("找到 %s 个词向量。" % len(embeddings_index))
找到 400000 个词向量。
现在,让我们准备一个相应的嵌入矩阵,可以在 Keras `Embedding` 层中使用。它是一个简单的 NumPy 矩阵,其中索引 `i` 处的条目是我们 `vectorizer` 词汇中索引 `i` 单词的预训练向量。
num_tokens = len(voc) + 2
embedding_dim = 100
hits = 0
misses = 0

# 准备嵌入矩阵
embedding_matrix = np.zeros((num_tokens, embedding_dim))
for word, i in word_index.items():
    embedding_vector = embeddings_index.get(word)
    if embedding_vector is not None:
        # 嵌入索引中未找到的单词将是全零的。
        # 这包括“填充”和“OOV”的表示
        embedding_matrix[i] = embedding_vector
        hits += 1
    else:
        misses += 1
print("转换了 %d 个单词 (%d 次未找到)" % (hits, misses))
转换了 18021 个单词 (1979 次未找到)
接下来,我们将预训练的词嵌入矩阵加载到 `Embedding` 层中。 请注意,我们将 `trainable=False` 以保持嵌入固定(我们不想在训练期间更新它们)。
from keras.layers import Embedding

embedding_layer = Embedding(
    num_tokens,
    embedding_dim,
    trainable=False,
)
embedding_layer.build((1,))
embedding_layer.set_weights([embedding_matrix])
--- ## 构建模型 一个简单的 1D 卷积网络,使用全局最大池化和最后的分类器。
int_sequences_input = keras.Input(shape=(None,), dtype="int32")
embedded_sequences = embedding_layer(int_sequences_input)
x = layers.Conv1D(128, 5, activation="relu")(embedded_sequences)
x = layers.MaxPooling1D(5)(x)
x = layers.Conv1D(128, 5, activation="relu")(x)
x = layers.MaxPooling1D(5)(x)
x = layers.Conv1D(128, 5, activation="relu")(x)
x = layers.GlobalMaxPooling1D()(x)
x = layers.Dense(128, activation="relu")(x)
x = layers.Dropout(0.5)(x)
preds = layers.Dense(len(class_names), activation="softmax")(x)
model = keras.Model(int_sequences_input, preds)
model.summary()
模型: "functional_1"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓
┃ 层 (类型)                       输出形状                    参数 # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━┩
│ input_layer (InputLayer)        │ (None, None)              │          0 │
├─────────────────────────────────┼───────────────────────────┼────────────┤
│ embedding (Embedding)           │ (None, None, 100)         │  2,000,200 │
├─────────────────────────────────┼───────────────────────────┼────────────┤
│ conv1d (Conv1D)                 │ (None, None, 128)         │     64,128 │
├─────────────────────────────────┼───────────────────────────┼────────────┤
│ max_pooling1d (MaxPooling1D)    │ (None, None, 128)         │          0 │
├─────────────────────────────────┼───────────────────────────┼────────────┤
│ conv1d_1 (Conv1D)               │ (None, None, 128)         │     82,048 │
├─────────────────────────────────┼───────────────────────────┼────────────┤
│ max_pooling1d_1 (MaxPooling1D)  │ (None, None, 128)         │          0 │
├─────────────────────────────────┼───────────────────────────┼────────────┤
│ conv1d_2 (Conv1D)               │ (None, None, 128)         │     82,048 │
├─────────────────────────────────┼───────────────────────────┼────────────┤
│ global_max_pooling1d            │ (, 128)               │          0 │
│ (GlobalMaxPooling1D)            │                           │            │
├─────────────────────────────────┼───────────────────────────┼────────────┤
│ dense (Dense)                   │ (, 128)               │     16,512 │
├─────────────────────────────────┼───────────────────────────┼────────────┤
│ dropout (Dropout)               │ (, 128)               │          0 │
├─────────────────────────────────┼───────────────────────────┼────────────┤
│ dense_1 (Dense)                 │ (, 20)                │      2,580 │
└─────────────────────────────────┴───────────────────────────┴────────────┘
 总参数: 2,247,516 (8.57 MB)
 可训练参数: 2,247,516 (8.57 MB)
 不可训练参数: 0 (0.00 B)
--- ## 训练模型 首先,将我们的字符串列表数据转换为 NumPy 整数索引数组。数组进行右侧填充。
x_train = vectorizer(np.array([[s] for s in train_samples])).numpy()
x_val = vectorizer(np.array([[s] for s in val_samples])).numpy()

y_train = np.array(train_labels)
y_val = np.array(val_labels)
我们使用类别交叉熵作为我们的损失,因为我们正在进行 softmax 分类。 此外,我们使用 `sparse_categorical_crossentropy` 因为我们的标签是整数。
model.compile(
    loss="sparse_categorical_crossentropy", optimizer="rmsprop", metrics=["acc"]
)
model.fit(x_train, y_train, batch_size=128, epochs=20, validation_data=(x_val, y_val))
Epoch 1/20
   2/125 ━━━━━━━━━━━━━━━━━━━━  9s 78ms/step - acc: 0.0352 - loss: 3.2164 

警告:在调用 absl::InitializeLog() 之前的所有日志消息都写入 STDERR
I0000 00:00:1700434131.619687    6780 device_compiler.h:187] 使用 XLA 编译集群!  该行最多记录一次。

 125/125 ━━━━━━━━━━━━━━━━━━━━ 22s 123ms/step - acc: 0.0926 - loss: 2.8961 - val_acc: 0.2451 - val_loss: 2.1965
Epoch 2/20
 125/125 ━━━━━━━━━━━━━━━━━━━━ 10s 78ms/step - acc: 0.2628 - loss: 2.1377 - val_acc: 0.4421 - val_loss: 1.6594
Epoch 3/20
 125/125 ━━━━━━━━━━━━━━━━━━━━ 10s 78ms/step - acc: 0.4504 - loss: 1.5765 - val_acc: 0.5849 - val_loss: 1.2577
Epoch 4/20
 125/125 ━━━━━━━━━━━━━━━━━━━━ 10s 76ms/step - acc: 0.5711 - loss: 1.2639 - val_acc: 0.6277 - val_loss: 1.1153
Epoch 5/20
 125/125 ━━━━━━━━━━━━━━━━━━━━ 9s 74ms/step - acc: 0.6430 - loss: 1.0318 - val_acc: 0.6684 - val_loss: 0.9902
Epoch 6/20
 125/125 ━━━━━━━━━━━━━━━━━━━━ 9s 72ms/step - acc: 0.6990 - loss: 0.8844 - val_acc: 0.6619 - val_loss: 1.0109
Epoch 7/20
 125/125 ━━━━━━━━━━━━━━━━━━━━ 9s 70ms/step - acc: 0.7330 - loss: 0.7614 - val_acc: 0.6832 - val_loss: 0.9585
Epoch 8/20
 125/125 ━━━━━━━━━━━━━━━━━━━━ 8s 68ms/step - acc: 0.7795 - loss: 0.6328 - val_acc: 0.6847 - val_loss: 0.9917
Epoch 9/20
 125/125 ━━━━━━━━━━━━━━━━━━━━ 8s 64ms/step - acc: 0.8203 - loss: 0.5242 - val_acc: 0.7187 - val_loss: 0.9224
Epoch 10/20
 125/125 ━━━━━━━━━━━━━━━━━━━━ 8s 60ms/step - acc: 0.8506 - loss: 0.4265 - val_acc: 0.7342 - val_loss: 0.9098
Epoch 11/20
 125/125 ━━━━━━━━━━━━━━━━━━━━ 7s 56ms/step - acc: 0.8756 - loss: 0.3659 - val_acc: 0.7204 - val_loss: 1.0022
Epoch 12/20
 125/125 ━━━━━━━━━━━━━━━━━━━━ 7s 54ms/step - acc: 0.8921 - loss: 0.3079 - val_acc: 0.7209 - val_loss: 1.0477
Epoch 13/20
 125/125 ━━━━━━━━━━━━━━━━━━━━ 7s 54ms/step - acc: 0.9077 - loss: 0.2767 - val_acc: 0.7169 - val_loss: 1.0915
Epoch 14/20
 125/125 ━━━━━━━━━━━━━━━━━━━━ 6s 50ms/step - acc: 0.9244 - loss: 0.2253 - val_acc: 0.7382 - val_loss: 1.1397
Epoch 15/20
 125/125 ━━━━━━━━━━━━━━━━━━━━ 6s 49ms/step - acc: 0.9301 - loss: 0.2054 - val_acc: 0.7562 - val_loss: 1.0984
Epoch 16/20
 125/125 ━━━━━━━━━━━━━━━━━━━━ 5s 42ms/step - acc: 0.9373 - loss: 0.1769 - val_acc: 0.7387 - val_loss: 1.2294
Epoch 17/20
 125/125 ━━━━━━━━━━━━━━━━━━━━ 5s 41ms/step - acc: 0.9467 - loss: 0.1626 - val_acc: 0.7009 - val_loss: 1.4906
Epoch 18/20
 125/125 ━━━━━━━━━━━━━━━━━━━━ 5s 39ms/step - acc: 0.9471 - loss: 0.1544 - val_acc: 0.7184 - val_loss: 1.6050
Epoch 19/20
 125/125 ━━━━━━━━━━━━━━━━━━━━ 5s 37ms/step - acc: 0.9532 - loss: 0.1388 - val_acc: 0.7407 - val_loss: 1.4360
Epoch 20/20
 125/125 ━━━━━━━━━━━━━━━━━━━━ 5s 37ms/step - acc: 0.9519 - loss: 0.1388 - val_acc: 0.7309 - val_loss: 1.5327

<keras.src.callbacks.history.History at 0x7fbf50e6b910>
--- ## 导出端到端模型 现在,我们可能想要导出一个`Model`对象,它接受任意长度的字符串作为输入,而不是索引序列。这将使模型变得更加可移植,因为您不必担心输入预处理管道。 我们的`vectorizer`实际上是一个Keras层,所以很简单:
string_input = keras.Input(shape=(1,), dtype="string")
x = vectorizer(string_input)
preds = model(x)
end_to_end_model = keras.Model(string_input, preds)

probabilities = end_to_end_model(
    keras.ops.convert_to_tensor(
        [["this message is about computer graphics and 3D modeling"]]
    )
)

print(class_names[np.argmax(probabilities[0])])
计算机图形学