AI-For-Beginners 词嵌入实战:用自定义数据集重跑 Embeddings 作业(PyTorch / TensorFlow 双版本)

原创2026-10-01 18:47:141,442 阅读
文章标签:教程人工智能机器学习深度学习计算机视觉NLP

AI-For-Beginners 词嵌入实战:用自定义数据集重跑 Embeddings 作业(PyTorch / TensorFlow 双版本)

本文是 AI-For-Beginners 课程「5-NLP / 14-Embeddings」配套作业(assignment.md)的完整实战指南。该作业要求学习者使用本课提供的 Notebook(PyTorch 或 TensorFlow 任一版本),换成自己的数据集(例如 Kaggle 上的 Beatles 歌词文本)重新运行,改写 Notebook 突出自己的观察,并记录实验结论。读完本文后,你将掌握:如何加载自定义文本数据集并构建词表、如何用 Embedding/EmbeddingBag 层完成从 bag-of-words 到 embedding bag 的迁移、如何接入 Word2Vec/GloVe 预训练向量,以及如何系统地记录和呈现自己的实验发现。

作业任务解读

作业原文(英文版)要求如下三件事,这也是本文组织内容的主线:

  1. 换数据重跑:使用本课配套 Notebook——EmbeddingsPyTorch.ipynb 或 EmbeddingsTF.ipynb——以你自己的数据集重新执行全部流程;数据可来自 Kaggle 等公开来源,使用时须保留合适的引用/归属声明。
  2. 改写 Notebook:在 Notebook 中突出你自己的发现,而不是简单照抄原有步骤;例如替换数据加载与词表构建环节,调整超参数并记录差异。
  3. 尝试不同类型的数据并记录结果:建议尝试与课程示例(新闻文本 AG News)不同类型的语料,比如歌词文本(作业示例中提及 Beatles 歌词),并用文字记录你的观察。

作业的完整体现在「记录」而非「跑通」:换数据集后,词表规模、未登录词比例、训练损失与准确率曲线、向量语义(如近义词、类比运算)都会发生变化,这些正是需要你在改写后的 Notebook 中呈现的发现。

预备知识:本课要重跑的到底是什么

要完成作业,必须先理解课程 README.md 与两个 Notebook 中反复出现的核心概念:

  • Embedding(嵌入):把词表示成低维稠密向量。此前课程中基于 BoW / TF-IDF 的分类器使用的是长度等于 vocab_size 的高维稀疏 one-hot 向量,既浪费内存,又无法表达词与词之间的语义相似性。嵌入层输入一个词编号,输出一个长度为 embedding_size 的稠密向量,从结构上看非常接近 Linear/Dense 层,区别在于它可以直接接收词编号,免去构造庞大的 one-hot 向量。
  • Embedding bag(嵌入袋):把嵌入层作为网络第一层后,文本中的每个词先被映射为对应嵌入,再对全部嵌入做某种聚合(sum、average 或 max),得到整条文本的数值表示,最后接线性分类器。两个 Notebook 中的分类器架构都是「Embedding → 聚合 → 线性分类器」三段式。
  • 变长序列处理:BoW 下所有样本向量长度固定为 vocab_size;改用词嵌入后,每条样本的词数不同,需要处理变长序列——PyTorch 版给出「padding」与「offset」两种方案,TensorFlow 版则由 TextVectorization 自动填充 PAD token。
  • 语义嵌入与预训练向量:普通嵌入层学到的向量不一定有语义,为了让近义词在向量空间(如欧氏距离)中彼此靠近,需要在大型语料上预训练,典型方法即 Word2Vec(含 CBoW 与 skip-gram 两种架构)、GloVe、FastText。预训练词表与语料词表通常不一致,这构成了换数据集后最值得观察的现象。

先跑通官方 Notebook:以 AG News 为基线

在换数据集之前,先分别跑通两个官方 Notebook,把基线结果记下来,作为后续对比的参照。

PyTorch 版流程

