找回密码
 立即注册
首页 业界区 业界 吴恩达深度学习课程五:自然语言处理 第二周:词嵌入 ...

吴恩达深度学习课程五:自然语言处理 第二周:词嵌入 课后习题与代码实践

啦迩 5 天前
此分类用于记录吴恩达深度学习课程的学习笔记。
课程相关信息链接如下:

  • 原课程视频链接:[双语字幕]吴恩达深度学习deeplearning.ai
  • github课程资料,含课件与笔记:吴恩达深度学习教学资料
  • 课程配套练习(中英)与答案:吴恩达深度学习课后习题与答案
本篇为第五课第二周的课后习题和代码实践部分。
1. 理论习题

【中英】【吴恩达课后测验】Course 5 -序列模型 - 第二周测验
本周习题同样较为简单,就不再展开了。
2. 代码实践

词向量与Emoji生成器-CSDN博客
在本周的编程作业里,链接里的博主除了编码演示关于词向量的一些基本应用外,主要是实现了一个表情生成器。
其原理是通过文本和相应的表情标签进行监督学习,构建分类模型,在完成训练后,通过对模型输出的下游加工,可以实现“输入文本,输出配有表情的文本”的效果。感兴趣可以进入了解。
同样,我们还是使用成熟框架来演示本周的内容,得益于 PyTorch 对基础模块的封装非常完善,我们可以较简洁地完成本周内容的演示,主要内容列举如下:

  • 如何在代码中使用词嵌入?
  • 使用词向量取代独热编码对命名实体识别模型性能的影响。
  • 使用词向量进行情绪分类。
2.1 在PyTorch 中使用词嵌入

在 PyTorch 调用词嵌入的方法被封装在模型模块中,就像我们调用方法创建全连接层和卷积层一样,现在,我们要做的就是创建嵌入层
先来单独看看创建嵌入层的方法本身:
  1. self.embedding = nn.Embedding(  
  2.     num_embeddings=vocab_size,  # 词典大小
  3.     embedding_dim=embed_dim,    # 词向量维度,既一个词用多少维的向量表示。
  4.     # 上面这两个参数就划定好了词嵌入矩阵的大小。
  5.     padding_idx=word_vocab["<PAD>"]  # 获取填充符索引,固定其向量为 0 ,并屏蔽梯度计算。
  6. )
复制代码
这里有一点需要强调,如果你对我们上周的实践内容还有印象,会发现我们其实已经在前面的代码了显式定义了  的索引:
  1. word_vocab["<PAD>"] = 0
复制代码
也就是说,在方法的参数里,我们可以直接写成:
  1. padding_idx= 0
复制代码
但是,我们基本不会这么做。
这其实是代码规范里一个老生常谈的问题:避免硬编码
在这里,一旦词表构建策略发生调整(例如交换  与  的索引),参数就会被错误使用,却不会触发任何报错,最终导致模型在训练过程中学到错误的表示。
因此,我们在实践中更倾向使用统一的变量,来显式表达语义依赖,避免在调整时引入隐蔽错误。
回到正题,了解了嵌入层方法本身后,现在就来看看如何将其应用在模型中,先看看我们上周使用独热编码的模型代码:
  1. class RNNTagger(nn.Module):  
  2.     def __init__(self, vocab_size, hidden_dim, num_classes,  
  3.                  rnn_type='RNN', bidirectional=False, num_layers=1):  
  4.         super().__init__()  
  5.         self.vocab_size = vocab_size  
  6.         self.bidirectional = bidirectional  
  7.         self.rnn_type = rnn_type.upper()  
  8.   
  9.         input_size = vocab_size  # 独热编码输入维度 = 词表大小  
  10.   
  11.         if self.rnn_type == 'RNN':  
  12.             self.rnn = nn.RNN(input_size, hidden_dim, batch_first=True,  
  13.                               bidirectional=bidirectional, num_layers=num_layers)  
  14.         ......其他模型选择
  15.         
  16.         self.fc = nn.Linear(hidden_dim * (2 if bidirectional else 1), num_classes)  
  17.   
  18.     def forward(self, x):   
  19.         x_onehot = torch.nn.functional.one_hot(x, num_classes=self.vocab_size).float() # 传播第一步,将输入从索引转换为独热编码。
  20.         out, _ = self.rnn(x_onehot)  
  21.         out = self.fc(out)  
  22.         return out
复制代码
而把使用独热编码改为使用词向量的工作量也并不大,我们需要:

  • 新增参数定义词向量维度。
  • 创建嵌入层。
  • 在传播的第一步将索引从转换为独热编码改为输入嵌入层提取词向量。
