最近 AI 行业最热闹的话题之一,就是 Meta 时隔约 16 个月重新回到开源模型赛道的中心位置。与此同时,“知识蒸馏”这个词也被推到了台前,扎克伯格公开表达了对蒸馏技术的认可。很多人看到新闻标题会冒出两个疑问:第一,Meta 开源这件事对开发者到底意味着什么;第二,知识蒸馏到底是什么,为什么它能让小模型越来越强。
本文不打算只做热点解读,而是把这两件事放在一起,拆成一条完整的学习主线。我会先梳理 Meta 开源战略的背景,再系统讲解知识蒸馏的核心原理,最后用 PyTorch 完整实现一个“教师模型指导学生模型”的实战案例。即使你之前没有接触过模型蒸馏,跟着本文一步步操作,也能跑通一个可用的小型蒸馏项目。
1. 背景与核心概念
1.1 Meta 开源战略的回归
Meta 旗下的 Llama 系列开源模型在开发者社区有非常高的知名度。从第一代 Llama 开源起,大量研究者和工程师基于它做微调、量化、部署,形成了繁荣的上下游生态。不过开源热情并不是一直保持同一热度,中间经历了一段相对沉寂的时期,社区里甚至有不少人担心 Meta 会收紧开源策略。
所以当“16 个月后 Meta 杀回开源”这个信息出现时,行业的反应才会这么大。与其说这是某一家公司的动作,不如说它释放了一个信号:开源依然是当前大模型技术扩散的重要路径,闭源模型所不具备的可定制、可审计、可私有化部署优势,依然是大量企业刚需。
当然,需要提醒的是,大模型迭代速度非常快,具体版本号、发布时间、许可协议这些信息,以 Meta 官方和开源仓库为准。这里我们重点讨论的是“开源 + 蒸馏”这一套技术组合为什么值得开发者关注。
1.2 Llama 系列与开源生态
如果你对 Llama 还不够熟悉,可以用一句话概括:Llama 是 Meta 发布的一系列大语言模型,以相对开放的权重和较好的性能,成为开源大模型领域的重要参考系。
开源模型的价值不只是“可以免费下载权重”,更在于它衍生出的完整技术生态:
- 模型权重开放,研究者可以复现论文结论。
- 开发者可以基于开源模型做领域微调,生成自己的垂直模型。
- 社区可以围绕模型开发推理框架、量化工具、部署方案和应用层产品。
- 企业可以在私有化环境内部署开源模型,解决数据出域和安全合规问题。
正是因为这个生态的存在,“开源模型”在搜索词里才会始终维持高热度。与闭源模型相比,开源模型更适合做技术沉淀和二次开发,而知识蒸馏恰恰是二次开发中非常重要的一环。
1.3 知识蒸馏是什么
知识蒸馏(Knowledge Distillation)是一种模型压缩与知识迁移技术。它的核心思想是:用一个已经训练好的大模型(教师模型)来指导一个小模型(学生模型)的训练,让学生模型去模仿教师模型的输出行为,从而在参数量远小于教师模型的情况下,逼近教师模型的性能。
为什么要这么做?因为在实际工程中,大模型虽然效果更好,但推理成本高、延迟大,很难直接部署到资源受限的环境。比如把一个大语言模型放进手机端,或者部署到高并发在线服务,都会遇到显存、带宽和响应时间的压力。知识蒸馏提供了一条通路:把大模型学到的“知识”提炼到小模型里,让小模型在部署层面更轻量,同时尽量保留精度。
打个比方,大模型像一位经验丰富的老师,小模型像一名学生。老师不只是告诉学生“正确答案是什么”,还会把自己做题时的判断倾向、模糊边界的处理方式一并传递给学生。这种“软知识”往往比单纯学习硬标签效果更好,这也是蒸馏技术名称的由来。
1.4 为什么“蒸馏”成为关键词
大模型时代,蒸馏的热度持续上升,原因可以归结为三点:
第一,模型越来越大,直接部署成本过高。如果没有蒸馏、量化、剪枝这类技术,大模型很难真正进入业务场景。
第二,开源模型让蒸馏变得更容易落地。你可以合法拿到开源大模型的权重,在本地或自己的服务器上构造蒸馏数据,而不必依赖不透明的云端接口做数据搬运。
第三,蒸馏可以解决“大模型能力”与“小模型效率”之间的矛盾。小模型如果只靠自身的参数量学不到复杂能力,通过蒸馏就有可能把大模型的知识压缩到小模型结构里。
简单来说,AI 应用要想大规模落地,不能只依赖超大模型,必须靠一整套“降本增效”的技术方案。知识蒸馏就是这套方案里最核心的技术之一。
2. 环境准备与版本说明
2.1 实验环境
本文的实战案例基于 Python 和 PyTorch,适合在本地电脑或云服务器上运行。为了避免版本问题,先说明环境要求:
Python 3.9 及以上 PyTorch 2.0 及以上 torchvision 0.15 及以上 CUDA 可选(没有 GPU 也可以运行,只是训练会慢一些)如果还没有安装 PyTorch,可以使用 pip 安装 CPU 版本:
pip install torch torchvision如果机器有 NVIDIA GPU,并且已经配置好 CUDA 环境,推荐安装对应版本的 CUDA 版 PyTorch。具体安装命令可以参考 PyTorch 官网的安装向导。
需要说明的是,深度学习库更新很快,本文代码基于常见稳定版本编写。如果你使用的版本更高,大概率也能直接运行;如果个别 API 有变化,以官方文档为准。
2.2 示例项目结构
为了让代码更好维护,建议先创建项目目录:
distill_demo/ ├── main.py # 完整训练与蒸馏脚本 └── README.md # 项目说明(可选)本文的核心代码都放在main.py中。如果你有更好的工程习惯,也可以把模型定义、数据集加载、训练逻辑拆成不同模块,这里出于教程简化考虑,先使用单文件形式。
3. 知识蒸馏的核心原理拆解
3.1 教师模型与学生模型
知识蒸馏涉及两个模型:
- 教师模型:通常是参数量较大的模型,已经训练好或者至少经过充分训练。它提供知识来源。
- 学生模型:参数量较小的模型,结构更轻量,是真正要被部署的对象。
在训练阶段,学生模型有两个学习目标:一是学习真实的标签(硬标签),二是模仿教师模型的输出分布(软标签)。硬标签让学生模型不偏离真实任务,软标签则让学生模型学习到类别之间的相似性信息。
例如图像分类任务中,一张图片是猫的图片,硬标签是“猫”。教师模型在输出时,可能对“猫”给出 0.85 的概率,对“狗”给出 0.09 的概率,对“老虎”给出 0.04 的概率。这个概率分布里蕴含着“猫和狗比较像,猫和汽车不像”的语义关系。学生模型如果只学硬标签,学不到这种关系;但如果同时模仿教师模型的概率分布,就能学到更丰富的知识。
3.2 蒸馏损失函数
蒸馏训练通常包含两个损失项:
总损失 = α * 硬标签损失 + β * 蒸馏损失其中:
- 硬标签损失用交叉熵计算,衡量学生模型预测与真实标签之间的差异。
- 蒸馏损失用 KL 散度计算,衡量学生模型输出分布与教师模型输出分布之间的差异。
- α 和 β 是两个权重系数,用来控制两个损失的相对重要性。
一个经典的蒸馏损失公式是 Hinton 在蒸馏论文中提出的,训练时需要使用“温度参数 T”对 logits 进行处理。核心代码如下:
import torch import torch.nn.functional as F def distillation_loss(student_logits, teacher_logits, labels, T=3.0, alpha=0.7): """ student_logits: 学生模型输出的原始 logits teacher_logits: 教师模型输出的原始 logits labels: 真实标签 T: 温度参数 alpha: 硬标签损失的权重 """ hard_loss = F.cross_entropy(student_logits, labels) soft_student = F.log_softmax(student_logits / T, dim=-1) soft_teacher = F.softmax(teacher_logits / T, dim=-1) distill_loss = F.kl_div(soft_student, soft_teacher, reduction='batchmean') * (T * T) return alpha * hard_loss + (1 - alpha) * distill_loss代码中(T * T)是一个缩放系数。因为软标签经过温度缩放后,梯度量级会变小,乘上T^2可以保持平衡。
3.3 温度参数的作用
温度参数 T 是蒸馏中最直观也最需要理解的概念。
- 当 T 等于 1 时,相当于直接使用原始的 softmax 输出。
- 当 T 大于 1 时,概率分布变得更平滑,类别之间的差异被“放大”,教师模型对相似类别的偏好更容易传递给学生模型。
- 当 T 小于 1 时,概率分布变得更尖锐,更接近 one-hot 编码,损失了软标签的多样性信息。
实际使用中,T 通常设置在 2 到 8 之间。温度过高会让分布太平滑,引入过多噪声;温度过低又会让蒸馏退化为普通标签学习。这个参数需要结合任务做实验调整,没有固定最优值。
3.4 常见误区
误区一:蒸馏只适合大语言模型。实际上,知识蒸馏在计算机视觉、语音识别、推荐系统、目标检测等方向都有广泛应用。本文用图像分类来做示例,原理在所有领域是相通的。
误区二:学生模型越接近教师模型越好。学生模型的参数量有限,强行模仿教师模型在困难样本上的输出,反而可能影响泛化能力。蒸馏的期望是“在相同参数量下效果更好”,而不是“小模型能完全超越大模型”。
误区三:蒸馏过程不需要真实标签。真实标签仍然是重要监督信号。完全脱离硬标签的蒸馏可能让学生模型继承教师模型的错误;而加入硬标签后,学生模型有机会在关键类别上纠正偏差。
误区四:蒸馏只在训练阶段使用。蒸馏得到的知识和能力固化在学生模型权重中,推理阶段不再需要教师模型,这也是蒸馏能降低部署成本的根本原因。
4. 完整实战案例
接下来我们从零实现一个图像分类知识蒸馏案例,数据集使用 MNIST,任务是把 0 到 9 的手写数字分类。教师模型使用一个较大的卷积神经网络,学生模型使用一个更小的网络,通过蒸馏训练让小模型逼近大模型效果。
4.1 创建项目结构
首先创建项目目录:
mkdir distill_demo cd distill_demo touch main.py然后在main.py中写入完整代码。我们分步骤解释每个部分。
4.2 导入依赖与配置参数
import torch import torch.nn as nn import torch.optim as optim import torch.nn.functional as F from torch.utils.data import DataLoader from torchvision import datasets, transforms # 设备配置:有 GPU 用 GPU,没有 GPU 用 CPU device = torch.device("cuda" if torch.cuda.is_available() else "cpu") # 超参数配置 BATCH_SIZE = 128 EPOCHS = 5 LEARNING_RATE = 1e-3 TEMPERATURE = 4.0 ALPHA = 0.7 # 硬标签损失权重 DISTILL_WEIGHT = 0.3 # 蒸馏损失权重这里把蒸馏相关的超参数单独提出,便于后续调参。温度设为4.0,是图像分类蒸馏任务中比较常见的取值。
4.3 加载 MNIST 数据集
transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) train_dataset = datasets.MNIST( root="./data", train=True, download=True, transform=transform ) test_dataset = datasets.MNIST( root="./data", train=False, download=True, transform=transform ) train_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True) test_loader = DataLoader(test_dataset, batch_size=BATCH_SIZE, shuffle=False)MNIST 是入门友好的公开数据集,download=True会在第一次运行时自动下载数据。如果网络环境受限,可以手动下载后放到./data目录。
4.4 定义教师模型与学生模型
教师模型使用一个相对复杂的卷积网络:
class TeacherNet(nn.Module): def __init__(self): super(TeacherNet, self).__init__() self.conv1 = nn.Conv2d(1, 32, kernel_size=3, padding=1) self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1) self.conv3 = nn.Conv2d(64, 128, kernel_size=3, padding=1) self.pool = nn.MaxPool2d(2, 2) self.fc1 = nn.Linear(128 * 3 * 3, 256) self.fc2 = nn.Linear(256, 10) self.dropout = nn.Dropout(0.3) def forward(self, x): x = self.pool(F.relu(self.conv1(x))) x = self.pool(F.relu(self.conv2(x))) x = self.pool(F.relu(self.conv3(x))) x = x.view(x.size(0), -1) x = F.relu(self.fc1(x)) x = self.dropout(x) return self.fc2(x)学生模型则尽量精简:
class StudentNet(nn.Module): def __init__(self): super(StudentNet, self).__init__() self.conv1 = nn.Conv2d(1, 16, kernel_size=3, padding=1) self.conv2 = nn.Conv2d(16, 32, kernel_size=3, padding=1) self.pool = nn.MaxPool2d(2, 2) self.fc1 = nn.Linear(32 * 7 * 7, 64) self.fc2 = nn.Linear(64, 10) def forward(self, x): x = self.pool(F.relu(self.conv1(x))) x = self.pool(F.relu(self.conv2(x))) x = x.view(x.size(0), -1) x = F.relu(self.fc1(x)) return self.fc2(x)对比两个模型可以看出,教师模型的卷积通道更多、层数更深、全连接层更宽,参数量明显大于学生模型。这正是“大而强”与“小而快”的典型对比。
4.5 训练教师模型
蒸馏之前,必须先有一个训练好的教师模型。我们先普通训练几个 epoch:
def train_teacher(): model = TeacherNet().to(device) optimizer = optim.Adam(model.parameters(), lr=LEARNING_RATE) model.train() for epoch in range(EPOCHS): total_loss = 0 correct = 0 total = 0 for images, labels in train_loader: images, labels = images.to(device), labels.to(device) optimizer.zero_grad() outputs = model(images) loss = F.cross_entropy(outputs, labels) loss.backward() optimizer.step() total_loss += loss.item() correct += (outputs.argmax(1) == labels).sum().item() total += labels.size(0) print(f"Teacher Epoch {epoch+1}/{EPOCHS}, Loss: {total_loss/len(train_loader):.4f}, Acc: {correct/total:.4f}") return model这里只训练 5 个 epoch,演示目的已经足够。实际项目需要更多轮次和更充分的数据增强。
4.6 蒸馏训练学生模型
蒸馏训练是本文核心。每次训练迭代中,同时计算硬标签损失和软标签蒸馏损失:
def train_student_with_distillation(teacher_model): teacher_model.eval() student_model = StudentNet().to(device) optimizer = optim.Adam(student_model.parameters(), lr=LEARNING_RATE) student_model.train() for epoch in range(EPOCHS): total_loss = 0 correct = 0 total = 0 for images, labels in train_loader: images, labels = images.to(device), labels.to(device) optimizer.zero_grad() student_logits = student_model(images) with torch.no_grad(): teacher_logits = teacher_model(images) # 硬标签交叉熵损失 hard_loss = F.cross_entropy(student_logits, labels) # 软标签蒸馏损失 soft_student = F.log_softmax(student_logits / TEMPERATURE, dim=-1) soft_teacher = F.softmax(teacher_logits / TEMPERATURE, dim=-1) distill_loss = F.kl_div( soft_student, soft_teacher, reduction='batchmean' ) * (TEMPERATURE * TEMPERATURE) loss = ALPHA * hard_loss + DISTILL_WEIGHT * distill_loss loss.backward() optimizer.step() total_loss += loss.item() correct += (student_logits.argmax(1) == labels).sum().item() total += labels.size(0) print(f"Student Epoch {epoch+1}/{EPOCHS}, Loss: {total_loss/len(train_loader):.4f}, Acc: {correct/total:.4f}") return student_model关键点在于with torch.no_grad()包裹教师模型的前向过程。教师模型不需要更新梯度,这样可以节省大量显存和计算资源。
4.7 模型评估
最后编写统一的评估函数,对比教师模型、蒸馏学生模型、普通训练学生模型三者的测试准确率:
def evaluate(model, data_loader): model.eval() correct = 0 total = 0 with torch.no_grad(): for images, labels in data_loader: images, labels = images.to(device), labels.to(device) outputs = model(images) correct += (outputs.argmax(1) == labels).sum().item() total += labels.size(0) return correct / total def train_student_without_distillation(): """训练一个不使用蒸馏的学生模型作为对照组""" model = StudentNet().to(device) optimizer = optim.Adam(model.parameters(), lr=LEARNING_RATE) model.train() for epoch in range(EPOCHS): for images, labels in train_loader: images, labels = images.to(device), labels.to(device) optimizer.zero_grad() outputs = model(images) loss = F.cross_entropy(outputs, labels) loss.backward() optimizer.step() return model4.8 主流程运行
在main.py末尾加入主流程:
if __name__ == "__main__": print("Step 1: Training teacher model...") teacher = train_teacher() teacher_acc = evaluate(teacher, test_loader) print(f"Teacher test accuracy: {teacher_acc:.4f}") print("Step 2: Training student model with distillation...") distill_student = train_student_with_distillation(teacher) distill_acc = evaluate(distill_student, test_loader) print(f"Distilled student test accuracy: {distill_acc:.4f}") print("Step 3: Training student model without distillation...") normal_student = train_student_without_distillation() normal_acc = evaluate(normal_student, test_loader) print(f"Normal student test accuracy: {normal_acc:.4f}") print("\nFinal Results:") print(f"Teacher : {teacher_acc:.4f}") print(f"Distilled Student : {distill_acc:.4f}") print(f"Normal Student : {normal_acc:.4f}")执行训练命令:
python main.py预期输出大致如下:
Step 1: Training teacher model... Teacher Epoch 1/5, Loss: 0.2134, Acc: 0.9345 ... Teacher test accuracy: 0.9890 Step 2: Training student model with distillation... ... Distilled student test accuracy: 0.9860 Step 3: Training student model without distillation... ... Normal student test accuracy: 0.9750由于随机种子和训练时长不同,每个人的输出会有差异,但整体趋势应该是:教师模型准确率最高,蒸馏学生模型次之,普通学生模型最低。这个结果说明蒸馏确实提升了小模型的表现。
4.9 结果说明
从实验结果可以看到,学生模型参数量远小于教师模型,但经过蒸馏后,准确率能接近教师模型,且明显高于不蒸馏的学生模型。这就是蒸馏技术在工程中的价值:以更小的算力开销换取接近大模型的性能。
如果希望进一步提升蒸馏效果,可以尝试:
- 增加训练轮次,让学生模型充分收敛。
- 使用更复杂的教师模型,提供更准确的软标签。
- 动态调整温度参数,先高后低。
- 加入数据增强,提升学生模型的泛化能力。
5. 常见问题与排查思路
在实际运行蒸馏代码时,可能会遇到以下几类问题。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 训练速度很慢 | 使用 CPU 训练,且教师模型加大数据量 | 无 GPU 时减少 epoch,或改用简单数据集 |
| 显存不足 OOM | 教师模型和学生模型同时参与前向计算 | 教师模型前向包裹no_grad,或减小 batch size |
| 蒸馏后学生模型效果提升不明显 | 温度参数不合适,或 α / β 权重失衡 | 尝试 T=2~6,调整 α 与蒸馏权重 |
| 训练损失不下降 | 学习率过大或数据预处理异常 | 调低学习率,检查数据归一化 |
| 教师模型效果太差 | 教师模型训练不充分 | 增加教师模型训练轮次,先保证教师模型精度高 |
| 模型保存后推理结果不一致 | 训练与推理模式未切换 | 推理时调用model.eval(),关闭 dropout 和 BN 更新 |
代码报kl_div维度错误 | 软标签对数概率与目标分布维度不匹配 | 检查log_softmax和softmax是否在最后一维计算 |
遇到报错时,可以先打印模型输出的 shape,再核对损失函数输入。大多数时候,问题出在logits没有经过正确的维度处理,或者设备不统一(一个在 CPU,一个在 GPU)。
另一个高频问题是:有的读者在蒸馏时忘记冻结教师模型。虽然教师模型即使参与梯度计算也能跑通,但会白白消耗大量计算资源,而且有可能把错误的梯度回传到教师模型,影响知识来源的稳定性。正确做法是始终用torch.no_grad()包裹教师模型。
6. 最佳实践与工程建议
6.1 教师模型的选择
蒸馏效果的上限,通常取决于教师模型的“知识质量”。如果教师模型本身精度不高,学生模型能学到的有效信息也有限。因此在蒸馏之前,要先确保教师模型得到充分训练。
在真实业务中,教师模型可以从以下来源获取:
- 自己训练的大模型:数据可控,但要承担训练成本。
- 开源的预训练大模型:省时省力,但要关注开源许可协议。
- 云端大模型 API 的输出:可以用来构造蒸馏数据,但要考虑数据安全和接口成本。
无论哪种方式,都要做好数据筛选。教师模型的错误输出如果被学生模型当成“标准答案”,会污染学生模型的训练效果。
6.2 温度与损失权重
温度 T 和损失权重 α 没有普适最优值,应该通过实验调参。推荐做法是固定其他变量,先在小验证集上扫一遍温度,观察学生模型在验证集上的表现。然后再调整 α 与蒸馏损失的权重,观察训练过程的稳定性。
一个经验是:任务越复杂,温度可以适当调高,让软标签携带更多类别关系信息;任务越简单,温度可以靠近 1,避免过平滑。
另外,蒸馏损失可以动态变化。训练初期可以加大蒸馏损失权重,让学生模型尽快模仿教师模型;训练后期适当提高硬标签损失权重,让学生模型学着修正教师模型的偏差。这种动态调整策略在实际项目中比较有效。
6.3 开源协议的合规性
Meta 开源模型涉及特定开源许可证。使用开源模型训练蒸馏模型,必须遵守对应许可证的条款。哪怕训练后的学生模型结构和原模型完全不同,只要蒸馏过程中使用了原模型权重或输出,就要关注是否符合许可范围。
建议在项目初期就把许可证问题列入技术方案评审。需要确认的问题包括:
- 是否可以商用。
- 是否允许基于模型输出做二次训练。
- 是否需要保留版权声明。
- 如果修改了模型,是否需要开源修改后的版本。
这些条款直接影响产品的商业化路径,不能只看“开源”两个字。
6.4 蒸馏后的评估
蒸馏完成后,不能只看准确率一个指标。在线服务场景下,还需要关注:
- 推理延迟:小模型比大模型快多少。
- 显存占用:能否在目标设备上运行。
- 长尾样本表现:学生模型在困难样本上会不会明显退化。
- 稳定性:面对输入扰动时,输出是否依然可靠。
- 可解释性:如果业务需要解释,小模型的决策是否符合预期。
建议建立一个包含常规样本、困难样本、边界样本的评测集,用它做回归测试。每次蒸馏、微调、量化后,都跑一遍完整评测,防止模型效果回退。
6.5 工程化部署
蒸馏得到的模型体积小、速度快,非常适合服务化部署。这里给出几条工程化建议:
- 模型格式:训练完成后导出为 ONNX 或 TensorRT 格式,方便在不同推理引擎上运行。
- 推理优化:结合量化(INT8、FP16)进一步压缩体积。
- 服务设计:小模型单机吞吐更高,可以配合弹性伸缩策略,应对流量波动。
- 日志监控:记录推理耗时、错误率和输入分布变化,及时发现数据漂移问题。
- 版本管理:蒸馏出的模型也要记录训练参数、数据集版本、教师模型版本,方便问题回溯。
从开源模型到蒸馏小模型,再到上线部署,这是一条完整的工程链路。只跑通训练脚本只是第一步,真正有挑战的是如何让模型稳定、高效地服务业务。
7. 总结与下一步
从 Meta 重新开源,到知识蒸馏成为热门讨论词,这背后反映的是 AI 行业对“能力”和“成本”的双重追求。开源模型提供了高质量的知识来源,蒸馏技术提供了轻量化落地的路径,两者结合,让更多团队有机会把大模型能力放进自己的产品里。
本文围绕这条主线完成了几件事:
- 解释了 Meta 开源战略的变化和开源生态的意义。
- 系统拆解了知识蒸馏的原理,包括教师模型、学生模型、温度参数和损失函数。
- 用 PyTorch 完整实现了一个 MNIST 分类蒸馏案例,可以直接复制运行。
- 给出了常见问题排查表和工程化部署建议。
接下来可以继续学习的方向有很多:如果你对算法细节感兴趣,可以阅读 Hinton 的经典蒸馏论文,再深入了解 Soft Label、Feature Distillation、Self-Distillation 等进阶方法;如果你偏工程,可以尝试把蒸馏应用到自己的业务模型上,比如文本分类、目标检测、语音识别;如果你关注大模型部署,可以进一步研究量化、剪枝、推理加速等配套技术。
无论选择哪个方向,都建议亲手跑一遍代码,哪怕只是修改几个超参数,也比只看理论更有效果。开源模型已经为你提供了很好的实验土壤,接下来就看你怎么利用它们了。