阿明是市图书馆的新任管理员。过去那套推荐系统非常“认真”:每当一位读者对某本书感兴趣,它都要拿这本书和全馆所有书逐一比较,再给出推荐。小阅览室还能撑住,可到了藏书百万册的大城市图书馆,这套“全库两两比对”的做法立刻崩溃——工作量是书本数量的平方。Longformer 就是阿明的管理方案:他不再要求每本书认识全馆,而是先查手边最近的书(滑动窗口),再每隔几本抽样以望得更远(膨胀窗口),最后把少数核心参考书放到前台供所有人查阅(全局 token)。这套稀疏但高效的管理策略,让图书馆的工作量从“平方级”降到“线性级”,也正是 Longformer 处理超长文本的核心思路。

岗位关联

对 AI 从业者而言,掌握 Longformer 意味着你能在生产环境中处理动辄上万 token 的合同、论文和代码,不再被序列长度逼到截断或爆显存,这是长文本 NLP 工程落地的关键能力。

目录
  1. 可视化演示
  2. 滑动窗口机制
  3. 膨胀窗口设计
  4. 全局Token策略
  5. 线性复杂度原理
  6. 显存优化技巧
  7. 推理加速方法
  8. 变体横向对比
  9. HuggingFace实践
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

本节配有交互动效(进入视口后自动加载,约 3KB)。

\u6ED1\u52A8\u7A97\u53E3\u6CE8\u610F\u529B: \u6BCF\u4E2AToken\u53EA\u5173\u6CE8\u7A97\u53E3\u5185\u7684\u90BB\u5C45 \u6ED1\u52A8\u7A97\u53E3\u7279\u70B9\u81A8\u80C0\u7A97\u53E3\u4F18\u52BF\u5168\u5...\u6ED1\u52A8\u7A97\u53E3\u7279\u70B9\u81A8\u80C0...</sp...

滑动窗口机制

阿明发现,读者想了解一本书时,最相关的往往是同一排书架、相隔不远的那几本。与其让每本书都和全馆书籍比较,不如立一条规则:每个 token 只关注左右一定范围内的邻居。这个“左右一定范围”就是滑动窗口。

在 Longformer 里,每个 token 不再与整个序列做点积,而是只与窗口内的 token 计算注意力分数。窗口外的位置在 softmax 之前被掩码遮住,相当于直接不在推荐列表里出现。因为窗口大小是人工设定的常数,所以注意力计算量不再随序列长度平方增长。

实际工程中,窗口大小通常选 512 或 1024。如果任务中句子局部依赖很强,比如文本分类和抽取式问答,512 已经够用;如果文档跨度很大,比如长文档摘要,1024 会更稳妥。窗口越大,能看到的上下文越多,但计算和显存也随之上升。

阿明的“邻架规则”之所以有效,是因为语言里的相关性大多是局部的:一句话的核心意思,往往由前后几句话决定。这样既保住了主要信息,又把全库比对变成了线性工作量。

Cost(n,w)=n×(2w+1)=O(nw)\displaystyle \text{Cost}(n,w) = n \times (2w+1) = O(nw)
这里的 w 是单侧窗口半径,w 固定后,总成本随序列长度 n 线性增长。
  1. n(序列长度)文本被切分后的 token 总数,相当于图书馆里的书的总数。
    nn
  2. w(单侧窗口半径)每个 token 能往左、往右各看 w 个位置。
    ww
  3. 2w+1左右各 w 个邻居,再加上自己当前的位置。
    2w+12w+1
  4. 线性成本每行只算固定个数,不再是整个 n 维向量相乘。
    O(n(2w+1))O(n(2w+1))

膨胀窗口设计

只看邻架也有问题:有些重要的书隔了三个书架,但阿明的规则永远看不到它。于是他想到一个办法:不连续地查每一本,而是每隔 d 本抽一本。这样同样走一趟,视线却能越过好几个书架。

这就是膨胀注意力。它保留滑动窗口的“局部范围”思想,但窗口内只取距离为 d、2d、3d……的采样点。比如膨胀率 d=2,一个 token 会关注距离 2、4、6 的位置,而不是位置 1、2、3。于是每层计算量没有变多,最远却能看到原来的几倍。