更改完的代码如下:
  1. class RNNTagger(nn.Module):  
  2.     def __init__(self, vocab_size, hidden_dim, num_classes,  
  3.                  rnn_type='RNN', bidirectional=False, num_layers=1,  
  4.                  embed_dim=300):  # ← 新增一个嵌入维度参数  
  5.         super().__init__()  
  6.         self.bidirectional = bidirectional  
  7.         self.rnn_type = rnn_type.upper()  
  8.   
  9.         # 新增:词嵌入层  
  10.         self.embedding = nn.Embedding(  
  11.             num_embeddings=vocab_size,  
  12.             embedding_dim=embed_dim,  
  13.             padding_idx=word_vocab["<PAD>"]  
  14.         )  
  15.   
  16.         input_size = embed_dim  # ← RNN 输入改为 embedding 维度  
  17.   
  18.         if self.rnn_type == 'RNN':  
  19.             self.rnn = nn.RNN(input_size, hidden_dim, batch_first=True,  
  20.                               bidirectional=bidirectional, num_layers=num_layers)  
  21.         elif self.rnn_type == 'LSTM':  
  22.             self.rnn = nn.LSTM(input_size, hidden_dim, batch_first=True,  
  23.                                bidirectional=bidirectional, num_layers=num_layers)  
  24.         elif self.rnn_type == 'GRU':  
  25.             self.rnn = nn.GRU(input_size, hidden_dim, batch_first=True,  
  26.                               bidirectional=bidirectional, num_layers=num_layers)  
  27.         else:  
  28.             raise ValueError("rnn_type must be 'RNN','LSTM','GRU'")  
  29.   
  30.         self.fc = nn.Linear(hidden_dim * (2 if bidirectional else 1), num_classes)  
  31.   
  32.     def forward(self, x):  
  33.         x_embed = self.embedding(x) #在传播中首先输入嵌入层提取词向量。
  34.         out, _ = self.rnn(x_embed)  
  35.         out = self.fc(out)  
  36.         return out
复制代码
这样,只需要在模型部分完成改动,我们便可以直接应用上周的代码框架直接进行训练。
下面就来看看效果:
2.2 使用词嵌入进行命名实体识别

我们先使用普通的双向 RNN 来看看效果,多次实验部分结果如下:
1.png

可以看出,相比独热编码,虽然在指标上并没有明显的提升,但是使用 300 维的词向量完成相同的训练,只需要独热编码训练用时的约 65% ,这种优势会随着词表规模增加而更明显。
显然,词向量避免了独热编码向量的极高维度且极其稀疏缺陷,这是在计算性能上的极大提升。
简单打印一些训练后的词向量如下:(只截取了前 20 维)
2.png

然后,我们再试试上周综合表现最好的 GRU ,结果如下:
3.png

训练用时同样得到了极大提升,但是,好像出现问题了:
在使用了词嵌入后,明明还增加了训练轮次,但指标反而不如使用独热编码高,这是为什么?
实际上,词嵌入并不天然适用于所有 NLP 任务。对于实体命名识别这类以词和标签强对齐为主的任务,one-hot 表示由于其完全区分词身份的特性,反而可能取得更好的效果。
说简单些,由于命名实体识别任务更关注“这个词是什么”,而不是“词之间的关系”,使用完全正交的独热编码反而让界限更明显。
来简单看个例子:
假设在训练语料中,Apple 大量以 B-ORG(组织)标签出现,而 Google、Microsoft 等词在词嵌入空间中与 Apple 距离很近,这是因为它们共享了“公司”“科技”“产品”等相似上下文。但在具体句子中:

  • Apple released a new product. → Apple 是 ORG(苹果公司)
  • I ate an apple after lunch. → apple 是 O(食物苹果)
对于 NER 来说,关键不是“这个词在语义上像什么”,而是在当前任务标注体系下,它在这个位置对应什么标签
而词嵌入会引入一种强烈的归纳偏置:语义相近的词,其表示也应当相近。当模型的上下文建模能力有限时,这种相似性结构可能被过度利用,使模型倾向于根据词向量的邻近关系做出判断,而不是严格依赖监督信号本身,从而在某些语境下产生错误的实体类型预测。
比如词嵌入会天然鼓励模型将 Apple 与其他科技公司词拉近,从而放大“公司语义”的共性,而如果模型设计对大小写不敏感,另外的部分语料里又让水果间的距离更近,就可能导致 “香蕉公司”,“菠萝公司” 等错误识别。
而在 one-hot 表示下,Apple 的表示与任何其他词完全独立,模型只能依赖监督信号本身去学习它在不同上下文中与标签之间的对应关系,反而避免了这种误解
总结来说,在不进行进一步上下文建模或结构化约束的情况下,词嵌入由于为多义词提供了共享的连续表示,可能在某些任务中引入语义混淆,从而影响模型对具体标签的判别。
而 one-hot 表示通过其完全正交的设计,显式区分了不同词项的身份,在词—标签强对齐的任务中反而在一定程度上缓解这一问题。
现在,我们在词嵌入的强项:情绪分类上再来看看其效果:
2.3 使用词向量进行情绪分类

