04-词向量到嵌入:让机器理解语言的奥秘

0 阅读8分钟

词向量到嵌入:让机器理解语言的奥秘

探索自然语言处理的核心技术,理解如何让计算机读懂人类语言。

前言

语言是人类智慧的结晶,但计算机只能理解数字。如何把"语言"变成"数字"?这就是**词嵌入(Word Embedding)**技术要解决的核心问题。

今天,我们从词向量的发展历程出发,深入理解这项让机器"读懂"语言的关键技术。


一、语言表示的演进

从符号到向量

计算机处理语言的第一步,是将语言转化为数值表示:

语言表示的三个阶段:

1. 独热编码(One-Hot)
   "苹果" → [0, 0, 0, 1, 0, 0, ...](词汇表长度)

2. 词向量(Word Vector)
   "苹果" → [0.23, -0.45, 0.67, ...](固定维度,如300维)

3. 上下文嵌入(Contextual Embedding)
   "苹果" → 根据上下文动态生成向量

为什么需要更好的表示?

独热编码的问题

# 假设词汇表有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. 维度灾难:词汇表有多大,向量就有多长
  2. 稀疏性:只有一个位置是1,其余都是0
  3. 语义缺失:无法表达词与词之间的相似性
"苹果" → [0, 0, 1, 0, 0, ...]
"香蕉" → [0, 0, 0, 1, 0, ...]

两者距离相等,无法体现它们都是"水果"

二、Word2Vec:词向量的里程碑

核心思想

词的语义由它的上下文决定——这是Word2Vec的理论基础。

"你可以通过一个人交往的朋友来了解这个人。" —— 同样适用于词

句子:我喜欢吃 苹果 和香蕉

苹果的上下文:我、喜欢、吃、和、香蕉
苹果的语义 ≈ 上下文词的语义

香蕉的上下文:我、喜欢、吃、苹果、和
苹果和香蕉有相似的上下文 → 它们语义相近

两种训练架构

1. CBOW(连续词袋模型)

目标:用上下文预测中心词

输入:上下文词 ["我", "喜欢", "吃", "香蕉"]
输出:中心词 "苹果"

        上下文词
           ↓
        取平均
           ↓
    ┌─────────────┐
    │   隐藏层    │  ← 词向量在这里学习
    └─────────────┘
           ↓
        Softmax
           ↓
        预测词
2. Skip-gram

目标:用中心词预测上下文

输入:中心词 "苹果"
输出:上下文词 ["我", "喜欢", "吃", "香蕉"]

        中心词
           ↓
    ┌─────────────┐
    │   隐藏层    │
    └─────────────┘
           ↓
        Softmax
           ↓
      多个上下文预测

代码实现

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()

词向量的神奇特性

训练完成后,词向量会呈现出令人惊叹的语义关系:

# 经典例子: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

词向量空间的几何意义

        王后(Queen)
            ●
           /│
          / │
         /  │
    女人 ●   │
        │   │
        │   │
    男人 ●───┼───● 国王(King)
        │
        │
        ●

性别关系:Man → Woman 的向量 ≈ King → Queen 的向量
国家-首都:France → Paris 的向量 ≈ Italy → Rome 的向量

三、GloVe:全局向量表示

核心思想

Word2Vec基于局部上下文,GloVe则结合了全局统计信息

共现矩阵(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利用这种全局共现统计来学习词向量

目标函数

J = Σᵢⱼ f(Xᵢⱼ) (wᵢᵀw̃ⱼ + bᵢ + b̃ⱼ - log Xᵢⱼ)²

其中:
- Xᵢⱼ:词i和词j的共现次数
- wᵢ, w̃ⱼ:词向量
- f(X):权重函数

四、FastText:字符级嵌入

核心创新

FastText将词分解为字符n-gram,能处理未登录词(OOV):

单词:"苹果"

字符n-gram(n=3):
<苹, 苹果, 果>, <苹果>

最终向量 = n-gram向量之和
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为每个词学习固定的向量:

"我 喜欢 吃 苹果"
"苹果 公司 发布 新款 iPhone"

两处的"苹果"向量相同,但语义不同!

上下文嵌入

BERT等模型根据上下文动态生成向量:

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. 文本相似度计算

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. 词义可视化

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上下文感知动态语义需要预训练

思考与练习

  1. 思考题

    • 为什么词向量能捕捉语义关系?
    • 静态嵌入和动态嵌入各适合什么场景?
  2. 动手练习

    • 使用Gensim训练一个中文Word2Vec模型
    • 用t-SNE可视化词向量空间
  3. 延伸阅读


下期预告

下一篇文章,我们将深入探讨:GPT系列全解析:从GPT-1到GPT-4的进化

会解答这些问题:

  • GPT系列的技术演进路径是什么?
  • 每一代GPT的关键创新是什么?
  • ChatGPT是如何炼成的?

关注专栏,不错过后续更新!


作者:ECH00O00 本文首发于掘金专栏《AI科普实验室》 欢迎评论区交流讨论,点赞收藏就是最大的鼓励 ❤️