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

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

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

前言

语言是人类智慧的结晶,但计算机只能理解数字。如何把"语言"变成"数字"?这就是**词嵌入(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. 维度灾难:词汇表有多大,向量就有多长
  2. 稀疏性:只有一个位置是1,其余都是0
  3. 语义缺失:无法表达词与词之间的相似性
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 上下文感知 动态语义 需要预训练

思考与练习

  1. 思考题

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

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


下期预告

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

会解答这些问题:

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

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


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

相关推荐
新智元1 小时前
OpenClaw 引爆纽约集会,虾教日烧 10 亿 tokens!老黄认证:史上最强软件
人工智能
石臻臻的杂货铺2 小时前
OpenClaw 永久免费的提取任何网页的终极方案
人工智能
一语07162 小时前
3分钟搞懂深度学习AI:实操篇:卷积层
人工智能·算法
工边页字2 小时前
AI 开发必懂:Context Window(上下文窗口)到底是什么?
前端·人工智能·后端
火山引擎开发者社区2 小时前
AgentKit 云端沙盒赋能 AI 内容创作,让创意触手可及
人工智能
hyunbar7772 小时前
创建个人知识库(lamaIndex + ChromaDB + 本地开源模型)
人工智能
claude_dev2 小时前
基于 Win10 从零搭建 OpenClaw:Kimi K2.5 + 飞书机器人 完整实战指南
人工智能
over6972 小时前
📸《拍照记单词》—— 从零到上线的完整开发指南(超详细版)
前端·人工智能·产品
数字卢语2 小时前
OpenClaw 多 Agent 实战:腾讯云部署到 Telegram 群聊分身协作
人工智能