要进行新的任务,自然首先要引入新的数据集,这里我们使用情绪分类中的经典数据集:IMDb
IMDb 数据集是一个经典的二分类情绪分析任务数据集,它的输入是电影评论文本,输出是情绪标签,0 表示负面(negative),1 表示正面(positive)。
训练集和测试集各包含 25,000 条评论,评论长度不固定,从几十词到几百词不等,文本中包含标点、大小写、数字等元素。数据类别平衡。比较适合我们的演示。
同样,我们使用之前介绍过的 HuggingFace Datasets 来下载,完整代码附在最后,这里展示几条样本数据:
4.png

现在,同样使用 单层双向 GRU 来进行实验,设置词表大小为 20000,批次大小为 32,部分训练结果如下:
5.png

可以在较少的轮次中,实现较好的拟合。
而在同等参数下使用独热编码则会爆内存,以我的电脑配置需要将词表缩小到 5000 以下,并将批次大小降至 6,才勉强可以运行且单轮时间较长,这样的配置并不适配一般的使用场景,因此就不再展示独热编码的效果了。
词向量在计算效率与大词表适应性上拥有极大优势,可以在保持模型性能的同时,大幅降低显存消耗与训练时间。
如果你的资源足够,可以进行更多的尝试看看效果。
3. 附录

3.1 使用词嵌入进行情绪分类 PyTorch版
  1. import torch  import torch.nn as nn  from torch.utils.data import DataLoader  from torch.nn.utils.rnn import pad_sequence  from datasets import load_dataset  from collections import Counter  from sklearn.metrics import accuracy_score, f1_score  import time  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")  dataset = load_dataset("mteb/imdb")  train_data = dataset['train']  test_data  = dataset['test']     def build_vocab(dataset, max_vocab_size=5000):      counter = Counter()      for item in dataset:          counter.update(item['text'].split())      most_common = counter.most_common(max_vocab_size)      word_vocab = {w:i+2 for i,(w,_) in enumerate(most_common)}      word_vocab["<PAD>"] = 0      word_vocab[""] = 1      return word_vocab    word_vocab = build_vocab(train_data)  vocab_size = len(word_vocab)    def encode(item):      x = torch.tensor([word_vocab.get(w,1) for w in item['text'].split()], dtype=torch.long)      y = torch.tensor(item['label'], dtype=torch.long)      return x, y    train_dataset = [encode(item) for item in train_data]  test_dataset  = [encode(item) for item in test_data]    def collate_fn(batch):      xs, ys = zip(*batch)      xs_pad = pad_sequence(xs, batch_first=True, padding_value=word_vocab[""])      ys = torch.tensor(ys, dtype=torch.long)      return xs_pad.to(device), ys.to(device)    train_loader = DataLoader(train_dataset, batch_size=6, shuffle=True, collate_fn=collate_fn)  test_loader  = DataLoader(test_dataset, batch_size=6, shuffle=False, collate_fn=collate_fn)    class GRUSentiment(nn.Module):      def __init__(self, vocab_size, hidden_dim, num_classes,                   bidirectional=True, embed_dim=300):          super().__init__()          self.bidirectional = bidirectional          self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=0)          self.rnn = nn.RNN(embed_dim, hidden_dim, batch_first=True,                            bidirectional=bidirectional, num_layers=1)          self.fc = nn.Linear(hidden_dim * (2 if bidirectional else 1), num_classes)        def forward(self, x):          x = self.embedding(x)                   out, _ = self.rnn(x)                    if self.bidirectional:              out = torch.cat([out[:, -1, :self.rnn.hidden_size],                               out[:, 0, self.rnn.hidden_size:]], dim=1)          else:              out = out[:, -1, :]          out = self.fc(out)          return out  def train_validate(model, train_loader, test_loader, epochs=3, lr=0.001):      model.to(device)      criterion = nn.CrossEntropyLoss()      optimizer = torch.optim.Adam(model.parameters(), lr=lr)        for epoch in range(epochs):          model.train()          total_loss = 0          total_correct = 0          total_tokens = 0          start_time = time.time()            for x_batch, y_batch in train_loader:              optimizer.zero_grad()              outputs = model(x_batch)              loss = criterion(outputs, y_batch)              loss.backward()              optimizer.step()              total_loss += loss.item()                preds = outputs.argmax(dim=-1)              total_correct += (preds == y_batch).sum().item()              total_tokens += y_batch.numel()            train_acc = total_correct / total_tokens          avg_loss = total_loss / len(train_loader)            # 验证          model.eval()          all_preds, all_labels = [], []          val_total_correct = 0          val_total_tokens = 0            with torch.no_grad():              for x_batch, y_batch in test_loader:                  outputs = model(x_batch)                  preds = outputs.argmax(dim=-1)                  all_preds.extend(preds.cpu().tolist())                  all_labels.extend(y_batch.cpu().tolist())                  val_total_correct += (preds == y_batch).sum().item()                  val_total_tokens += y_batch.numel()            val_acc = val_total_correct / val_total_tokens          val_f1  = f1_score(all_labels, all_preds, average='macro')          epoch_time = time.time() - start_time            print(              f"轮次 {epoch+1} | "            f"训练损失: {avg_loss:.4f} | "            f"训练准确率: {train_acc:.4f} | "            f"验证准确率: {val_acc:.4f} | "            f"验证F1: {val_f1:.4f} | "            f"本轮耗时: {epoch_time:.2f} 秒"          )        print("\n训练完成!")      print(f"最终验证准确率: {val_acc:.4f}, F1-macro: {val_f1:.4f}")      return model    if __name__ == "__main__":      model = GRUSentiment(          vocab_size=vocab_size,          hidden_dim=128,          num_classes=2,          bidirectional=True,          embed_dim=300      )      model = train_validate(model, train_loader, test_loader, epochs=10, lr=0.001)
