Fasttext - 哈夫曼树介绍
1 课程概览
本课讲解哈夫曼树(Huffman Tree),也叫赫夫曼树。哈夫曼树是层次 softmax 的底层结构,用于构建带权路径长度最小的二叉树。作用是让 Fasttext 在做文本分类时,计算超多类别分类更快速。类比折半查找(猜数字游戏),不用每个类别都计算,分层次缩小范围。
2 核心概念与定义
- 哈夫曼树(Huffman Tree):也叫赫夫曼树,带权路径长度(WPL)最小的二叉树。
- WPL(Weighted Path Length):带权路径长度,所有叶子节点的权值乘以所在层数之和。
- 叶子节点:没有子节点的节点。
- 根节点:树的顶部节点。
- 层数:根节点为第 0 层,往下递增。
3 模型与算法详解
哈夫曼树的作用
构建带权路径长度最小的二叉树,用于层次 softmax。
- 让 Fasttext 做文本分类时计算标签概率更快
- 计算超多类别分类时提高效率
- 不再使用传统 softmax
类比:折半查找(猜数字游戏)
猜 1~100 之间的数字,用折半查找,10 次以内出结果。
| 步骤 | 范围 | 猜测 | 结果 |
|---|---|---|---|
| 1 | 1~100 | 50 | 猜小了 |
| 2 | 51~100 | 75 | 猜大了 |
| 3 | 51~74 | 63 | 猜小了 |
| 4 | 64~74 | 68 | ... |
- 传统 softmax:每个类别都算,1000 个类别算 1000 次
- 层次 softmax:分层次算,快速出结果
WPL 计算
所有叶子节点的权值乘以所在层数之和。
$$\text{WPL} = \sum_{i=1}^{n} w_i \times l_i$$
WPL 计算示例
根
/ \
9 7 (第 1 层)
/ \
3 5 (第 2 层)
| 叶子节点 | 权值 | 层数 | 贡献 |
|---|---|---|---|
| 9 | 9 | 1 | 9×1=9 |
| 7 | 7 | 2 | 7×2=14 |
| 3 | 3 | 3 | 3×3=9 |
| 5 | 5 | 3 | 5×3=15 |
$$\text{WPL} = 9 \times 1 + 7 \times 2 + 3 \times 3 + 5 \times 3 = 9 + 14 + 9 + 15 = 47$$
哈夫曼树构建原则
权值大的节点离根近,权值小的节点离根远。
- 权值大:出现频率高,离根近,路径短
- 权值小:出现频率低,离根远,路径长
- 这样 WPL 最小
4 数学原理与推导
WPL 公式
$$\text{WPL} = \sum_{i=1}^{n} w_i \times l_i$$
其中:
- $w_i$ 是第 $i$ 个叶子节点的权值
- $l_i$ 是第 $i$ 个叶子节点所在的层数
- $n$ 是叶子节点的数量
层次 softmax 复杂度
| 方法 | 复杂度 | 说明 |
|---|---|---|
| 传统 softmax | $O(K)$ | $K$ 是类别数 |
| 层次 softmax | $O(\log K)$ | 使用哈夫曼树 |
5 代码示例
import heapq
class HuffmanNode:
"""哈夫曼树节点"""
def __init__(self, weight, char=None):
self.weight = weight # 权值
self.char = char # 字符(叶子节点)
self.left = None # 左子节点
self.right = None # 右子节点
def __lt__(self, other):
return self.weight < other.weight
def build_huffman_tree(char_weights):
"""
构建哈夫曼树
:param char_weights: 字符和权值的列表 [(char, weight), ...]
:return: 哈夫曼树的根节点
"""
# 1. 创建叶子节点
heap = [HuffmanNode(weight, char) for char, weight in char_weights]
heapq.heapify(heap)
# 2. 构建哈夫曼树
while len(heap) > 1:
# 取出权值最小的两个节点
left = heapq.heappop(heap)
right = heapq.heappop(heap)
# 创建新节点,权值为两子节点之和
merged = HuffmanNode(left.weight + right.weight)
merged.left = left
merged.right = right
heapq.heappush(heap, merged)
return heap[0]
def calculate_wpl(root, depth=0):
"""
计算带权路径长度 WPL
:param root: 哈夫曼树的根节点
:param depth: 当前深度
:return: WPL
"""
if root is None:
return 0
# 叶子节点
if root.left is None and root.right is None:
return root.weight * depth
# 递归计算左右子树
return (calculate_wpl(root.left, depth + 1) +
calculate_wpl(root.right, depth + 1))
# 测试
if __name__ == "__main__":
# 示例:字符和权值
char_weights = [('A', 5), ('B', 2), ('C', 3), ('D', 4)]
# 构建哈夫曼树
root = build_huffman_tree(char_weights)
# 计算 WPL
wpl = calculate_wpl(root)
print(f"带权路径长度 WPL: {wpl}")
代码说明
| 代码 | 说明 |
|---|---|
HuffmanNode | 哈夫曼树节点 |
heapq.heapify(heap) | 将列表转为最小堆 |
heapq.heappop(heap) | 取出权值最小的节点 |
heapq.heappush(heap, merged) | 将新节点加入堆 |
calculate_wpl(root, depth) | 计算 WPL |
6 重难点与易错提醒
- ❗重点:哈夫曼树是带权路径长度最小的二叉树。
- ❗重点:WPL = 所有叶子节点的权值 × 层数之和。
- ❗重点:权值大的节点离根近,权值小的节点离根远。
- ⚠️易错:层数从根节点开始算,根节点为第 0 层。
- 💡深入理解:层次 softmax 类比折半查找。
7 课堂问答精选
Q1:哈夫曼树的作用是什么?
A:构建带权路径长度最小的二叉树,用于层次 softmax,让 Fasttext 做文本分类时计算标签概率更快。
Q2:WPL 如何计算?
A:WPL = 所有叶子节点的权值 × 层数之和。例如:9×1 + 7×2 + 3×3 + 5×3 = 47。
Q3:哈夫曼树的构建原则是什么?
A:权值大的节点离根近,路径短;权值小的节点离根远,路径长。这样 WPL 最小。
Q4:层次 softmax 和传统 softmax 的复杂度对比?
A:传统 softmax 复杂度 $O(K)$,层次 softmax 复杂度 $O(\log K)$,使用哈夫曼树提高效率。
8 本课小结
- 哈夫曼树:带权路径长度最小的二叉树。
- WPL = 所有叶子节点的权值 × 层数之和。
- 构建原则:权值大离根近,权值小离根远。
- 层次 softmax 复杂度 $O(\log K)$,优于传统 softmax 的 $O(K)$。
9 延伸思考
- 负采样是如何工作的?
- Fasttext 如何使用哈夫曼树进行分类?