PyTorch 版依赖课程提供的辅助模块 torchnlp.py,其中封装了数据集加载、词表构建、编码、padding/offset 化与训练循环。它首先用 torchtext.datasets.AG_NEWS 加载新闻语料,用 torchtext.data.utils.get_tokenizer('basic_english') 做分词,并通过 torchtext.vocab.vocab(counter, min_freq=min_freq) 构建词表(支持 ngrams 与 min_freq 参数),最后返回 train_dataset, test_dataset, classes, vocab 四个对象。

第一步:构建 Embedding 分类器。 这是课程中最基础的嵌入分类器实现:

class EmbedClassifier(torch.nn.Module):
    def __init__(self, vocab_size, embed_dim, num_class):
        super().__init__()
        self.embedding = torch.nn.Embedding(vocab_size, embed_dim)
        self.fc = torch.nn.Linear(embed_dim, num_class)

    def forward(self, x):
        x = self.embedding(x)
        x = torch.mean(x, dim=1)
        return self.fc(x)

前向过程即「嵌入 → 对所有词向量取均值 → 全连接分类」,是 embedding bag 思想的最简实现。

第二步:用 padding 处理变长序列。 由于每条样本词数不同,需要给 DataLoader 提供 collate_fn:

def padify(b):
    # b 是长度为 batch_size 的元组列表:每项形如 (label, text_sequence)
    v = [encode(x[1]) for x in b]            # 逐条把文本向量化为词编号序列
    l = max(map(len, v))                     # 取小批量内最长序列的长度
    return (                                 # 返回 (标签张量, 特征张量)
        torch.LongTensor([t[0]-1 for t in b]),
        torch.stack([torch.nn.functional.pad(torch.tensor(t), (0, l-len(t)), mode='constant', value=0) for t in v])
    )

train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=16, collate_fn=padify, shuffle=True)

注意 torch.nn.functional.pad 的 (0, l-len(t)) 表示只在序列尾部填充,value=0 对应 PAD 词编号;t[0]-1 把 AG News 的 1~4 标签转成 0~3 索引。官方 Notebook 中设置 embed_dim=32、lr=1、epoch_size=25000(为了省时只训练不足一个 epoch),训练约 2.25 万条记录后准确率约 0.76;继续训练并微调学习率可以逼近约 90% 的准确率。

第三步:改用 EmbeddingBag 与 offset 表示。 padding 会把所有序列拉齐到批内最长长度,并非最高效的表示。另一种方案是把批内所有序列拼成一个长向量,再用 offset 向量记录每条序列的起点。课程配图 offset-sequence-representation.png 直观展示了该表示(图中以字符序列为例,本课处理的是词序列,原理相同)。对应实现:

class EmbedClassifier(torch.nn.Module):
    def __init__(self, vocab_size, embed_dim, num_class):
        super().__init__()
        self.embedding = torch.nn.EmbeddingBag(vocab_size, embed_dim)  # 内置聚合层
        self.fc = torch.nn.Linear(embed_dim, num_class)

    def forward(self, text, off):
        x = self.embedding(text, off)
        return self.fc(x)

def offsetify(b):
    x = [torch.tensor(encode(t[1])) for t in b]
    o = [0] + [len(t) for t in x]              # 累加长度得到各序列起点
    o = torch.tensor(o[:-1]).cumsum(dim=0)
    return (torch.LongTensor([t[0]-1 for t in b]), torch.cat(x), o)

train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=16, collate_fn=offsetify, shuffle=True)

torch.nn.EmbeddingBag 的输入是「拼接后的内容向量 + offset 向量」,其内置聚合方式可选 mean、sum 或 max。注意此时 dataloader 每批返回三个值(标签、文本、offset),官方 Notebook 为此专门定义了 train_epoch_emb(torchnlp.py 中亦有等价封装),训练时以 lr=4、epoch_size=25000 得到约 0.755 的准确率。

第四步:接入预训练语义嵌入。 使用 gensim 加载在 Google News 上预训练的 Word2Vec(api.load('word2vec-google-news-300'),首次下载耗时较长),可以观察语义特性:

for w, p in w2v.most_similar('neural'):
    print(f"{w} -> {p}")
# neuronal -> 0.780, neurons -> 0.733, neural_circuits -> 0.725 ...

以及著名的向量类比:king - man + woman ≈ queen(w2v.most_similar(positive=['king','woman'], negative=['man'])[0] 返回 ('queen', 0.7118))。