多层 Transformer 叠加后,每一层都让信息再跳一次。浅层看相邻语义,深层看到跨段落关系,这就是膨胀窗口扩大感受野的原理。工程上可以只在靠后的若干层开启膨胀,避免最底层丢失细节。

回到图书馆:阿明不需要把每个书架都停下来,只需隔架扫一眼,就能判断远处是否有值得推荐的书籍。膨胀窗口是一款“省钱望远镜”,代价是可能漏掉中间细节,所以工程中通常与连续窗口搭配使用。

SpanL≈2×d×r×L\displaystyle \text{Span}_L \approx 2 \times d \times r \times L
r 表示每侧采样的位置个数,d 表示膨胀率,L 表示层数;层数越深,能覆盖的总跨度越大。
  1. d(膨胀率)每隔 d 个位置取一个采样点。
    dd
  2. r(每侧采样数)单侧最多抽取 r 个位置。
    rr
  3. 单层跨度最大关注距离约为 2dr。
    2dr2dr
  4. 多层叠加L 层之后,信息最远可以传播到大约 2drL 的位置。
    2drL2drL

全局Token策略

有些书是全馆的核心参考书,比如字典、地图和馆史资料。阿明把它们放到前台,任何读者都能直接查阅;这些书也可以主动跳到任意书架。这个“前台核心书”的概念,对应 Longformer 里的全局 token。

全局 token 的注意力模式非常特殊:它要对序列中所有 token 计算注意力,同时所有 token 也要关注它。换句话说,全局 token 保留了完整注意力的“特权”,但数量必须非常少,否则又会回到全连接的成本。

实际设置时,通常把 CLS 符号、特殊分隔符、问题 token 或任务关键实体设为全局。比如在长文档问答中,把问题整体设为全局,让模型每读到一段正文都能随时对照问题。数量一般控制在几个到几十个,不能像滑动窗口那样大量使用。

阿明在前台只放了几本核心参考书,而不是把整个书架搬过去。全局 token 的价值在于“精准锚点”:它让长文档中的关键信息始终在场,却不会让计算量失控。

Cost=n×(2w+1)+n×g\displaystyle \text{Cost} = n \times (2w+1) + n \times g
g 是全局 token 数量。第一项是滑动窗口成本,第二项是所有 token 与全局 token 交互的成本。
  1. g(全局 token 数)被放到“前台”的特殊位置个数。
    gg
  2. n×g每个普通 token 都要和每个全局 token 做一次注意力计算。
    n×gn \times g
  3. 窗口部分普通 token 之间仍只做窗口内注意力。
    n×(2w+1)n \times (2w+1)
  4. 总成本只要 g 很小,整体仍是线性复杂度。
    O(n(w+g))O(n(w+g))

线性复杂度原理

原始 Transformer 的注意力,相当于每一位读者都要和全馆所有书做一次比较。如果读者有 n 个,书也有 n 本,总操作就是 n×n,也就是平方级复杂度。阿明的三件套——邻架、抽样、前台——让每本书只需要和固定数量的书发生关联。

Longformer 没有改变注意力公式本身,它改变的是“哪些位置允许互相看”。具体做法是在 QK^T 计算之后加一个稀疏掩码:窗口外、抽样外、非全局位置统统遮掉。这样每个 token 的注意力行只有 k 个非零元素,k 是常数。

从复杂度上看,全注意力需要计算 n² 对关系,Longformer 只需要 n×k 对关系。当序列长度从 512 涨到 8192 时,全注意力的成本会涨 256 倍,而 Longformer 只涨 16 倍,这就是工程意义上的“线性注意力”。

阿明并没有发明一种全新的“推荐算法”,他只是改变了哪些书会出现在比较名单上。名单缩小后,平方级工作变成了线性工作,这才是 Longformer 能在超长文本上跑起来的根本原因。

Full: O(n2)Longformer: O(n×(2w+g+1))\displaystyle \text{Full: } O(n^2) \qquad \text{Longformer: } O(n \times (2w+g+1))
Full 是平方级,Longformer 是线性级;差距主要在“每行非零元素个数”是不是常数。
  1. 全注意力每一行都要和所有 n 个位置计算。
    O(n2)O(n^2)
  2. 滑动窗口项每行最多关注 2w+1 个邻居。
    2w+12w+1
  3. 全局 token 项每行还要额外关注 g 个全局位置。
    gg
  4. 总复杂度括号内是常数,所以整体是 O(n)。
    O(n)O(n)

