词向量到嵌入:让机器理解语言的奥秘
探索自然语言处理的核心技术,理解如何让计算机读懂人类语言。
前言
语言是人类智慧的结晶,但计算机只能理解数字。如何把"语言"变成"数字"?这就是**词嵌入(Word Embedding)**技术要解决的核心问题。
今天,我们从词向量的发展历程出发,深入理解这项让机器"读懂"语言的关键技术。
一、语言表示的演进
从符号到向量
计算机处理语言的第一步,是将语言转化为数值表示:
sql
语言表示的三个阶段:
1. 独热编码(One-Hot)
"苹果" → [0, 0, 0, 1, 0, 0, ...](词汇表长度)
2. 词向量(Word Vector)
"苹果" → [0.23, -0.45, 0.67, ...](固定维度,如300维)
3. 上下文嵌入(Contextual Embedding)
"苹果" → 根据上下文动态生成向量
为什么需要更好的表示?
独热编码的问题:
python
# 假设词汇表有10000个词
vocab = {"我": 0, "喜欢": 1, "苹果": 2, "香蕉": 3, ...}
# One-Hot编码
def one_hot(word, vocab_size=10000):
vec = [0] * vocab_size
vec[vocab[word]] = 1
return vec
print(one_hot("苹果"))
# [0, 0, 1, 0, 0, ..., 0] 共10000个数字,只有一个1
问题:
- 维度灾难:词汇表有多大,向量就有多长
- 稀疏性:只有一个位置是1,其余都是0
- 语义缺失:无法表达词与词之间的相似性
arduino
"苹果" → [0, 0, 1, 0, 0, ...]
"香蕉" → [0, 0, 0, 1, 0, ...]
两者距离相等,无法体现它们都是"水果"
二、Word2Vec:词向量的里程碑
核心思想
词的语义由它的上下文决定------这是Word2Vec的理论基础。
"你可以通过一个人交往的朋友来了解这个人。" ------ 同样适用于词
句子:我喜欢吃 苹果 和香蕉
苹果的上下文:我、喜欢、吃、和、香蕉
苹果的语义 ≈ 上下文词的语义
香蕉的上下文:我、喜欢、吃、苹果、和
苹果和香蕉有相似的上下文 → 它们语义相近
两种训练架构
1. CBOW(连续词袋模型)
目标:用上下文预测中心词
arduino
输入:上下文词 ["我", "喜欢", "吃", "香蕉"]
输出:中心词 "苹果"
上下文词
↓
取平均
↓
┌─────────────┐
│ 隐藏层 │ ← 词向量在这里学习
└─────────────┘
↓
Softmax
↓
预测词
2. Skip-gram
目标:用中心词预测上下文
arduino
输入:中心词 "苹果"
输出:上下文词 ["我", "喜欢", "吃", "香蕉"]
中心词
↓
┌─────────────┐
│ 隐藏层 │
└─────────────┘
↓
Softmax
↓
多个上下文预测
代码实现
python
import torch
import torch.nn as nn
import torch.optim as optim
class SkipGram(nn.Module):
def __init__(self, vocab_size, embedding_dim):
super().__init__()
# 中心词嵌入
self.center_embedding = nn.Embedding(vocab_size, embedding_dim)
# 上下文词嵌入
self.context_embedding = nn.Embedding(vocab_size, embedding_dim)
# 初始化
nn.init.xavier_uniform_(self.center_embedding.weight)
nn.init.xavier_uniform_(self.context_embedding.weight)
def forward(self, center_word, context_word):
# 获取嵌入向量
center_vec = self.center_embedding(center_word) # (batch, embed_dim)
context_vec = self.context_embedding(context_word) # (batch, embed_dim)
# 计算点积
score = torch.sum(center_vec * context_vec, dim=1) # (batch,)
return score
def get_embedding(self, word_idx):
"""获取词向量"""
return self.center_embedding.weight[word_idx]
# 训练示例
def train_skipgram():
# 模拟数据
corpus = ["我 喜欢 吃 苹果", "我 喜欢 吃 香蕉", "他 不 喜欢 苹果"]
vocab = {"我": 0, "喜欢": 1, "吃": 2, "苹果": 3, "香蕉": 4, "他": 5, "不": 6}
vocab_size = len(vocab)
embedding_dim = 10
model = SkipGram(vocab_size, embedding_dim)
optimizer = optim.Adam(model.parameters(), lr=0.01)
# 生成训练数据(中心词-上下文对)
def get_training_data(corpus, window_size=2):
data = []
for sentence in corpus:
words = sentence.split()
for i, center in enumerate(words):
for j in range(max(0, i - window_size), min(len(words), i + window_size + 1)):
if i != j:
data.append((vocab[words[i]], vocab[words[j]]))
return data
training_data = get_training_data(corpus)
# 训练
for epoch in range(100):
total_loss = 0
for center, context in training_data:
center_tensor = torch.tensor([center])
context_tensor = torch.tensor([context])
# 正样本
pos_score = model(center_tensor, context_tensor)
pos_loss = -torch.log(torch.sigmoid(pos_score))
# 负采样(简化:随机选负样本)
neg_context = torch.randint(0, vocab_size, (1,))
neg_score = model(center_tensor, neg_context)
neg_loss = -torch.log(1 - torch.sigmoid(neg_score))
loss = pos_loss + neg_loss
total_loss += loss.item()
optimizer.zero_grad()
loss.backward()
optimizer.step()
if epoch % 20 == 0:
print(f"Epoch {epoch}, Loss: {total_loss:.4f}")
# 查看词向量
print("\n词向量示例:")
for word, idx in vocab.items():
vec = model.get_embedding(idx).detach().numpy()
print(f"{word}: {vec[:3]}...") # 只显示前3维
train_skipgram()
词向量的神奇特性
训练完成后,词向量会呈现出令人惊叹的语义关系:
python
# 经典例子:King - Man + Woman ≈ Queen
# 使用预训练的GloVe向量
import numpy as np
def load_glove_vectors(glove_file):
"""加载GloVe预训练向量"""
word_vectors = {}
with open(glove_file, 'r', encoding='utf-8') as f:
for line in f:
values = line.split()
word = values[0]
vector = np.array(values[1:], dtype='float32')
word_vectors[word] = vector
return word_vectors
def analogy(word_vectors, a, b, c):
"""
类比推理:a is to b as c is to ?
计算: result = b - a + c
"""
result = word_vectors[b] - word_vectors[a] + word_vectors[c]
# 找最相似的词
best_word = None
best_similarity = -1
for word, vector in word_vectors.items():
if word in [a, b, c]:
continue
similarity = np.dot(result, vector) / (np.linalg.norm(result) * np.linalg.norm(vector))
if similarity > best_similarity:
best_similarity = similarity
best_word = word
return best_word
# 示例结果
print("King - Man + Woman =", analogy("king", "man", "woman")) # queen
print("Paris - France + Italy =", analogy("paris", "france", "italy")) # rome
词向量空间的几何意义:
scss
王后(Queen)
●
/│
/ │
/ │
女人 ● │
│ │
│ │
男人 ●───┼───● 国王(King)
│
│
●
性别关系:Man → Woman 的向量 ≈ King → Queen 的向量
国家-首都:France → Paris 的向量 ≈ Italy → Rome 的向量
三、GloVe:全局向量表示
核心思想
Word2Vec基于局部上下文,GloVe则结合了全局统计信息:
markdown
共现矩阵(Co-occurrence Matrix):
我 喜欢 吃 苹果 香蕉
我 0 3 2 2 2
喜欢 3 0 3 2 2
吃 2 3 0 1 1
苹果 2 2 1 0 1
香蕉 2 2 1 1 0
GloVe利用这种全局共现统计来学习词向量
目标函数
scss
J = Σᵢⱼ f(Xᵢⱼ) (wᵢᵀw̃ⱼ + bᵢ + b̃ⱼ - log Xᵢⱼ)²
其中:
- Xᵢⱼ:词i和词j的共现次数
- wᵢ, w̃ⱼ:词向量
- f(X):权重函数
四、FastText:字符级嵌入
核心创新
FastText将词分解为字符n-gram,能处理未登录词(OOV):
ini
单词:"苹果"
字符n-gram(n=3):
<苹, 苹果, 果>, <苹果>
最终向量 = n-gram向量之和
python
def get_ngrams(word, n=3):
"""获取字符n-gram"""
# 添加边界符号
word = '<' + word + '>'
ngrams = []
for i in range(len(word) - n + 1):
ngrams.append(word[i:i+n])
return ngrams
print(get_ngrams("apple"))
# ['<ap', 'app', 'ppl', 'ple', 'le>']
优势:
- 能处理拼写错误
- 能处理新词
- 对形态丰富的语言效果更好
五、上下文嵌入:BERT时代的革命
静态嵌入的局限
Word2Vec/GloVe为每个词学习固定的向量:
arduino
"我 喜欢 吃 苹果"
"苹果 公司 发布 新款 iPhone"
两处的"苹果"向量相同,但语义不同!
上下文嵌入
BERT等模型根据上下文动态生成向量:
python
from transformers import BertModel, BertTokenizer
import torch
# 加载预训练模型
tokenizer = BertTokenizer.from_pretrained('bert-base-chinese')
model = BertModel.from_pretrained('bert-base-chinese')
def get_contextual_embedding(text, word_idx):
"""获取上下文嵌入"""
inputs = tokenizer(text, return_tensors='pt')
outputs = model(**inputs)
# 获取指定位置的嵌入
embedding = outputs.last_hidden_state[0, word_idx, :]
return embedding.detach().numpy()
# 同一个词,不同上下文
text1 = "我喜欢吃苹果"
text2 = "苹果公司发布新品"
emb1 = get_contextual_embedding(text1, 4) # "苹果"
emb2 = get_contextual_embedding(text2, 0) # "苹果"
# 两个嵌入不同!
similarity = np.dot(emb1, emb2) / (np.linalg.norm(emb1) * np.linalg.norm(emb2))
print(f"相似度: {similarity:.4f}") # 低于1,因为语义不同
主流嵌入模型对比
| 模型 | 类型 | 特点 |
|---|---|---|
| Word2Vec | 静态 | 开创性工作,简单高效 |
| GloVe | 静态 | 利用全局统计信息 |
| FastText | 静态 | 支持子词,处理OOV |
| ELMo | 动态 | 双向LSTM,上下文相关 |
| BERT | 动态 | 双向Transformer,强大 |
| GPT | 动态 | 单向Transformer,生成能力强 |
| Word2Vec | 静态 | 开创性工作,简单高效 |
六、嵌入的实际应用
1. 文本相似度计算
python
def cosine_similarity(vec1, vec2):
"""余弦相似度"""
return np.dot(vec1, vec2) / (np.linalg.norm(vec1) * np.linalg.norm(vec2))
# 句子向量 = 词向量的平均
def sentence_embedding(sentence, word_vectors):
words = sentence.split()
vectors = [word_vectors[w] for w in words if w in word_vectors]
if not vectors:
return None
return np.mean(vectors, axis=0)
# 相似度计算
sentence1 = "我喜欢吃苹果"
sentence2 = "我爱吃香蕉"
sentence3 = "今天天气很好"
emb1 = sentence_embedding(sentence1, word_vectors)
emb2 = sentence_embedding(sentence2, word_vectors)
emb3 = sentence_embedding(sentence3, word_vectors)
print(f"句子1和句子2相似度: {cosine_similarity(emb1, emb2):.4f}")
print(f"句子1和句子3相似度: {cosine_similarity(emb1, emb3):.4f}")
2. 词义可视化
python
from sklearn.decomposition import PCA
import matplotlib.pyplot as plt
def visualize_words(word_vectors, words):
"""词向量可视化"""
# 获取词向量矩阵
vectors = np.array([word_vectors[w] for w in words])
# PCA降维到2维
pca = PCA(n_components=2)
result = pca.fit_transform(vectors)
# 绘图
plt.figure(figsize=(10, 8))
plt.scatter(result[:, 0], result[:, 1])
for i, word in enumerate(words):
plt.annotate(word, xy=(result[i, 0], result[i, 1]))
plt.title("词向量可视化")
plt.show()
# 可视化示例
words = ["国王", "王后", "男人", "女人", "苹果", "香蕉", "水果"]
visualize_words(word_vectors, words)
七、嵌入技术的未来发展
趋势与挑战
| 方向 | 说明 |
|---|---|
| 多语言嵌入 | 跨语言的统一表示空间 |
| 多模态嵌入 | 文本、图像、音频统一表示 |
| 领域自适应 | 针对特定领域优化嵌入 |
| 高效压缩 | 降低嵌入维度,减少存储 |
小结
| 技术 | 核心思想 | 优势 | 局限 |
|---|---|---|---|
| One-Hot | 简单编码 | 直观 | 维度灾难、无语义 |
| Word2Vec | 上下文预测 | 语义关系 | 静态向量 |
| GloVe | 全局共现 | 统计信息 | 静态向量 |
| FastText | 字符n-gram | 处理OOV | 计算量大 |
| BERT | 上下文感知 | 动态语义 | 需要预训练 |
思考与练习
-
思考题:
- 为什么词向量能捕捉语义关系?
- 静态嵌入和动态嵌入各适合什么场景?
-
动手练习:
- 使用Gensim训练一个中文Word2Vec模型
- 用t-SNE可视化词向量空间
-
延伸阅读:
下期预告
下一篇文章,我们将深入探讨:GPT系列全解析:从GPT-1到GPT-4的进化
会解答这些问题:
- GPT系列的技术演进路径是什么?
- 每一代GPT的关键创新是什么?
- ChatGPT是如何炼成的?
关注专栏,不错过后续更新!
作者:ECH00O00 本文首发于掘金专栏《AI科普实验室》 欢迎评论区交流讨论,点赞收藏就是最大的鼓励 ❤️