把 Word2Vec 权重灌入嵌入层时,预训练词表与语料词表大概率不一致,因此要对缺失词初始化随机权重。官方实现用 net.embedding.weight[i].data = torch.normal(0.0, 1.0, (embed_size,)) 兜底;在 AG News 上,vocab_size=95812 的词表中找到约 4.1 万词,缺 5.4 万词。官方还演示了第二种思路:直接用 torchtext.vocab.GloVe(name='6B', dim=50) 作为加载语料的词表,使词表与预训练向量完全对齐,再通过 net.embedding.weight.data = vocab.vectors 一次性灌入全部权重。换数据集后,「预训练词表覆盖率」正是你最值得量化记录的指标。

TensorFlow 版流程

TensorFlow 版(EmbeddingsTF.ipynb)用 tfds.load('ag_news_subset') 加载数据,模型由四层堆叠而成:

  • TextVectorization(max_tokens=vocab_size,input_shape=(1,)):字符串 → 词编号张量,忽略低频词;
  • Embedding(vocab_size, 100):每个词编号 → 100 维稠密向量,n×1 张量变成 n×100;
  • Lambda(lambda x: tf.reduce_mean(x, axis=1)):沿第一个轴取平均,把整个序列压成 100 维;
  • Dense(4, activation='softmax'):线性分类器。

model.summary() 显示该模型约 300 万参数,其中嵌入层占 3000000(vocab_size=30000 × 100)。词表通过 vectorizer.adapt(ds_train.take(500).map(extract_text)) 在训练数据子集上自适应构建——官方注明这是为了加速,可能漏掉部分 token 使准确率略降,但现实中小样本子集往往已能给出不错的词表估计。基线训练得到 val_acc 约 0.864。

变长序列方面,vectorizer('Hello, world!') 得到 2 个 token,vectorizer('I am glad to meet you!') 得到 6 个 token;批量向量化时自动以 PAD token(编号 0)右填充为矩形张量,例如两条样本统一变成 shape=(2, 6)。官方建议:为了减少 padding 浪费,可按序列长度递增排序后再分批,让每个 batch 内长度相近。

接入 Word2Vec 同样面临词表对齐问题,官方给出两条路线:

  1. 用 tokenizer 词表:遍历 vectorizer.get_vocabulary(),能查到的词用 w2v.get_vector(w) 填充权重矩阵 W,查不到的词留零或生成随机向量;然后用 keras.layers.Embedding(vocab_size, embed_size, weights=[W], trainable=False) 构造冻结的预训练嵌入层。官方在 30000 词表上命中约 4551 词、缺 784 词(注:trainable=False 会略降准确率但显著加快训练)。
  2. 用预训练词表:vocab = list(w2v.vocab.keys()),用 vectorizer.set_vocabulary(vocab) 重建向量化层,再用 gensim 提供的便捷函数 w2v.get_keras_embedding(train_embeddings=False) 直接生成 Keras 嵌入层,从而彻底绕开两套词表不一致的问题。

两种实现最终都指向同一个结论:语料中的词在预训练词表中缺失越多,直接冻结预训练嵌入带来的收益越有限——这恰好是作业希望你用新数据集去验证的点。

换数据集重跑:以歌词类文本为例

官方 Notebook 的基线数据是新闻文本(AG News,4 类)。作业明确建议尝试不同类型的语料,比如歌词(Beatles 歌词即为作业给出的示例方向)。换数据集后的改造点集中在以下环节,可对照 Notebook 逐段替换:

1. 数据加载与标注。 歌词数据集通常没有现成的四分类标签,常见的可行方案包括:按乐队/歌手分组(如把多支乐队的歌词分为几类)、按年代/专辑分组,或把任务改为二分类(如「是否为某乐队的歌词」)。替换 torchtext.datasets.AG_NEWS 或 tfds.load('ag_news_subset') 的加载代码即可,注意公开数据集需保留出处与归属声明。

2. 词表规模与参数。 歌词词汇量远小于新闻语料。以官方 AG News 为例,PyTorch 版词表达 95812 词;换成歌词语料后词表通常缩至数千到数万量级。相应地可以:调低 vocab_size(TensorFlow 版默认 30000)、调整 min_freq(torchtext.vocab.vocab 的最低词频过滤)、缩小 embed_dim(官方示例用 32,预训练向量则固定为 300 或 50)。这些参数的每一次调整都应在改写后的 Notebook 中记录。

