实验任务一: 词嵌入¶
词嵌入¶
实验目标
通过本次实验,你将掌握以下内容:
- 认识词嵌入。
- 从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 = {}
# 读取 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 + 1 # 从1开始编号
index_to_word[idx + 1] = word
# 创建嵌入矩阵 (词汇大小 x 维度)
vocab_size = len(word_to_vec) + 1 # +1 是因为从1开始编号
embedding_matrix = np.zeros((vocab_size, embedding_dim), dtype=np.float32)
for word, idx in word_to_index.items():
embedding_matrix[idx] = word_to_vec[word]
return word_to_vec, word_to_index, index_to_word, embedding_matrix
# 使用示例(请替换 'glove.6B.50d.txt' 为你的GloVe文件路径)
glove_path = "glove.6B.50d.txt"
word_to_vec, word_to_index, index_to_word, embedding_matrix = load_glove_embeddings(glove_path)
# 示例:查看 'king' 的词向量
print("king 的词向量:", word_to_vec.get("king"))
2.1 寻找相似的词¶
请在词汇表中寻找跟king最相似的10个单词并打印这两个单词间的相似度,可以使用余弦相似度度量两个单词的相似性。
import numpy as np
from scipy.spatial.distance import cosine
def find_top_similar_words(target_word, word_to_vec, top_n=5):
"""
找到离 target_word 最近的 top_n 个单词(基于余弦相似度)
:param target_word: 目标单词
:param word_to_vec: 词向量字典 {word: vector}
:param top_n: 返回最相近的单词数
:return: [(word, similarity)] 排序后的列表
"""
return similarities[:top_n]
# 查找 "king" 最相似的 20 个单词
top_words = find_top_similar_words("king", word_to_vec, top_n=5)
# 打印结果
for word, sim in top_words:
print(f"{word}: {sim:.4f}")
2.2 多义词¶
有一些词往往具有多个意思比如苹果。请先思考一个多义词,并且使用Glove词嵌入进行验证。即Glove中与其最相似的20个单词中是否包含这两个意思的相关单词。最后,请给出这个单词并且打印跟其最相似的20个单词的相似度。
import numpy as np
from scipy.spatial.distance import cosine
def find_top_similar_words(target_word, word_to_vec, top_n=5):
"""
找到离 target_word 最近的 top_n 个单词(基于余弦相似度)
:param target_word: 目标单词
:param word_to_vec: 词向量字典 {word: vector}
:param top_n: 返回最相近的单词数
:return: [(word, similarity)] 排序后的列表
"""
return similarities[:top_n]
# 查找 "king" 最相似的 20 个单词
top_words = find_top_similar_words("king", word_to_vec, top_n=20)
# 打印结果
for word, sim in top_words:
print(f"{word}: {sim:.4f}")
2.3 使用词嵌入表示关系(类比)¶
有一个著名的例子是: 国王的词嵌入-男人的词嵌入约等于女王的词嵌入-女人的词嵌入,即embedding(国王)-embedding(男人)≈embedding(女王)-embedding(女人)。
基于这个案例,我们可以用embedding(china)-embedding(beijing)定义首都的关系。请基于中国-北京的得到的首都关系向量,找出英国的首都。英国使用2个单词england和britain进行探索,并且打印出相似度最高的10个单词。再用类似的方式找出伦敦(london)为首都对应的国家。
import numpy as np
from scipy.spatial.distance import cosine
def find_top_similar_embeddings(target_embedding, word_to_vec, top_n=10):
"""
根据一个词向量,找到最相似的 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]
# 获得以下单词的词嵌入
england_, china_, beijing_ = word_to_vec.get("england"), word_to_vec.get("china"), word_to_vec.get("beijing")
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。
在这些步骤后,我们顺序地把一段文本映射成了数字,可以送入模型中进行处理。
# 使用 split 进行分词
def tokenize(text):
return text.lower().split()
def numericalize(text):
return torch.tensor([word_to_index.get(word, 0) for word in tokenize(text)], dtype=torch.long)
def pad_tensor(tensor, target_length=100, 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 100.
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=100):
X = []
for text in text_list:
token_ids = numericalize(text)
# 都处理成长度为100的序列
token_ids = pad_tensor(token_ids)
X.append(token_ids)
return torch.stack(X)
# 生成训练数据
X_train = create_data(train_text, seq_len=100)
Y_train = torch.Tensor(train_y)
# 生成测试数据
X_test = create_data(test_text, seq_len=100)
Y_test = torch.Tensor(test_y)
# 考虑到训练时间 只取前 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=True)
3.2 嵌入层¶
nn.Embedding()是 PyTorch 中用于创建词嵌入层(embedding layer)的模块,通常用于自然语言处理(NLP)任务。它的主要功能是将单词索引映射为稠密的向量表示。
在本次实验中,我们使用self.embedding = nn.Embedding.from_pretrained(embedding_matrix, freeze=True),其中参数embedding_matrix是Glove的词嵌入,即我们使用Glove的词嵌入来初始化嵌入层;参数freeze表示嵌入层是否会更新参数,我们设置为freeze=True,即不会更新词嵌入。
3.3 文本分类网络¶
请基于在上文给出的数据处理和词嵌入矩阵,完成以下文本分类代码。包括四个部分,定义文本分类网络,实现训练函数,实现测试函数以及定义损失函数。
#TODO:定义文本分类网络
class TextClassifier(nn.Module):
def __init__(self, vocab_size, embedding_dim, hidden_dim, num_classes):
super(TextClassifier, self).__init__()
#TODO: 实现模型结构
#TODO 实现self.embedding: 嵌入层
#TODO 实现self.fc: 分类层
def forward(self, x):
x = self.embedding(x)
#TODO: 对一个句子中的所有单词的嵌入取平均得到最终的文档嵌入
return self.fc(x)
# TODO: 实现训练函数,注意要把数据也放到gpu上避免报错
def train_model(model, dataloader, criterion, optimizer):
# TODO: 实现测试函数,返回在测试集上的准确率
def evaluate_model(model, dataloader):
# 初始化模型
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
embedding_matrix = torch.Tensor(embedding_matrix)
model = TextClassifier(embedding_matrix).to(device)
#TODO 实现criterion: 定义交叉熵损失函数
optimizer = optim.Adam(model.parameters(), lr=0.001)
# 训练模型
EPOCHS = 5
for epoch in range(EPOCHS):
train_model(model, train_loader, criterion, optimizer)
acc = evaluate_model(model, test_loader)
print(f"Epoch {epoch+1}, Accuracy: {acc*100:.2f}%")
思考题
使用Glove词嵌入进行初始化,是否比随机初始化取得更好的效果?
思考题
上述代码在不改变模型(即仍然只有self.embedding和self.fc,不额外引入如dropout等层)和超参数(即batch size和学习率)的情况下,我们可以修改哪些地方来提升模型性能。请列举两个方面。