文章目录
一、词向量FastText概述
1.1 为什么需要FastText?
在介绍FastText之前,我们首先要理解它解决了什么问题。这需要从它的“前身”Word2Vec说起。
Word2Vec (Skip-gram & CBOW) 的局限性:
Word2Vec是NLP领域的里程碑式技术,它通过上下文来学习词语的向量表示。其核心思想是“一个词语的语义由其周围的词语决定”。
然而,Word2Vec有一个致命的弱点:它无法处理未登录词。
- 什么是未登录词? 指的是在模型训练时从未在语料库中出现过的词。
- 为什么会出现? 语言是动态发展的,新词、专有名词(人名、地名)、拼写错误等层出不穷。
- 后果是什么? 当一个新词出现时,Word2Vec无法为其生成向量,因为它在词表中没有“身份”。这对于需要处理海量、开放文本的机器翻译任务来说,是一个巨大的障碍。
FastText的诞生:
FastText由Facebook AI Research(FAIR)团队开发的一种用于高效学习词向量和文本分类的算法,它在 Word2Vec 基础上进行了改进,在2017年提出。它巧妙地解决了OOV问题,并且训练速度比Word2Vec更快。它的核心思想非常简单而强大:
一个词由其内部的字符N-gram组成。
例如,单词"apple"
可以被分解为它的字符级N-gram:
- Bi-grams (N=2):
<ap
,pp
,pl
,le
,e>
- Tri-grams (N=3):
<app
,ppl
,ple
,le>
FastText的核心理念是:一个词的向量,是其所有字符N-gram向量的平均值。
这样一来,即使遇到了一个从未见过的词,比如"applz"
,我们也可以将其分解为字符N-gram(如<ap
,pp
,pl
,lz
,z>
),然后在模型中查找这些N-gram的向量(这些N-gram很可能在训练时见过),最后取平均,就能得到"applz"
的一个合理的向量表示。
1.2 主要特点
FastText 是 Facebook AI Research 开主要特点包括:
- 子词信息处理:将单词分解为字符级别的 n-gram,解决未登录词问题
- 层次化 softmax:提高训练效率
- 支持文本分类:不仅可用于词向量表示,还可用于文本分类任务
1.3 FastText的优缺点与总结
1、优点
- 处理OOV词: 这是它最核心、最强大的优势。
- 能更好地处理形态丰富的语言: 对于有大量词缀变化的语言(如德语、俄语、土耳其语),FastText能更好地捕捉词根信息,效果显著优于Word2Vec。
- 训练速度快: 由于Hierarchical Softmax等优化技术,FastText的训练速度通常比Word2Vec更快。
- 在小数据集上表现更好: 由于字符N-gram提供了额外的信息,即使某些词在语料库中出现次数很少,其组成部分的N-gram也可能很常见,因此模型能更好地学习其表示。
2、缺点
- 模型更大: 除了词向量,还需要存储所有字符N-gram的向量,这会显著增加模型的内存占用。
- 对词序不敏感: 和Word2Vec一样,FastText的词向量是上下文无关的,它无法处理一词多义问题。例如,
"bank"
在“river bank”和“investment bank”中的含义是不同的,但FastText只会为它生成一个固定的向量。
二、FastText算法详解
FastText的算法架构与Word2Vec中的CBOW(Continuous Bag of Words)模型非常相似。我们以CBOW为例来解释。
2.1 模型结构
CBOW模型的任务是:根据一个词的上下文,预测这个词本身。
- 输入: 一个词的上下文词(例如,对于中心词
"learning"
,上下文可能是"deep", "is", "neural"
)。 - 输出: 中心词(
"learning"
)。
FastText的CBOW模型结构如下:
- 输入层: 将上下文中的每个词转换为其对应的词向量。
- 投影层: 将所有上下文词向量相加或取平均,得到一个单一的向量表示。这一步与CBOW完全相同。
- 隐藏层: 这一步是FastText的关键创新。它不直接将投影层的向量与输出层连接。相反,它将这个向量与模型中所有字符N-gram的向量相加或取平均。
- 输出层: 使用Softmax函数,从整个词汇表中预测出概率最高的那个词作为中心词。
2.2 核心思想:共享内部信息
让我们用一个例子来理解FastText为什么能处理OOV词。
假设我们的语料库中有 "apple"
和 "apples"
这两个词。
- 对于Word2Vec:
"apple"
和"apples"
是两个完全独立的词,它们的向量之间没有直接关系。 - 对于FastText:
"apple"
的向量是其字符N-gram(如<app
,ppl
,ple
…)向量的平均。"apples"
的向量是其字符N-gram(如<app
,ppl
,ple
,les
…)向量的平均。
你会发现,这两个词共享了大量的字符N-gram,比如<app>
,<ppl>
,<ple>
。因此,它们的词向量在向量空间中会非常接近。模型学习到了“词根”和“词缀”的语义信息。
现在,来了一个新词"apply"
。它包含了字符N-gram<app>
,ppl
,ply>...
。因为<app>
和<ppl>
是从"apple"
和"apples"
中学习到的,所以"apply"
的向量会自然地与它们靠近,模型能够理解它是一个与“苹果”相关的词,而不是一个完全陌生的词。
2.3 训练过程
FastText的训练过程与CBOW几乎一样,使用负采样 或 Hierarchical Softmax 来优化计算效率,避免对整个庞大的词汇表进行Softmax计算。
- 构建词和N-gram的词典: 遍历整个语料库,收集所有唯一的词和所有唯一的字符N-gram,并为它们分配一个唯一的ID。
- 初始化向量: 为每个词和每个字符N-gram随机初始化一个向量。
- 滑动窗口: 在语料库上滑动一个固定大小的窗口。
- 前向传播: 对于每个窗口,根据上述模型结构计算输出词的概率。
- 反向传播与更新: 根据预测概率与真实标签之间的误差,使用梯度下降法(如SGD)更新模型中所有涉及的向量——包括上下文词的向量、中心词的向量,以及所有相关的字符N-gram的向量。
三、详细的Python实现(基于gensim)
我们将使用 gensim
库,因为它提供了非常高效且易用的FastText实现。
3.1 环境准备
首先,确保你已经安装了 gensim
和 nltk
(用于文本预处理)。
pip install gensim nltk
3.2 完整代码实现
下面是一个完整的流程,包括数据准备、模型训练、模型使用和可视化。
import gensim
import nltk
from nltk.tokenize import word_tokenize
from nltk.corpus import stopwords
import string
# 下载必要的NLTK数据
nltk.download('punkt')
nltk.download('stopwords')
# --- 步骤1: 数据准备 ---
# 我们使用一个小的示例语料库。在实际应用中,你需要一个巨大的文本文件。
sentences = [
["deep", "learning", "is", "fun", "and", "powerful"],
["natural", "language", "processing", "is", "a", "subfield", "of", "ai"],
["word", "embeddings", "like", "word2vec", "and", "fasttext", "are", "essential"],
["fasttext", "is", "an", "extension", "of", "word2vec"],
["it", "can", "handle", "out-of-vocabulary", "words", "effectively"],
"apple and apples are similar words".split(),
"apply and application share common roots".split(),
"this is a new word oov_example".split() # 我们将用这个来测试OOV
]
# 对数据进行简单的预处理:转换为小写,去除停用词和标点
stop_words = set(stopwords.words('english'))
def preprocess(sentence):
return [w.lower() for w in sentence if w.isalpha() and w not in stop_words]
processed_sentences = [preprocess(sent) for sent in sentences]
# --- 步骤2: 训练FastText模型 ---
# 参数说明:
# vector_size: 词向量的维度
# window: 上下文窗口大小
# min_count: 忽略总频率低于此值的词
# workers: 并行训练使用的线程数
# sg: 0 代表 CBOW, 1 代表 Skip-gram
# min_n: 最小的字符N-gram长度
# max_n: 最大的字符N-gram长度
# 注意: 当min_n > max_n时,不使用字符N-gram,模型退化为标准的Word2Vec CBOW
print("开始训练FastText模型...")
model = gensim.models.FastText(
sentences=processed_sentences,
vector_size=100,
window=5,
min_count=1,
workers=4,
sg=0, # 使用CBOW模式
min_n=3,
max_n=5
)
print("模型训练完成!")
# --- 步骤3: 使用模型 ---
# 3.1 查找词向量
print("\n查找 'learning' 的向量:")
vector_learning = model.wv['learning']
print(f"向量维度: {len(vector_learning)}")
# print(vector_learning)
# 3.2 查找相似词
print("\n与 'learning' 最相似的词:")
similar_words = model.wv.most_similar('learning', topn=5)
for word, score in similar_words:
print(f"{word}: {score:.4f}")
# 3.3 核心功能:查找OOV词的向量
print("\n--- 测试未登录词 ---")
oov_word = "oov_example"
print(f"查找 '{oov_word}' 的向量:")
try:
oov_vector = model.wv[oov_word]
print(f"成功!'{oov_word}' 的向量维度: {len(oov_vector)}")
print(f"向量前10维: {oov_vector[:10]}")
# 测试OOV词的相似词
print(f"\n与 '{oov_word}' 最相似的词:")
oov_similar_words = model.wv.most_similar(oov_word, topn=3)
for word, score in oov_similar_words:
print(f"{word}: {score:.4f}")
except KeyError:
print(f"错误:'{oov_word}' 不在词汇表中!")
# 对比一下,如果不用字符N-gram会发生什么
print("\n--- 对比:不使用字符N-gram的Word2Vec模型 ---")
word2vec_model = gensim.models.Word2Vec(
sentences=processed_sentences,
vector_size=100,
window=5,
min_count=1,
workers=4,
sg=0
)
try:
word2vec_oov_vector = word2vec_model.wv[oov_word]
print(f"Word2Vec 也找到了 '{oov_word}' 的向量?")
except KeyError:
print(f"Word2Vec 错误:'{oov_word}' 不在词汇表中!这是预期的结果。")
# --- 步骤4: 词向量可视化 (可选) ---
# 为了可视化,我们需要将高维向量降到2维。这里我们使用PCA。
# 由于我们的数据集太小,可视化效果可能不明显,但代码结构是通用的。
import numpy as np
from sklearn.decomposition import PCA
import matplotlib.pyplot as plt
# 选择一些词进行可视化
words_to_visualize = ['learning', 'powerful', 'language', 'processing', 'fasttext', 'word', 'oov_example']
word_vectors = [model.wv[word] for word in words_to_visualize]
# 使用PCA降维
pca = PCA(n_components=2)
result = pca.fit_transform(word_vectors)
# 绘制散点图
plt.figure(figsize=(10, 8))
plt.scatter(result[:, 0], result[:, 1])
for i, word in enumerate(words_to_visualize):
plt.annotate(word, xy=(result[i, 0], result[i, 1]))
plt.title("FastText Word Vector Visualization (PCA)")
plt.xlabel("PCA Dimension 1")
plt.ylabel("PCA Dimension 2")
plt.grid(True)
plt.show()
3.3 代码解读与分析
- 数据预处理: 我们将文本分词,并转换为小写,去除了停用词和标点符号。这是NLP任务的标准预处理步骤。
- 模型训练:
gensim.models.FastText(...)
是核心。vector_size=100
:我们生成100维的词向量。min_n=3, max_n=5
:这是FastText的关键。它告诉模型使用长度从3到5的字符N-gram。例如,"learning"
会被分解为'<lea'
,'ear'
,'arn'
,'rni'
,'nin'
,'ing'
,'ng>'
等N-gram。sg=0
:我们选择了CBOW架构。你也可以设置为1
来使用Skip-gram。
- 使用模型:
model.wv['word']
:获取词向量。model.wv.most_similar('word')
:查找最相似的词。- OOV测试: 我们特意创建了一个新词
"oov_example"
。运行代码你会发现,FastText成功地为它生成了向量,并且找到了与它相似的词(这些词可能共享了'ex'
,'amp'
,'ple'
等N-gram)。而对比的Word2Vec模型则直接报错,完美地展示了FastText的优势。
- 可视化: 我们使用PCA将100维的向量压缩到2维,并用散点图画出来。你可以直观地看到,语义相近的词(如
'learning'
和'powerful'
,或者'fasttext'
和'word'
)在空间中的位置也更接近。即使对于OOV词'oov_example'
,它也会被放置在与其共享N-gram的词附近。
四、基础 FastText 词向量实现
4.1 完整代码
import numpy as np
from collections import Counter, defaultdict
import random
from typing import List, Tuple, Dict, Set
import re
class FastText:
def __init__(self, vector_dim=100, window_size=5, min_count=1,
n_gram=3, learning_rate=0.025, negative=5):
"""
初始化 FastText 模型
Args:
vector_dim: 词向量维度
window_size: 上下文窗口大小
min_count: 最小词频阈值
n_gram: n-gram 长度
learning_rate: 学习率
negative: 负采样数量
"""
self.vector_dim = vector_dim
self.window_size = window_size
self.min_count = min_count
self.n_gram = n_gram
self.learning_rate = learning_rate
self.negative = negative
# 词汇表和 n-gram 表
self.word_to_idx = {}
self.idx_to_word = {}
self.ngram_to_idx = {}
self.idx_to_ngram = {}
# 模型参数
self.word_vectors = None
self.context_vectors = None
self.ngram_vectors = None
# 词频统计
self.word_freq = {}
self.ngram_freq = {}
def _get_ngrams(self, word: str) -> List[str]:
"""
获取单词的所有 n-gram
Args:
word: 输入单词
Returns:
n-gram 列表
"""
# 添加边界标记
word = '<' + word + '>'
ngrams = []
# 生成 n-gram
for i in range(len(word) - self.n_gram + 1):
ngrams.append(word[i:i + self.n_gram])
return ngrams
def _build_vocab(self, sentences: List[List[str]]):
"""
构建词汇表和 n-gram 表
Args:
sentences: 句子列表
"""
# 统计词频
word_counter = Counter()
ngram_counter = Counter()
for sentence in sentences:
for word in sentence:
word_counter[word] += 1
# 统计单词的 n-gram
for ngram in self._get_ngrams(word):
ngram_counter[ngram] += 1
# 过滤低频词
self.word_freq = {word: freq for word, freq in word_counter.items()
if freq >= self.min_count}
self.ngram_freq = {ngram: freq for ngram, freq in ngram_counter.items()
if freq >= self.min_count}
# 构建词汇表映射
self.word_to_idx = {word: idx for idx, word in enumerate(self.word_freq.keys())}
self.idx_to_word = {idx: word for word, idx in self.word_to_idx.items()}
# 构建 n-gram 映射
self.ngram_to_idx = {ngram: idx for idx, ngram in enumerate(self.ngram_freq.keys())}
self.idx_to_ngram = {idx: ngram for ngram, idx in self.ngram_to_idx.items()}
# 初始化向量矩阵
vocab_size = len(self.word_to_idx)
ngram_size = len(self.ngram_to_idx)
# 输入向量(中心词)和输出向量(上下文词)
self.word_vectors = np.random.uniform(-0.1, 0.1, (vocab_size, self.vector_dim))
self.context_vectors = np.random.uniform(-0.1, 0.1, (vocab_size, self.vector_dim))
self.ngram_vectors = np.random.uniform(-0.1, 0.1, (ngram_size, self.vector_dim))
def _get_word_vector(self, word: str) -> np.ndarray:
"""
获取单词的向量表示(基于其 n-gram 向量的平均)
Args:
word: 单词
Returns:
单词向量
"""
if word not in self.word_to_idx:
# 对于未登录词,使用其 n-gram 向量
ngrams = self._get_ngrams(word)
ngram_vectors = []
for ngram in ngrams:
if ngram in self.ngram_to_idx:
ngram_idx = self.ngram_to_idx[ngram]
ngram_vectors.append(self.ngram_vectors[ngram_idx])
if ngram_vectors:
return np.mean(ngram_vectors, axis=0)
else:
# 如果没有已知的 n-gram,返回零向量
return np.zeros(self.vector_dim)
else:
# 对于已知词,同样使用 n-gram 向量
ngrams = self._get_ngrams(word)
ngram_vectors = []
for ngram in ngrams:
if ngram in self.ngram_to_idx:
ngram_idx = self.ngram_to_idx[ngram]
ngram_vectors.append(self.ngram_vectors[ngram_idx])
if ngram_vectors:
return np.mean(ngram_vectors, axis=0)
else:
return np.zeros(self.vector_dim)
def _sigmoid(self, x: float) -> float:
"""
Sigmoid 函数
Args:
x: 输入值
Returns:
sigmoid(x)
"""
if x > 10:
return 1.0
elif x < -10:
return 0.0
else:
return 1.0 / (1.0 + np.exp(-x))
def _negative_sampling(self, target_idx: int, vocab_size: int) -> List[int]:
"""
负采样
Args:
target_idx: 目标词索引
vocab_size: 词汇表大小
Returns:
负样本索引列表
"""
# 简单的负采样实现(可以进一步优化)
neg_samples = []
while len(neg_samples) < self.negative:
idx = random.randint(0, vocab_size - 1)
if idx != target_idx:
neg_samples.append(idx)
return neg_samples
def train(self, sentences: List[List[str]], epochs: int = 5):
"""
训练 FastText 模型
Args:
sentences: 句子列表
epochs: 训练轮数
"""
# 构建词汇表
self._build_vocab(sentences)
vocab_size = len(self.word_to_idx)
# 训练过程
for epoch in range(epochs):
loss = 0.0
count = 0
for sentence in sentences:
# 获取句子中词的索引
word_indices = [self.word_to_idx[word] for word in sentence
if word in self.word_to_idx]
for (position, word_idx) in enumerate(word_indices):
# 获取上下文窗口
start = max(0, position - self.window_size)
end = min(len(word_indices), position + self.window_size + 1)
for context_pos in range(start, end):
if context_pos == position:
continue
context_idx = word_indices[context_pos]
# 获取中心词向量(基于 n-gram)
center_word = self.idx_to_word[word_idx]
center_vector = self._get_word_vector(center_word)
# 正样本更新
context_vector = self.context_vectors[context_idx]
score = np.dot(center_vector, context_vector)
prob = self._sigmoid(score)
# 更新梯度
grad = (1 - prob)
self.context_vectors[context_idx] += self.learning_rate * grad * center_vector
# 更新 n-gram 向量
ngrams = self._get_ngrams(center_word)
for ngram in ngrams:
if ngram in self.ngram_to_idx:
ngram_idx = self.ngram_to_idx[ngram]
self.ngram_vectors[ngram_idx] += self.learning_rate * grad * context_vector
loss -= np.log(prob + 1e-10)
# 负采样更新
neg_samples = self._negative_sampling(context_idx, vocab_size)
for neg_idx in neg_samples:
neg_vector = self.context_vectors[neg_idx]
score = np.dot(center_vector, neg_vector)
prob = self._sigmoid(-score)
# 更新梯度
grad = (1 - prob)
self.context_vectors[neg_idx] -= self.learning_rate * grad * center_vector
# 更新 n-gram 向量
for ngram in ngrams:
if ngram in self.ngram_to_idx:
ngram_idx = self.ngram_to_idx[ngram]
self.ngram_vectors[ngram_idx] -= self.learning_rate * grad * neg_vector
loss -= np.log(prob + 1e-10)
count += 1
if count > 0:
print(f"Epoch {epoch+1}/{epochs}, Loss: {loss/count:.4f}")
def get_word_vector(self, word: str) -> np.ndarray:
"""
获取单词向量
Args:
word: 单词
Returns:
单词向量
"""
return self._get_word_vector(word)
def most_similar(self, word: str, topn: int = 10) -> List[Tuple[str, float]]:
"""
查找最相似的词
Args:
word: 目标词
topn: 返回最相似词的数量
Returns:
(词, 相似度) 元组列表
"""
if word not in self.word_to_idx and word not in [w for sublist in [self._get_ngrams(w) for w in self.word_freq.keys()] for w in sublist]:
raise ValueError(f"Word '{word}' not in vocabulary")
word_vector = self._get_word_vector(word)
norm_word_vector = word_vector / (np.linalg.norm(word_vector) + 1e-10)
similarities = []
for other_word in self.word_freq.keys():
if other_word == word:
continue
other_vector = self._get_word_vector(other_word)
norm_other_vector = other_vector / (np.linalg.norm(other_vector) + 1e-10)
similarity = np.dot(norm_word_vector, norm_other_vector)
similarities.append((other_word, similarity))
# 按相似度排序
similarities.sort(key=lambda x: x[1], reverse=True)
return similarities[:topn]
# 使用示例
if __name__ == "__main__":
# 示例文本数据
sentences = [
["the", "quick", "brown", "fox", "jumps", "over", "the", "lazy", "dog"],
["the", "quick", "brown", "cat", "jumps", "over", "the", "lazy", "dog"],
["a", "quick", "brown", "fox", "runs", "fast"],
["the", "lazy", "dog", "sleeps", "all", "day"],
["cats", "and", "dogs", "are", "pets"]
]
# 创建并训练模型
model = FastText(vector_dim=50, window_size=2, min_count=1, n_gram=3)
model.train(sentences, epochs=10)
# 获取词向量
word_vec = model.get_word_vector("quick")
print(f"Vector for 'quick': {word_vec[:5]}...") # 只显示前5个元素
# 查找相似词
similar_words = model.most_similar("quick", topn=3)
print(f"Words similar to 'quick': {similar_words}")
4.2 执行结果
Epoch 1/10, Loss: 4.1585
Epoch 2/10, Loss: 4.1390
Epoch 3/10, Loss: 4.1153
Epoch 4/10, Loss: 4.0734
Epoch 5/10, Loss: 3.9923
Epoch 6/10, Loss: 3.8603
Epoch 7/10, Loss: 3.6481
Epoch 8/10, Loss: 3.3709
Epoch 9/10, Loss: 3.1050
Epoch 10/10, Loss: 2.8841
Vector for 'quick': [ 0.63750407 -0.03965305 0.23450264 -0.16499844 -0.27144903]...
Words similar to 'quick': [('jumps', np.float64(0.9475779011834671)), ('lazy', np.float64(0.9371813260498959)), ('sleeps', np.float64(0.9366864225793249))]
五、总结
FastText通过引入字符级的N-gram,巧妙地解决了Word2Vec无法处理未登录词的痛点,并且在处理形态复杂的语言时表现出色。它不仅是一个词向量工具,其思想也对后来的模型(如Subword Regularized Neural Machine Translation)产生了深远影响。
在机器翻译任务中,FastText通常用作:
- 源语言和目标语言的词嵌入层: 为输入的源语言句子和输出的目标语言句子提供初始的、语义丰富的向量表示。
- 数据增强: 在训练数据中引入一些拼写错误或变体,利用FastText的鲁棒性来提升模型的泛化能力。
对于任何希望入门NLP或进行机器翻译实践的人来说,深入理解并掌握FastText的原理和使用都是一项非常有价值的技能。