词嵌入(Word Embedding) 是一种将词汇表示为实数向量 的技术,通常是低维度的连续向量。这些向量被设计为捕捉词汇之间的语义相似性 ,使得语义相似的词在嵌入空间中的距离也更近。词嵌入可以看作是将离散的语言符号(如单词、短语)映射到向量空间,从而在一定程度上解决了自然语言处理中的语义表示问题。
简单来说,词嵌入就是一种将词汇转换成向量的方法,使得计算机能够理解词与词之间的关系,并在不同的NLP任务(如文本分类、机器翻译、问答系统等)中表现出色。
一、词嵌入的动机和重要性
自然语言中的单词本质上是离散的符号,对于计算机来说,直接处理这些符号是困难的。传统的词表示方法(如词袋模型(Bag of Words) 、TF-IDF等)会导致词与词之间的相互关系无法捕捉到,例如"king"和"queen"这两个词虽然表示不同的对象,但在某种语义上有相似性。
词嵌入技术通过以下方式克服这些问题:
- 密集表示:将单词表示为低维的密集向量(例如100维或300维),而不是像词袋模型那样的高维稀疏向量。
- 语义信息捕捉:相似的词(如"king"和"queen")在向量空间中靠得更近。
- 可用于神经网络:词嵌入作为向量输入,能够直接用于神经网络模型(如RNN、LSTM、Transformer)进行下游的NLP任务。
二、词嵌入的表示形式
-
One-hot表示:在早期,单词通常以one-hot编码表示。词汇表中的每个单词用一个长度为词汇表大小的向量表示,在向量中,目标单词的位置为1,其余位置为0。这种表示无法捕捉单词之间的语义相似性,且向量维度非常高。
例如,假设词汇表包含 ["dog", "cat", "mouse", "king", "queen"],那么 "dog" 的 one-hot 表示是:
[1, 0, 0, 0, 0]
-
词嵌入表示:与one-hot不同,词嵌入将单词映射到低维实数向量。这个向量表示捕捉到了词汇的语义关系和上下文特性,例如,通过预训练的词嵌入,模型会"知道" "king" 和 "queen" 是相似的,因为它们的向量距离较近。
例如,"dog" 的词嵌入可能是:
[0.13, -0.24, 0.65, ... , 0.42]
三、常见的词嵌入技术
-
Word2Vec:
- Word2Vec 是最早成功应用于词嵌入学习的技术之一,由谷歌在2013年推出。它使用浅层神经网络,将单词映射到向量空间,捕捉词汇的语义相似性。
- Word2Vec 有两种模型:CBOW (连续词袋模型)和 Skip-gram。CBOW根据上下文词预测目标词,Skip-gram则根据目标词预测上下文。
优点:
- 能够高效地学习到低维度且能捕捉语义关系的词向量。
- 适用于大规模无监督文本数据。
缺点:
- 对于词汇的不同含义(如"bank"既可以指银行,也可以指河岸),它无法根据上下文进行区分。
-
GloVe:
- GloVe (Global Vectors for Word Representation) 是斯坦福大学提出的另一种词嵌入方法。它通过构建词汇共现矩阵并进行矩阵分解,生成词向量。它的目标是捕捉全局的语义信息,而不仅仅是局部上下文。
优点:
- GloVe捕捉全局语义信息,能够更好地反映词与词之间的统计关系。
- 提供了丰富的预训练模型(如Common Crawl和Wikipedia上训练的模型),可以在各种NLP任务中直接使用。
-
FastText:
- FastText 是Facebook提出的改进版Word2Vec,能够生成单词的子词级别嵌入。例如,FastText能够将词分解为多个n-gram(如"apple"可以分解为 "app", "ple"),然后为这些n-gram生成向量。这使得FastText在处理稀有词或拼写错误时表现更好。
优点:
- 能够处理未见词(Out-of-Vocabulary, OOV)问题,因为它可以生成单词的子词嵌入。
- 适合多语言和拼写错误的文本处理场景。
-
ELMo:
- ELMo (Embeddings from Language Models) 是一个上下文相关的词嵌入模型。与Word2Vec不同,ELMo可以根据不同的上下文为同一个词生成不同的嵌入向量。例如,词"bank"在"银行"和"河岸"这两种不同的上下文中会有不同的词向量。
- ELMo基于双向LSTM,通过联合语言模型的方式,结合上下文信息生成词向量。
优点:
- 能够根据上下文动态调整词的表示,克服了Word2Vec静态词嵌入的问题。
- 在NLP任务中表现优秀,尤其是在句子级别的任务上。
-
BERT:
- BERT (Bidirectional Encoder Representations from Transformers) 是基于Transformer架构的预训练语言模型。BERT能够捕捉到单词的双向上下文信息,这意味着它不仅考虑当前单词的前后文,而且能够生成动态的词嵌入。与ELMo相似,BERT的词嵌入是上下文相关的,但BERT的架构(Transformer)使得它更强大。
- BERT的预训练任务包括掩码语言模型(MLM)和下一个句子预测(NSP),通过这两项任务使得它在各种NLP任务中表现优异。
优点:
- 动态的词嵌入可以根据上下文生成最符合语义的词向量。
- 在多个NLP任务(如问答、机器翻译、文本分类)中达到了最新的性能记录。
四、词嵌入的应用场景
词嵌入的主要优势在于其能够捕捉单词之间的语义相似性,并为许多NLP任务提供有力支持。以下是一些常见的应用场景:
-
文本分类:
- 在情感分析、垃圾邮件检测、主题分类等任务中,词嵌入可以作为输入特征,为神经网络模型(如CNN、LSTM)提供丰富的语义信息。
-
信息检索:
- 词嵌入帮助提高搜索系统的性能,通过嵌入空间中的语义相似性,用户查询词可以与文档中的内容更好地匹配。
-
机器翻译:
- 词嵌入能够捕捉不同语言中的语义关系,帮助机器翻译系统生成更符合语义的翻译结果。
-
命名实体识别(NER):
- 词嵌入在命名实体识别任务中用于表示输入的单词特征,结合上下文信息帮助模型识别出人名、地名、公司名等实体。
-
问答系统:
- 在问答系统中,词嵌入帮助模型理解用户问题中的词语语义,并从知识库中找到最合适的答案。
-
推荐系统:
- 词嵌入可以用于推荐系统,将用户的文本评论或搜索词转换为语义向量,从而为用户推荐相关的产品或内容。
五、Tokenizer(分词器)
Tokenizer 是将原始文本(通常是句子或段落)**转换为可处理的离散单词或子词单位(token)**的过程。每个 token 通常表示文本中的一个单词、子词甚至字符。Tokenizer 的主要作用是将自然语言的字符串转换为模型能够理解的数值输入形式。
特点:
- 文本到token映射:Tokenizer的工作是将文本数据(字符串形式)分割为更小的单位,即 token。通常每个 token 对应于词汇表中的一个索引(整数),这些索引被输入到模型中。
- 词汇表 :Tokenizer基于一个词汇表(vocabulary),这个词汇表定义了所有可以被模型处理的单词或子词。当遇到词汇表之外的单词时,通常会有一种机制来处理,比如用特殊的未知词符
[UNK]
表示。 - 子词分词(如BPE、WordPiece):一些模型(如BERT、GPT)使用子词级别的tokenization,将单词拆分为多个子词单位,从而有效处理未见词(OOV)问题。例如,"playing" 可以被拆分为 "play" 和 "##ing"。
例子:
假设有如下句子:
python
sentence = "I love playing football."
使用 Tokenizer 后,这个句子可能会被分词为:
python
tokens = ["I", "love", "playing", "football", "."]
再进一步转换为索引(假设词汇表中每个单词有对应的ID):
python
token_ids = [101, 2000, 2030, 2079, 102]
这些 token ids 会被输入到模型中。
词嵌入和 Tokenizer 的工作流程
- 文本 -> Tokenizer :Tokenizer 是文本处理的第一步,用于将原始文本转换为 token(或词汇表中的索引)。这个步骤是模型的基础输入准备步骤。
- Token -> 词嵌入 :词嵌入 是将 token(离散索引表示)转换为实数向量。它是模型中的输入层之一,用于将离散的单词或 token 映射为可以处理的连续向量。
六、Word2Vec + LSTM 代码举例
以下是使用 Word2Vec 词嵌入与 LSTM 网络进行文本分类的完整代码。
bash
import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
from nltk.tokenize import word_tokenize
from sklearn.model_selection import train_test_split
from gensim.models import Word2Vec
import nltk
nltk.download('punkt')
# 示例数据集
data = [
("I love programming and coding.", "positive"),
("Python is an amazing language.", "positive"),
("I hate bugs in the code.", "negative"),
("Debugging is so frustrating.", "negative"),
("Machine learning is fascinating.", "positive"),
("I don't like syntax errors.", "negative")
]
# 提取文本和标签
sentences = [text for text, label in data]
labels = [1 if label == "positive" else 0 for text, label in data]
# 分词
tokenized_sentences = [word_tokenize(sentence.lower()) for sentence in sentences]
# 训练 Word2Vec 模型
word2vec_model = Word2Vec(sentences=tokenized_sentences, vector_size=100, window=5, min_count=1, workers=4)
# 为单词构建词汇表并映射单词到索引
vocab = {word: index for index, word in enumerate(word2vec_model.wv.index_to_key)}
vocab_size = len(vocab)
# 将每个句子转换为索引序列
def sentence_to_indices(sentence, vocab):
return [vocab[word] for word in word_tokenize(sentence.lower()) if word in vocab]
# 将句子转换为索引序列
X_indices = [sentence_to_indices(sentence, vocab) for sentence in sentences]
# 找出最长句子长度,确保所有输入有相同长度
max_length = max(len(seq) for seq in X_indices)
# 填充句子,使得它们长度一致(填充0)
def pad_sequence(seq, max_length):
padded = np.zeros(max_length, dtype=int)
padded[:len(seq)] = seq
return padded
X_padded = np.array([pad_sequence(seq, max_length) for seq in X_indices])
y = np.array(labels)
# 将数据集划分为训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X_padded, y, test_size=0.2, random_state=42)
# 将数据转换为 PyTorch 张量
X_train_tensor = torch.tensor(X_train, dtype=torch.long)
X_test_tensor = torch.tensor(X_test, dtype=torch.long)
y_train_tensor = torch.tensor(y_train, dtype=torch.long)
y_test_tensor = torch.tensor(y_test, dtype=torch.long)
# 生成嵌入矩阵,大小为 (vocab_size, vector_size)
embedding_matrix = np.zeros((vocab_size, word2vec_model.vector_size))
for word, index in vocab.items():
embedding_matrix[index] = word2vec_model.wv[word]
# 将嵌入矩阵转换为 PyTorch 张量
embedding_tensor = torch.tensor(embedding_matrix, dtype=torch.float32)
# 定义 LSTM 模型,使用 Word2Vec 词嵌入
class LSTMClassifier(nn.Module):
def __init__(self, embedding_matrix, hidden_size, output_size, num_layers, trainable=False):
super(LSTMClassifier, self).__init__()
vocab_size, embedding_dim = embedding_matrix.shape
self.embedding = nn.Embedding.from_pretrained(embedding_matrix, freeze=not trainable)
self.lstm = nn.LSTM(embedding_dim, hidden_size, num_layers, batch_first=True)
self.fc = nn.Linear(hidden_size, output_size)
def forward(self, x):
embedded = self.embedding(x)
_, (hn, _) = self.lstm(embedded)
out = self.fc(hn[-1])
return out
# 定义超参数
input_size = word2vec_model.vector_size # Word2Vec 生成的词向量维度
hidden_size = 128 # LSTM 隐藏层大小
output_size = 2 # 二分类问题
num_layers = 1 # LSTM 层数
learning_rate = 0.001
num_epochs = 10
# 初始化模型、损失函数和优化器
model = LSTMClassifier(embedding_tensor, hidden_size, output_size, num_layers, trainable=False)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=learning_rate)
# 训练模型
for epoch in range(num_epochs):
model.train()
outputs = model(X_train_tensor)
loss = criterion(outputs, y_train_tensor)
optimizer.zero_grad()
loss.backward()
optimizer.step()
print(f"Epoch [{epoch+1}/{num_epochs}], Loss: {loss.item():.4f}")
# 测试模型
model.eval()
with torch.no_grad():
test_outputs = model(X_test_tensor)
_, predicted = torch.max(test_outputs, 1)
accuracy = (predicted == y_test_tensor).sum().item() / len(y_test_tensor)
print(f"测试集准确率: {accuracy:.4f}")