显存优化技巧

图书馆旺季时,阿明发现临时借阅记录堆满了仓库。他不再把每一阶段的中间草稿都保存下来,而是只保存关键结果,需要反向计算时再重新生成。这种做法叫梯度检查点,本质是用时间换空间。

显存压力主要来自两部分:一是注意力矩阵本身,二是反向传播需要保存的中间激活。Longformer 用稀疏注意力把注意力矩阵从 n² 压到 n×k,大幅减少第一类开销;梯度检查点则针对第二类开销,随机丢弃部分中间激活,反向传播时重新计算。

分块注意力也是工程中常用的手段。它不让完整 n×n 矩阵一次性驻留显存,而是按块计算和累积注意力分数。配合混合精度训练,可以把长文本模型的训练从“一张卡放不下”变成“多卡也能塞下”。

阿明把仓库里用不到的草稿全部丢掉,等需要时再重新写一遍。虽然多花了一点时间,但图书馆终于不会被资料淹没了。

Mattn=O(n×k),k=2w+g+1\displaystyle M_{\text{attn}} = O(n \times k), \qquad k = 2w + g + 1
注意力矩阵的显存开销由 n² 变成了 n×k,k 是每行实际关注的 token 数。
  1. 注意力矩阵显存全注意力要存 n² 个分数,稀疏注意力只存 n×k 个。
    O(n×k)O(n \times k)
  2. k 的组成窗口内 2w+1 个位置,再加上全局 token 数 g。
    k=2w+g+1k = 2w + g + 1
  3. 梯度检查点只保存部分激活,反向传播时重算,减少峰值显存。
    Mcheckpoint<MfullM_{\text{checkpoint}} < M_{\text{full}}
  4. 总显存稀疏结构与重计算叠加后,长序列训练成为可能。
    O(nk)+O(activations)O(nk) + O(\text{activations})

推理加速方法

训练完成后,阿明还要面对每天成千上万的现场咨询。他不再每次临时翻书架,而是把最常用的推荐路径做成快捷卡片,直接递给读者。这就是推理阶段的缓存和 kernel 融合。

Longformer 的稀疏注意力天然适合算子融合:把掩码生成、QK^T 计算、softmax 和加权求和合并成一个自定义 kernel,减少中间矩阵存到显存再读回的次数。尤其在推理时,序列是逐步生成的,KV 缓存让已计算过的历史状态可以复用。

另一个实用技巧是批处理。长文本请求往往长度差异很大,动态 padding 和按长度分组可以避免整批被最长的样本拖慢。工程上还可以把 Longformer 导出为 ONNX 或 TorchScript,利用图优化进一步压缩延迟。

阿明的快捷卡片并不改变推荐质量,但能大幅减少每次咨询的翻书时间。推理优化也是如此:算法不变,服务的速度和吞吐量却完全不同。

Speedup≈n2n×(2w+1)=n2w+1\displaystyle \text{Speedup} \approx \frac{n^2}{n \times (2w+1)} = \frac{n}{2w+1}
这是理论上的稀疏化加速比;实际还要考虑 kernel 开销,但数量级可以参考。
  1. 全注意力耗时每行要算 n 个位置的分数。
    n2n^2
  2. 稀疏注意力耗时每行只算 2w+1 个位置。
    n(2w+1)n(2w+1)
  3. 理论加速比两者相除得到 n 除以窗口宽度。
    n2w+1\frac{n}{2w+1}
  4. 工程含义n 越大,稀疏化的收益越明显。

变体横向对比

阿明的图书馆管理方案,在隔壁街区也出现了不同版本。BigBird 图书馆采用了“邻架 + 前台 + 随机抽查”,也就是说除了固定邻居,每条书架上还会随机抽几本作为补充。随机连接让信息传播更均匀,但随机性也给复现带来一点麻烦。

Linformer 图书馆走的则是另一条路:它把所有书浓缩成固定长度的摘要卡,注意力不再发生在原书之间,而是发生在书和摘要卡之间。这样也能把复杂度降到线性,但代价是长距离细节会被压缩。