复制代码
3.2 使用词嵌入进行情绪分类 TF版
  1. import tensorflow as tf  from tensorflow.keras.preprocessing.sequence import pad_sequences  from tensorflow.keras.utils import to_categorical  from datasets import load_dataset  from collections import Counter  import numpy as np  import time    device = "GPU" if tf.config.list_physical_devices('GPU') else "CPU"      dataset = load_dataset("mteb/imdb")  train_data = dataset['train']  test_data  = dataset['test']    def build_vocab(dataset, max_vocab_size=5000):      counter = Counter()      for item in dataset:          counter.update(item['text'].split())      most_common = counter.most_common(max_vocab_size)      word_vocab = {w:i+2 for i,(w,_) in enumerate(most_common)}      word_vocab["<PAD>"] = 0      word_vocab[""] = 1      return word_vocab    word_vocab = build_vocab(train_data, max_vocab_size=20000)  vocab_size = len(word_vocab)  print("词表大小:", vocab_size)    def encode(item):      x = [word_vocab.get(w, 1) for w in item['text'].split()]      y = item['label']      return x, y    train_encoded = [encode(item) for item in train_data]  test_encoded  = [encode(item) for item in test_data]    max_len = 200  X_train = pad_sequences([x for x, _ in train_encoded], maxlen=max_len, padding='post', truncating='post')  y_train = np.array([y for _, y in train_encoded])  X_test  = pad_sequences([x for x, _ in test_encoded], maxlen=max_len, padding='post', truncating='post')  y_test  = np.array([y for _, y in test_encoded])    def build_model(vocab_size, embed_dim=300, hidden_dim=128, bidirectional=True, num_classes=2):      inputs = tf.keras.Input(shape=(max_len,), dtype=tf.int32)      x = tf.keras.layers.Embedding(input_dim=vocab_size, output_dim=embed_dim, mask_zero=True)(inputs)      if bidirectional:          x = tf.keras.layers.Bidirectional(tf.keras.layers.GRU(hidden_dim))(x)      else:          x = tf.keras.layers.GRU(hidden_dim)(x)      outputs = tf.keras.layers.Dense(num_classes, activation='softmax')(x)      model = tf.keras.Model(inputs, outputs)      return model    model = build_model(vocab_size=vocab_size, embed_dim=300, hidden_dim=128, bidirectional=True, num_classes=2)  model.compile(optimizer=tf.keras.optimizers.Adam(0.001),                loss='sparse_categorical_crossentropy',                metrics=['accuracy'])    model.summary()    start_time = time.time()  history = model.fit(X_train, y_train, validation_data=(X_test, y_test),                      epochs=10, batch_size=32)  total_time = time.time() - start_time  print(f"训练完成,用时 {total_time:.2f} 秒")    y_pred = np.argmax(model.predict(X_test), axis=1)  from sklearn.metrics import f1_score  f1 = f1_score(y_test, y_pred, average='macro')  acc = np.mean(y_pred == y_test)  print(f"验证准确率: {acc:.4f}, F1-macro: {f1:.4f}")
复制代码
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册