3. 训练循环与超参数。 官方 train_epoch/train_epoch_emb(见 torchnlp.py 与 torchnlp.py)支持 lr、optimizer、loss_fn、epoch_size、report_freq 等参数。歌词语料通常更小,建议跑满多个 epoch 并尝试不同学习率;report_freq 控制打印进度的频率(官方为 200 条一个 batch 时每 200 批打印一次)。TensorFlow 版则直接通过 model.fit(..., epochs=n) 控制训练轮数。

4. 预训练向量的覆盖观测。 这是最能体现「自己的发现」的环节:加载 Word2Vec/GloVe 后,统计你的词表中有多少词命中预训练词表、多少缺失,并观察不同数据类型(新闻 vs 歌词)命中率的差异。歌词中的俚语、缩略词、口语化表达通常覆盖率偏低,这可以直接解释「为何预训练嵌入带来的准确率提升有限」。

5. 语义操作演示。 把 most_similar 与类比运算换成你的语料词:在歌词语料中考察近义词(如 'love'、'heart' 的邻近词)、验证 king - man + woman 类类比在你的词表上是否成立,并在 Notebook 中贴出向量距离的数值结果。

如何记录与呈现你的发现(作业评分关键)

作业的「重跑 + 改写 + 记录」要求最终产物是一份有观察、有对比、有结论的 Notebook。建议按以下框架组织:

  • 基线对照表:先记录官方 AG News 基线的词表大小、参数、准确率,再记录自定义数据集上的对应数值,逐项对比。
  • 观察一:词表结构差异。新闻 vs 歌词的词表规模、min_freq 过滤效果、常见词分布。
  • 观察二:padding 与 offset 两种表示的影响。批内最长序列长度、padding 占比、训练速度与最终准确率是否有差异(PyTorch 版两条路径可直接对比)。
  • 观察三:预训练嵌入的价值。命中率、冻结 vs 微调(trainable=True/False)、embed_dim=300(Word2Vec)或 50(GloVe 6B)对训练时间与过拟合的影响。
  • 观察四:语义验证。在你的语料中运行 most_similar 与类比运算,展示输出并解释结果是否符合语义直觉。
  • 限制与结论:基于上述数据,用文字总结「这类数据集在何种方案下表现更好,为什么」。

常见问题与排查要点

  • EmbeddingBag 与 Embedding 的输入差异:EmbeddingBag 接收拼接后的内容向量与 offset 两个输入,DataLoader 返回三元组,必须使用适配的训练循环(PyTorch 版为 train_epoch_emb),否则会因参数个数不匹配报错。
  • 词表不一致导致预训练向量无效:优先用「以预训练词表加载语料」的方案(PyTorch 用 GloVe 词表编码、TensorFlow 用 set_vocabulary + get_keras_embedding),并如实记录命中率。
  • 过拟合:换用预训练嵌入后嵌入层参数量剧增(如 300 维),官方明确提示需要更多训练样本避免过拟合;歌词类小语料上建议减少 embed_dim 或对嵌入层加冻结/正则。
  • 下载耗时:Word2Vec Google News 300 维向量体积较大,首次 api.load 或 torchtext.vocab.GloVe 下载可能需要较长时间,属正常现象,可在离线前先缓存。
  • 数据归属:使用 Kaggle 等来源的数据集时,须在 Notebook 中保留数据集的引用与授权说明,这是作业的显式要求。

小结

本作业的价值不在于把官方 Notebook 原样再跑一遍,而在于「换一个数据域之后,词嵌入方案的每个环节会发生什么、为什么」。以本课两个官方 Notebook(PyTorch / TensorFlow)为起点,配合 torchnlp.py 辅助模块,替换数据加载、调整词表与嵌入参数、接入预训练向量,最后把词表覆盖率、训练曲线与语义类比结果整理成可复现的观察记录,你就完成了从「跑通示例」到「有自己结论的实践」的跨越。

登录后查看全文
AI-For-Beginners