2、词嵌入与文本表示:One-hot编码、Word2Vec原理、GloVe与FastText、TensorFlow实现词嵌入层

好,咱们进入第二章。说实话,词嵌入这块内容,是我在NLP实战中觉得最「有意思」的部分之一。为什么?因为你要让计算机理解「苹果」和「香蕉」是相似的,而「苹果」和「汽车」不相似——这本身就是个很神奇的过程。

我个人习惯把文本表示分成两个时代:一个是「原始人时代」,一个是「现代文明时代」。咱们先从原始人时代讲起。

2.1 One-hot编码:最朴素的表示法

One-hot编码,说白了就是给每个词发一个「身份证号」。假设你的词汇表里有10000个词,那每个词就是一个10000维的向量,其中只有一位是1,其余全是0。

举个例子:

词汇表 = ["我", "爱", "自然", "语言", "处理"]
"我"   → [1, 0, 0, 0, 0]
"爱"   → [0, 1, 0, 0, 0]
"自然" → [0, 0, 1, 0, 0]

嗯,这里要注意一个问题。One-hot有两个致命缺陷:

  • 维度灾难:词汇量一大,向量维度就爆炸。我见过有人用50万词汇量的One-hot,那训练起来简直要命。
  • 语义孤岛:任意两个词之间的余弦相似度都是0。也就是说,「猫」和「狗」在计算机眼里毫无关系——这显然不合理。
避坑指南:我曾经在一个文本分类项目里直接用One-hot喂给DNN,结果准确率死活上不去。后来才发现,模型根本学不到词与词之间的语义关系。所以,除非你的词汇量极小(比如<1000),否则别用One-hot做主力特征。

2.2 Word2Vec:让词学会「物以类聚」

Word2Vec的出现,可以说是NLP领域的一次「工业革命」。它的核心思想其实很简单:一个词的含义,由它周围的词决定。你想想看,如果「苹果」经常和「吃」、「水果」、「甜」一起出现,那它大概率是一种水果。如果它和「手机」、「系统」、「应用商店」一起出现,那它大概率是那个科技公司。

Word2Vec有两种训练模式:

模式 核心思路 典型场景
CBOW(连续词袋) 用上下文词预测当前词 小数据集、高频词效果好
Skip-gram 用当前词预测上下文词 大数据集、低频词效果好

我个人习惯在小数据集上用CBOW,因为训练速度快。但如果数据量够大,Skip-gram的效果往往更好——它能更好地捕捉到低频词的语义。

训练完成后,每个词就被映射到一个稠密向量(比如300维)。这时候,「猫」和「狗」的向量距离就很近了。更神奇的是,Word2Vec还能做类比推理:

vec("国王") - vec("男人") + vec("女人") ≈ vec("女王")

我第一次看到这个结果时,说实话挺震撼的。模型居然学会了「性别」这个抽象概念。

2.3 GloVe与FastText:站在巨人的肩膀上

Word2Vec虽然好,但它只利用了局部上下文信息。GloVe(Global Vectors)则更进一步,它同时考虑了全局的共现统计信息。

GloVe的核心思想是:词与词的共现概率比,能反映它们之间的语义关系。比如「冰」和「水」经常一起出现,但「冰」和「蒸汽」很少一起出现。这个比例关系,GloVe能很好地捕捉到。

而FastText,则是我在项目中经常用的一个工具。它解决了Word2Vec的一个痛点:未登录词问题。你想想看,如果测试集里出现了一个训练集没见过的词,Word2Vec就直接懵了。FastText的做法是:把词拆成字符级别的n-gram。

举个例子,「苹果」会被拆成:

"苹", "果", "苹果", "苹果"  (假设n=2和3)

这样,即使遇到一个新词「苹狗」,模型也能通过「苹」和「狗」的子词信息,猜出它大概的意思。

实战建议:如果你的任务涉及大量专业术语或拼写变体(比如电商商品名、医疗术语),优先考虑FastText。我曾经在一个商品分类项目里,用FastText比Word2Vec提升了5个点的准确率。

2.4 TensorFlow实现词嵌入层

好了,理论讲完了,咱们来点实际的。在TensorFlow里实现词嵌入,其实就一行代码的事:

import tensorflow as tf

# 定义一个词嵌入层
embedding_layer = tf.keras.layers.Embedding(
    input_dim=10000,   # 词汇表大小
    output_dim=128,    # 嵌入维度
    embeddings_initializer='uniform',  # 初始化方式
    input_length=50    # 输入序列长度
)

# 使用示例
input_sequence = tf.constant([[1, 2, 3, 4, 5]])  # 假设是词索引
output = embedding_layer(input_sequence)
print(output.shape)  # (1, 5, 128)

嗯,这里要注意几个关键参数:

  • input_dim:一定要比实际词汇量略大,留出padding和OOV(未登录词)的位置。
  • output_dim:一般取50-300之间。太小了语义信息不够,太大了容易过拟合。
  • embeddings_initializer:我习惯用'uniform',但如果你有预训练的词向量,可以用预训练权重初始化。

如果你想用预训练的词向量(比如GloVe或Word2Vec),可以这样加载:

import numpy as np

# 假设你已经有了一个词向量字典 word2vec
embedding_matrix = np.zeros((vocab_size, embedding_dim))
for word, i in word_index.items():
    if word in word2vec:
        embedding_matrix[i] = word2vec[word]

# 创建嵌入层并加载预训练权重
embedding_layer = tf.keras.layers.Embedding(
    input_dim=vocab_size,
    output_dim=embedding_dim,
    weights=[embedding_matrix],
    trainable=True  # 是否微调
)
核心要点:trainable=True表示在训练过程中会微调词向量。如果你的下游任务数据量很大,建议设为True。如果数据量小,设为False可以防止过拟合。

最后,我想分享一个我在项目中的经验。很多人以为词嵌入层只是把词变成向量就完事了,其实不然。词嵌入层的训练过程,本质上是在学习一个「语义空间」。在这个空间里,相似的词会聚在一起,不同的词会分开。所以,词嵌入层的质量,直接决定了你整个NLP模型的天花板

我曾经在一个情感分析项目里,一开始用随机初始化的词嵌入,准确率只有82%。后来换成了预训练的GloVe词向量,准确率直接跳到89%。你看,这就是词嵌入的力量。

下一章,咱们会讲RNN和LSTM,到时候你会看到,词嵌入层和循环神经网络结合起来,能做出多么强大的文本模型。