实验任务一: 词嵌入¶
词嵌入¶
实验目标
通过本次实验,你将掌握以下内容:
- 认识词嵌入。
- 从Glove词嵌入中,探索一些词嵌入的基本性质。
- 利用词嵌入进行文本分类。
1. 词嵌入¶
词嵌入简介
词嵌入是指用一个低维向量来表示单词。词嵌入被用作自然语言处理任务(如情感分类、问答、翻译等)的基本组成部分。因此,在本次实验中,我们了解词嵌入的构造并且直观感受词嵌入。
本次实验所用的词嵌入和数据集下载链接如下:
词嵌入下载链接:¶
https://box.nju.edu.cn/d/591925358e264f3b9a75/
ag数据下载链接:¶
https://box.nju.edu.cn/f/7d3e4fce48fb446884c9/?dl=1
2. 探索词嵌入¶
在本节,我们将基于训练好的Glove词嵌入(感兴趣的同学可以自行google Glove的论文,GloVe: Global Vectors for Word Representation)进行一些初步探索。首先加载Glove词嵌入
import numpy as np
def load_glove_embeddings(glove_file, embedding_dim=50):
"""
读取 GloVe 词向量文件,并返回:
- word_to_vec: 单词到向量的映射
- word_to_index: 单词到索引的映射
- index_to_word: 索引到单词的映射
- embedding_matrix: 词嵌入矩阵
"""
word_to_vec = {}
word_to_index = {}
index_to_word = {}
# 预留特殊 token:0 给 <PAD>,1 给 <UNK>
word_to_index["<PAD>"] = 0
word_to_index["<UNK>"] = 1
index_to_word[0] = "<PAD>"
index_to_word[1] = "<UNK>"
# 读取 GloVe 词向量文件
with open(glove_file, 'r', encoding='utf-8') as f:
for idx, line in enumerate(f):
values = line.strip().split()
word = values[0] # 取出单词
vector = np.array(values[1:], dtype=np.float32) # 取出向量
word_to_vec[word] = vector
word_to_index[word] = idx + 2 # 0 和 1 已留给特殊 token
index_to_word[idx + 2] = word
# 创建嵌入矩阵 (词汇大小 x 维度)
vocab_size = len(word_to_vec) + 2
embedding_matrix = np.zeros((vocab_size, embedding_dim), dtype=np.float32)
embedding_matrix[1] = np.random.normal(scale=0.6, size=(embedding_dim,))
for word, idx in word_to_index.items():
if word in word_to_vec:
embedding_matrix[idx] = word_to_vec[word]
return word_to_vec, word_to_index, index_to_word, embedding_matrix
# 使用示例(使用项目中提供的 GloVe 文件)
glove_path = "glove.6B.50d.txt"
word_to_vec, word_to_index, index_to_word, embedding_matrix = load_glove_embeddings(glove_path)
# 示例:查看 'computer' 的词向量
print("computer 的词向量:", word_to_vec.get("computer"))
2.1 寻找相似的词¶
请在词汇表中寻找跟computer最相似的8个单词并打印它们与computer之间的相似度,可以使用余弦相似度度量两个单词的相似性。
import numpy as np
from scipy.spatial.distance import cosine
def find_top_similar_words(target_word, word_to_vec, top_n=8):
"""
找到离 target_word 最近的 top_n 个单词(基于余弦相似度)
:param target_word: 目标单词
:param word_to_vec: 词向量字典 {word: vector}
:param top_n: 返回最相近的单词数
:return: [(word, similarity)] 排序后的列表
"""
return similarities[:top_n]
# 查找 "computer" 最相似的 8 个单词
top_words = find_top_similar_words("computer", word_to_vec, top_n=8)
# 打印结果
for word, sim in top_words:
print(f"{word}: {sim:.4f}")
2.2 多义词¶
请从bank、apple、spring中任选一个多义词,并且使用Glove词嵌入进行验证。请先说明你选择的词,再打印与其最相似的15个单词及相似度,并分析这些结果是否同时覆盖了至少两种含义。下面的代码以bank为示例。
import numpy as np
from scipy.spatial.distance import cosine
def find_top_similar_words(target_word, word_to_vec, top_n=15):
"""
找到离 target_word 最近的 top_n 个单词(基于余弦相似度)
:param target_word: 目标单词
:param word_to_vec: 词向量字典 {word: vector}
:param top_n: 返回最相近的单词数
:return: [(word, similarity)] 排序后的列表
"""
return similarities[:top_n]
# 查找 "bank" 最相似的 15 个单词
top_words = find_top_similar_words("bank", word_to_vec, top_n=15)
# 打印结果
for word, sim in top_words:
print(f"{word}: {sim:.4f}")
2.3 使用词嵌入表示关系(类比)¶
有一个著名的例子是: 国王的词嵌入-男人的词嵌入约等于女王的词嵌入-女人的词嵌入,即embedding(国王)-embedding(男人)≈embedding(女王)-embedding(女人)。
基于这个案例,我们可以用embedding(france)-embedding(paris)定义首都的关系。请基于法国-巴黎得到的首都关系向量,找出意大利的首都,并打印出相似度最高的8个单词。再用类似的方式找出罗马(rome)为首都对应的国家。
import numpy as np
from scipy.spatial.distance import cosine
def find_top_similar_embeddings(target_embedding, word_to_vec, top_n=8):
"""
根据一个词向量,找到最相似的 top_n 个单词(基于余弦相似度)
:param target_embedding: 目标词向量 (numpy 数组)
:param word_to_vec: 词向量字典 {word: vector}
:param top_n: 返回最相近的单词数
:return: [(word, similarity)] 排序后的列表
"""
similarities = []
# 遍历所有单词,计算余弦相似度
for word, vec in word_to_vec.items():
similarity = 1 - cosine(target_embedding, vec) # 余弦相似度
similarities.append((word, similarity))
# 按相似度排序(降序)
similarities.sort(key=lambda x: x[1], reverse=True)
return similarities[:top_n]
# 获得以下单词的词嵌入
italy_, france_, paris_, rome_ = (
word_to_vec.get("italy"),
word_to_vec.get("france"),
word_to_vec.get("paris"),
word_to_vec.get("rome"),
)
2.4 词嵌入的不足¶
请叙述glove词嵌入的不足。言之有理即可,但避免出现一些比较大的阐述且没有分析,如性能一般,训练语料较少等。
3. 使用词嵌入进行文本分类¶
我们接下来将基于Glove词嵌入对AG News数据集进行文本分类。
AG News 数据集简介
AG News 数据集来源于 AG's corpus of news articles,是一个大型的新闻数据集,由 Antonio Gulli 从多个新闻网站收集整理。 AG News 数据集包含 4 类新闻,每类 30,000 条训练数据,共 120,000 条训练样本 和 7,600 条测试样本。
3.1 文本预处理¶
首先导入所需模块:
可能需要安装datasets包
import torch
import torch.nn as nn
import torch.optim as optim
from datasets import load_dataset, load_from_disk
from collections import Counter
from torch.nn.utils.rnn import pad_sequence
import torch.nn.functional as F
from tqdm import tqdm
import os
我们从AG News 数据集中加载文本。这是一个较小的语料库,有150000多个单词,但足够我们小试牛刀.
data_path = "ag_news"
dataset = load_from_disk(data_path)
# 提取所有文本数据和标签
train_text = [item['text'] for item in dataset['train']]
train_y = [item['label'] for item in dataset['train']]
test_text = [item['text'] for item in dataset['test']]
test_y = [item['label'] for item in dataset['test']]
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
词元化 下面的tokenize函数将文本行列表(lines)作为输入,列表中的每个元素是一个文本序列(如一条文本行)。每个文本序列又被拆分成一个词元列表,词元(token)是文本的基本单位。最后,返回一个由词元列表组成的列表,其中的每个词元都是一个字符串(string)。词元的类型是字符串,而模型需要的输入是数字,因此这种类型不方便模型使用。现在,让我们构建一个字典,通常也叫做词表(vocabulary),用来将字符串类型的词元映射到从0开始的数字索引中。这里我们使用之前Glove中定义过的word_to_index。
在这些步骤后,我们顺序地把一段文本映射成了数字,可以送入模型中进行处理。为了减少 padding 对后续池化结果的干扰,这里统一将文本处理为长度为80的序列,并在模型中显式忽略 <pad> 位置。
# 使用 split 进行分词
def tokenize(text):
return text.lower().split()
def numericalize(text):
return torch.tensor([word_to_index.get(word, word_to_index["<UNK>"]) for word in tokenize(text)], dtype=torch.long)
def pad_tensor(tensor, target_length=80, pad_value=0):
"""
Pads a tensor with the given pad_value up to target_length.
Args:
tensor (torch.Tensor): Input 1D tensor.
target_length (int): Desired length after padding. Default is 80.
pad_value (int): Value to pad with. Default is 0.
Returns:
torch.Tensor: Padded tensor of shape (target_length,).
"""
current_length = tensor.size(0)
if current_length >= target_length:
return tensor[:target_length] # Truncate if longer
else:
padding = torch.full((target_length - current_length,), pad_value, dtype=tensor.dtype)
return torch.cat((tensor, padding), dim=0)
# 生成训练数据
def create_data(text_list, seq_len=80):
X = []
for text in text_list:
token_ids = numericalize(text)
# 都处理成长度为80的序列
token_ids = pad_tensor(token_ids, target_length=seq_len)
X.append(token_ids)
return torch.stack(X)
# 生成训练数据
X_train = create_data(train_text, seq_len=80)
Y_train = torch.tensor(train_y, dtype=torch.long)
# 生成测试数据
X_test = create_data(test_text, seq_len=80)
Y_test = torch.tensor(test_y, dtype=torch.long)
# 考虑到训练时间 只取前 50% 的数据
subset_size = int(0.5 * len(X_train)) # 计算 50% 的样本数量
X_train = X_train[:subset_size]
Y_train = Y_train[:subset_size]
# 创建 DataLoader
batch_size = 32
train_data = torch.utils.data.TensorDataset(X_train, Y_train)
train_loader = torch.utils.data.DataLoader(train_data, batch_size=batch_size, shuffle=True)
test_data = torch.utils.data.TensorDataset(X_test, Y_test)
test_loader = torch.utils.data.DataLoader(test_data, batch_size=batch_size, shuffle=False)
3.2 嵌入层¶
nn.Embedding()是 PyTorch 中用于创建词嵌入层(embedding layer)的模块,通常用于自然语言处理(NLP)任务。它的主要功能是将单词索引映射为稠密的向量表示。
在本次实验中,我们使用 nn.Embedding.from_pretrained(embedding_matrix, freeze=True, padding_idx=0) 初始化嵌入层。其中 embedding_matrix 是 GloVe 词嵌入,freeze=True 表示初始版本先固定词向量。与往年只使用平均池化不同,这一版要求你同时使用 mask 均值池化和 mask 最大池化,再将二者拼接后送入分类器,从而同时保留整体语义和局部显著特征。
3.3 文本分类网络¶
请基于在上文给出的数据处理和词嵌入矩阵,完成以下文本分类代码。这里需要你完成五个部分:定义文本分类网络、实现带 mask 的均值池化与最大池化融合、实现训练函数并返回平均 loss、实现测试函数以及定义损失函数。注意 forward 中要使用 mask 忽略 padding 位置,并正确处理最大池化时的 pad 干扰。
# TODO 1: 定义文本分类网络
class TextClassifier(nn.Module):
def __init__(self, vocab_size, embedding_dim, hidden_dim, num_classes):
super(TextClassifier, self).__init__()
# TODO 1.1: 使用 embedding_matrix 初始化嵌入层,记得设置 freeze=True 和 padding_idx=0
# TODO 1.2: 定义两层分类器:Linear(embedding_dim * 2 -> hidden_dim) -> ReLU -> Linear(hidden_dim -> num_classes)
def forward(self, x):
mask = (x != 0).unsqueeze(-1).float()
x = self.embedding(x)
# TODO 1.3: 分别实现 mask 均值池化和 mask 最大池化
# 提示:最大池化前可以先把 pad 位置替换成一个极小值,再在池化后与均值池化结果拼接
return self.classifier(x)
# TODO 2: 实现训练函数,注意要把数据也放到 device 上,并返回当前 epoch 的平均 loss
def train_model(model, dataloader, criterion, optimizer):
pass
# TODO 3: 实现测试函数,返回在测试集上的准确率
def evaluate_model(model, dataloader):
pass
# 初始化模型
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
embedding_matrix = torch.Tensor(embedding_matrix)
vocab_size, embedding_dim = embedding_matrix.shape
hidden_dim = 128
num_classes = 4
model = TextClassifier(vocab_size, embedding_dim, hidden_dim, num_classes).to(device)
# TODO 4: 定义交叉熵损失函数
optimizer = optim.Adam(model.parameters(), lr=0.001)
# 训练模型
EPOCHS = 5
for epoch in range(EPOCHS):
train_loss = train_model(model, train_loader, criterion, optimizer)
acc = evaluate_model(model, test_loader)
print(f"Epoch {epoch+1}, Loss: {train_loss:.4f}, Accuracy: {acc*100:.2f}%")
思考题
在相同 epoch、batch size 和学习率下,比较以下三种设定的测试集准确率:Glove 初始化 + freeze=True + 均值池化、Glove 初始化 + freeze=True + 均值/最大值融合池化、Glove 初始化 + freeze=False + 均值/最大值融合池化。请分析差异来源。
思考题
结合 computer 相似词、多义词验证和类比任务的实验结果,各举出一个“比较合理”和一个“你认为不太合理”的结果,并分析产生这些结果的可能原因。
思考题
在不改变主干结构大类(仍然是 Embedding + 池化 + 分类器)且不修改 batch size 与学习率的前提下,尝试一种数据处理层面的改动来提升分类效果,例如 OOV 处理、padding 方式、截断位置、tokenize 方式等。请给出修改代码、结果和分析。