选择哪个变体,取决于任务特点。如果文本有很强的一维局部结构,比如合同和代码,Longformer 的滑动窗口最自然;如果希望全局信息也能通过随机捷径传播,BigBird 更稳;如果输入长度极度不均又希望结构简单,Linformer 的低秩投影更方便。

阿明并不认为自己的方案天下无敌。每个图书馆的布局、读者习惯和藏书结构不同,只有匹配任务特性的管理策略,才是真正高效的好策略。

Longformer: 滑动+膨胀+全局BigBird: 滑动+随机+全局Linformer: 低秩投影\displaystyle \text{Longformer: 滑动+膨胀+全局} \qquad \text{BigBird: 滑动+随机+全局} \qquad \text{Linformer: 低秩投影}
三者都是线性复杂度,但信息混合方式不同,选型要看数据的局部性和全局性需求。
  1. Longformer 模式用滑动窗口和膨胀抽样覆盖局部,用少量全局 token 做锚点。
    滑动+膨胀+全局\text{滑动} + \text{膨胀} + \text{全局}
  2. BigBird 模式在滑动窗口之上加入随机连接,强化全局信息传播。
    滑动+随机+全局\text{滑动} + \text{随机} + \text{全局}
  3. Linformer 模式通过投影矩阵把序列压缩到固定长度。
    低秩投影\text{低秩投影}
  4. 选型要点局部性强的任务优先 Longformer,需要更多全局捷径选 BigBird。
    场景→模式选择\text{场景} \to \text{模式选择}

HuggingFace实践

阿明把“窗口、抽样、前台”这三条规则写进了图书馆管理系统的配置文件。以后不管换哪个馆员,只要读取配置,就知道该怎么摆放书架。HuggingFace 的 Longformer 就是这样一个“可配置管理系统”。

使用 LongformerModel.from_pretrained 加载模型后,核心配置是 attention_window。它可以是一个整数,也可以是一个列表,表示每一层 Transformer 的窗口大小。比如浅层用 512,深层用 1024,就能兼顾局部细节和长程语义。

全局 token 通过 input_global_attention_mask 传入:把需要设为全局的位置置 1,其他置 0。这个掩码可以和 input_ids、attention_mask 一起送入模型。训练时建议从预训练权重出发做微调,否则稀疏注意力模式从头训练收敛很慢。

阿明把管理经验沉淀成配置后,新馆员只需要照章办事。对工程师来说,这意味着可以把 Longformer 当成一个可插拔的编码器,快速接入自己的长文本任务。

cost≈n×(attention_window+global_tokens)\displaystyle \text{cost} \approx n \times (\text{attention\_window} + \text{global\_tokens})
配置好窗口和全局 token 数量后,模型的计算量就基本确定,并随 n 线性增长。
  1. attention_window决定每层窗口半径,是 Longformer 最重要的超参数。
    attention_window\text{attention\_window}
  2. global_tokens由全局注意力掩码标记的位置数量。
    global_tokens\text{global\_tokens}
  3. 总成本每一行都要覆盖窗口位置和全局位置。
    n×(attention_window+global_tokens)n \times (\text{attention\_window} + \text{global\_tokens})
  4. 线性增长n 增大时成本按比例增加,不会爆炸。
    O(n)O(n)

AI 应用场景

超长法律合同审查

阶段:模型训练与微调

解决的问题:合同文本动辄上万 token,传统做法只能截断前 512 token,导致关键违约条款丢失。Longformer 用滑动窗口覆盖全文,并把合同编号、金额、日期等实体设为全局 token,让模型始终能看到关键信息。

具体案例:某法律科技公司用 Longformer 微调合同风险分类模型。输入整份合同,全局 token 标记“付款条件”和“违约责任”段落,模型在识别高风险条款时比截断方案 F1 提升 8 个百分点。

论文库离线向量化

阶段:离线推理与向量检索

解决的问题:把整篇学术论文编码成向量时,序列太长导致显存溢出。Longformer 的线性复杂度让单卡也能编码完整论文,不需要切段再拼接。

具体案例:某科研情报平台用 HuggingFace 的 Longformer 对 PubMed 摘要加正文编码,生成论文向量后存入向量数据库。用户搜索时可以先语义召回,再让模型精读全文。

代码缺陷定位

阶段:推理加速与线上服务

解决的问题:代码文件常常超过 2048 token,且缺陷往往与几十行之外的函数定义相关。Longformer 能把整个文件喂给模型,同时只对函数签名和 import 语句设置全局注意力。

具体案例:一家 DevOps 公司在 CI 流程中部署 Longformer 代码审查服务,用 ONNX 导出优化推理,把单文件分析延迟从 800ms 降到 350ms,并成功定位跨函数的空指针隐患。

Python 演示

python36 行
def build_longformer_mask(n, window=4, dilation=2, global_tokens=(0, 15)):    mask = []    for i in range(n):        allowed = set()        for step in range(-window, window + 1):            j = i + step            if 0 <= j < n:                allowed.add(j)        gap = dilation        while True:            left = i - gap            right = i + gap            if left < 0 and right >= n:                break            if left >= 0:                allowed.add(left)            if right < n:                allowed.add(right)            gap += dilation        for g in global_tokens:            allowed.add(g)        mask.append(sorted(allowed))    nonzero = sum(len(row) for row in mask)    full = n * n    ratio = nonzero / full * 100    return mask, nonzero, full, ratio  if __name__ == "__main__":    n = 32    mask, nonzero, full, ratio = build_longformer_mask(n)    print("sequence length:", n)    print("non-zero attention entries:", nonzero)    print("full attention entries:", full)    print("non-zero ratio: %.2f%%" % ratio)    print("sparsity ratio: %.2f%%" % (100 - ratio))
目的:用纯 Python 实现 Longformer 的稀疏注意力掩码,直观比较稀疏注意力与全注意力的计算量差距。
  1. allowed = set()每个 token 用集合保存它允许关注的位置。
  2. for step in range(-window, window + 1):对应滑动窗口的左右邻居遍历。
  3. if 0 <= j < n:越界的位置直接丢弃,保证索引合法。
  4. while True:循环叠加膨胀采样点,直到左右两侧都越界。
  5. gap += dilation让采样距离从 d 变成 2d、3d,不断扩大感受野。
  6. for g in global_tokens:把全局 token 强行加入每个 token 的关注列表。
  7. mask.append(sorted(allowed))最终生成每行有序的稀疏注意力索引。
  8. ratio = nonzero / full * 100计算稀疏注意力实际覆盖的比例,直观看出省了多少计算。

常见误区

误区我听说 Longformer 是线性注意力,所以它应该把所有 QK^T 都换成了核函数或低秩近似。

正解Longformer 并没有改变注意力计算公式,它只是用稀疏掩码让每个 token 只关注固定数量的位置。复杂度是 O(n),但本质是“稀疏注意力”,和 Performer 那种核函数线性注意力不是一回事。

为何常见:因为很多文章把“线性复杂度”简化成“线性注意力”,导致初学者把两类方法混为一谈。

误区既然窗口能看到更多上下文,那我直接把 attention_window 设成 4096,效果一定比 512 好。

正解窗口增大会线性增加计算和显存,而且可能引入噪声。很多长文档任务的局部性很强,512 或 1024 已经足够,关键是配合膨胀窗口和全局 token,而不是盲目加大窗口。

为何常见:人们容易忽略“信息冗余”和“资源成本”的平衡,只想用更大的窗口获得更优效果。

误区全局 token 能看到所有位置,那我干脆把所有 token 都设成全局,模型肯定最强。

正解如果全部设成全局,Longformer 就退化成原始全注意力,n² 的复杂度全部回来,失去稀疏化的意义。全局 token 应该像“前台参考书”一样少而精准。

为何常见:全局 token 听起来像“注意力特权”,容易让人忽略它的开销是 O(n×g),g 一旦等于 n 就会爆炸。

误区BigBird 也是滑动窗口加全局 token,所以它和 Longformer 就是同一个模型换了个名字。

正解两者最大的差别是 BigBird 额外引入了随机注意力:每个 token 还会随机关注少量远处位置。这个随机连接让信息传播更均匀,但也让训练结果带有随机性,复现时需要固定随机种子。

为何常见:只看结构图时,两者都有窗口和全局块,容易忽略中间的“随机块”。

面试题

Q1. Longformer的滑动窗口注意力主要解决什么问题?

单选题 · easy

  • A.提高词向量的表示维度
  • B.把注意力成本降到线性← 正确答案
  • C.让模型看得更远更全面
  • D.取代所有位置编码方法

答案:B。Longformer 用窗口内注意力替代全连接注意力,每行只算固定数量位置,因此把 O(n²) 降到了 O(n)。A、D 与维度或位置编码无关,C 是全局 token 的职责。

Q2. 典型的Longformer单侧窗口大小常设为?

单选题 · easy

  • A.通常取2或4个位置
  • B.通常取64或128个位置
  • C.通常取512或1024← 正确答案
  • D.通常取4096或8192

答案:C。Longformer 论文和工程实践中常用 512 或 1024 作为单侧窗口大小。太小则看不到长程依赖,太大会失去低复杂度优势。

Q3. 膨胀注意力中dilation=2的效果是?

单选题 · mid

  • A.只关注左右2个邻近位置
  • B.关注距离为1、2、3的位置
  • C.关注距离为2、4、6的位置← 正确答案
  • D.完全随机选择关注位置

答案:C。dilation=2 表示每隔 2 个位置取一个采样点,因此关注距离是 2、4、6……这样可以不增加计算量却扩大感受野。A、B 是连续窗口的行为,D 是随机注意力。

Q4. Longformer全局token适合放在哪些位置?

多选题 · mid

  • A.文档开头的CLS符号← 正确答案
  • B.段落首句的少量位置← 正确答案
  • C.任务关键实体附近位置← 正确答案
  • D.每一个词元都设成全局

答案:ABC。全局 token 用于提供稳定锚点,CLS、段落首句和关键实体都适合。如果每个 token 都设为全局,稀疏注意力就会退化成全注意力,失去线性复杂度优势。

Q5. Longformer的计算复杂度是?

单选题 · easy

  • A.序列长度的平方O(n²)
  • B.随序列长度线性O(n)← 正确答案
  • C.随窗口平方O(w²)
  • D.对数级别O(nlogn)

答案:B。滑动窗口和全局 token 都是固定常数个位置,因此总成本和 n 成线性关系。全注意力才是 O(n²),窗口大小不是序列长度。

Q6. 相比全注意力,Longformer在推理时的理论加速比约为?

单选题 · mid

  • A.约等于n除以窗口大小← 正确答案
  • B.约等于窗口大小除以n
  • C.约等于n乘窗口大小
  • D.约等于n平方乘窗口大小

答案:A。全注意力的计算量是 n²,Longformer 是 n×w,两者相除得到 n/w。n 越大,稀疏化带来的加速越明显。

Q7. 关于梯度检查点的说法,哪些是正确的?

多选题 · mid

  • A.不保存全部中间激活← 正确答案
  • B.反向传播时重新计算← 正确答案
  • C.顺带使用混合精度训练
  • D.缩小嵌入维度降显存

答案:AB。梯度检查点通过丢弃部分中间激活、在反向传播时重算来降低峰值显存。混合精度和缩小嵌入维度是其他优化手段,不属于梯度检查点的核心做法。

Q8. 关于BigBird与Longformer,正确的说法是?

多选题 · hard

  • A.两者都使用滑动窗口← 正确答案
  • B.BigBird额外加入随机注意← 正确答案
  • C.Longformer强制使用全局标记
  • D.两者复杂度均为O(n²)

答案:AB。BigBird 也有滑动窗口和全局 token,但它额外引入了随机注意力。Longformer 的全局 token 是可选配置,不是强制;两者复杂度都是 O(n)。

Q9. Linformer与前两者的核心差异是?

单选题 · hard

  • A.用卷积替换所有注意力层
  • B.将序列投影到固定低维空间← 正确答案
  • C.把滑动窗口扩大到整个序列
  • D.用循环网络压缩历史状态

答案:B。Linformer 通过低秩投影把序列长度压缩到固定维度,让注意力矩阵变成固定大小。它没有滑动窗口,也不用随机注意力。

Q10. 使用HuggingFace Longformer时,如果要让某个位置能访问全序列,应该怎么做?

单选题 · extreme

  • A.把attention_window设为0
  • B.给该位置global mask置1← 正确答案
  • C.开启output_hidden_states
  • D.在配置里关闭use_cache

答案:B。Longformer 通过 input_global_attention_mask 控制全局注意力,需要全序列访问的位置置 1。window 设为 0 反而会让它完全无法关注别人,后两个选项与全局注意力无关。