BERT 推理过程详解:第 2 步 从数字到答案

一句话理解

BERT 推理 = 把分词得到的数字送入模型 → 输出 logits → softmax 转概率 → argmax 取类别 → 查表返回答案。

📖 前置知识

本篇笔记承接 BERT 分词过程详解:第 1 步 从文字到数字。 假设你已经了解:分词器如何将文字转为 input_ids、attention_mask、token_type_ids 三个数组。

⚠️ 核心认知:BERT 分类模型只能输出"类别编号"

BERT 分类模型的全部输出 = 一个类别编号(如 12)。 它不会说话、不会生成文字、不会调用工具。 所谓的"回答问题",是代码拿到类别编号后查 categories.json 返回预设答案——这一步是 Python 代码做的,不是 AI 做的。

BERT = 一个只会指手指的人 你问他问题 → 他指向 17 个按钮中的第 12 个 就这么多。 按钮后面写着什么答案 → 是你提前贴上去的(categories.json) 按了按钮之后发生什么 → 是你写的代码控制的


推理的完整流程

承接分词笔记的输出(以"今天天气如何"为例):

  input_ids = [101, 791, 1921, 1921, 3698, 1963, 862, 102, 0, 0, ... 0]
               [CLS] 今    天    天    气    如    何  [SEP]  填充...

  attention_mask = [1, 1, 1, 1, 1, 1, 1, 1, 0, 0, ... 0]
                    真实内容=1         填充=0

  token_type_ids = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ... 0]
                    单句任务全是 0
    ↓
第 1 步:模型推理(数字 → logits)
    三个数组送入 BERT 模型
    模型输出 N 个数字(logits),每个数字对应一个类别的原始得分
    ↓
第 2 步:softmax 转概率
    logits → softmax 公式 → 概率(0~1,总和=1)
    ↓
第 3 步:argmax 取最大值
    概率最高的那个 → 类别编号
    ↓
第 4 步:查表返回答案
    类别编号 → 查 categories.json → 返回预设答案

实际例子:输入"今天天气如何"

第 1 阶段:模型推理(数字 → logits)

三个数组送入 BERT 模型(本项目用 ONNX 格式部署,也可以用 PyTorch 格式)
    ↓
模型输出 17 个数字(logits):

💡 为什么是 17 个?

logits 数量 = 类别数量。本项目 categories.json 定义了 17 个类别(如"打招呼"、"问天气"等),所以模型输出 17 个数字——每个数字对应一个类别的"原始得分"。如果是 82 个类别,就会输出 82 个 logits。


💡 模型从哪来?预训练 vs 微调

读者可能会问:这个模型和 categories.json 从哪来?

┌─────────────────────────────────────────────────────────────────────┐
│                        BERT 模型的"使用流程"                         │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  第 1 阶段:预训练模型 / 基座模型(bert-base-chinese)               │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │ 来源:Google 发布                                            │   │
│  │ 能力:理解中文语言(词向量、句法、语义)                        │   │
│  │ 不能:分类到你的业务类别(不知道"打招呼"是什么)                 │   │
│  │                                                              │   │
│  │ 类比:一个"懂中文的大学毕业生"                                 │   │
│  │   ├── 他懂语言、懂语法、能理解句子意思                         │   │
│  │   └── 但他不知道你们公司的业务                                 │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                              ↓                                      │
│                         微调(Fine-tuning)                         │
│                    用你的训练数据教它分类                             │
│                              ↓                                      │
│  第 2 阶段:微调模型 / 分类模型 / 业务模型(hqx-bert-base-cs)      │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │ 来源:你在 bert-base-chinese 基础上训练                       │   │
│  │ 能力:理解中文 + 分类到你的 17 个业务类别                       │   │
│  │                                                              │   │
│  │ 类比:这个毕业生经过"入职培训"后                               │   │
│  │   ├── 仍然懂中文                                              │   │
│  │   └── 现在还能把用户问题分类到你们公司的 17 个业务类别           │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘
完整流程:

1. 下载 bert-base-chinese(预训练模型 / 基座模型,懂中文但不懂你的业务)
2. 准备训练数据(问题 → 类别编号)
3. 微调训练(教它"打招呼"是类别12,"问天气"是类别5...)
4. 导出微调模型(PyTorch 或 ONNX 格式)
5. 准备 categories.json(类别编号 → 预设答案)
6. 部署使用(分词 → 推理 → 查表返回答案)

💡 术语说明:"微调"还是"训练"?

严格来说,training_data 文件夹里的过程叫"微调(Fine-tuning)":

┌─────────────────────────────────────────────────────────────────┐
│                        模型开发的两个阶段                         │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  阶段 1:预训练(Pre-training)                                  │
│  ├── 做什么:在海量文本上训练模型学习语言                          │
│  ├── 数据量:数十GB文本(如整个中文维基百科)                      │
│  ├── 算力需求:数千GPU小时                                       │
│  ├── 产出:bert-base-chinese(懂中文,但不懂你的业务)             │
│  └── 谁做的:Google                                              │
│                                                                 │
│  阶段 2:微调(Fine-tuning)← training_data 里的是这个            │
│  ├── 做什么:在预训练模型基础上,用你的数据教它做特定任务           │
│  ├── 数据量:几千条问答对(train.jsonl, val.jsonl)               │
│  ├── 算力需求:几十分钟~几小时(Colab T4 就够)                   │
│  ├── 产出:hqx-bert-base-cs(能分类到你的 17 个类别)             │
│  └── 谁做的:你                                                  │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

为什么叫"微调"而不是"训练"?
  ├── "训练"是广义说法(都可以用)
  ├── "微调"更精确:强调"在已有基础上做小幅调整"
  └── 学术/技术文档推荐用"微调"

模型命名约定:
  bert-base-chinese      → 预训练模型 / 基座模型
  hqx-bert-base-cs       → 微调模型 / 分类模型 / 业务模型

💡 模型内部发生了什么?

这里只展示了"输入 → 输出"的黑盒结果。如果想了解模型内部 12 层 Transformer 如何逐层"理解"输入,请看 BERT 的 12 层 Transformer:每层在做什么?

[-0.08, -0.38,  0.14, -0.74, -0.31,  1.19, -0.58, -0.55,
 -0.26,  0.23, -0.37,  0.16,  1.68,  0.08, -0.53, -0.30,  0.68]
 类别0   类别1   类别2   类别3   类别4   类别5   类别6   类别7
 类别8   类别9   类别10  类别11  类别12  类别13  类别14  类别15  类别16

这些数字叫 logits(原始得分):
  正数 → 模型觉得"可能是这个类别"
  负数 → 模型觉得"不太可能"
  越大 → 越可能

第 2 阶段:softmax 转概率 + 取最大值

logits → softmax 公式 → 概率(0~1,总和=1)

  P(i) = e^(score_i) / Σ e^(score_j)

转换后:
  类别 12:0.2432(24.32%)  ← 最高!
  类别 5:  约 15%
  类别 16:约 10%
  其余类别:各占几个百分点

→ argmax = 12(类别编号)
→ 置信度 = 24.32%

💡 什么是 softmax?

一句话:把模型输出的"原始得分"(logits)转成"概率"(0~1,总和=1)。

为什么需要 softmax?

模型输出的 logits:
  [-0.08, -0.38, 0.14, -0.74, ..., 1.68, ...]

问题:
  ├── 有正有负,不好理解
  ├── 数值大小没有上限
  └── 不能直接说"类别12有1.68的把握"——1.68是什么意思?

softmax 的作用:
  ├── 把所有数字转成 0~1 之间的概率
  ├── 所有概率加起来 = 1(100%)
  └── 现在可以说"类别12有24.32%的把握"
具体例子:

原始 logits(模型直接输出的):
  类别0: -0.08    类别5:  1.19    类别12: 1.68
  类别1: -0.38    类别6: -0.58    ...
  ...

经过 softmax 后:
  类别0: 0.04 (4%)     类别5:  0.15 (15%)   类别12: 0.24 (24%)  ← 最高
  ...
  所有概率加起来 = 1.00 (100%)
类比理解:

想象 17 个学生考试:

logits = 原始分数(可能有负分,不好比较)
  学生0: -0.08分   学生5: 1.19分   学生12: 1.68分

softmax = 把原始分数转成"排名百分比"
  学生0: 4%    学生5: 15%    学生12: 24%  ← 最高

现在一目了然:学生12表现最好(24%)

第 3 阶段:查表返回答案

类别 12 → 查 categories.json

{
  "12": {
    "category": "打招呼",
    "answer": "你好呀~ 我是海小星,海启星的对外形象官!有什么想了解的尽管问我😊"
  }
}

→ 返回预设答案

完整流程图

"今天天气如何"
    ↓ 分词器(见上一篇笔记)
token IDs: [101, 791, 1921, 1921, 3698, 1963, 862, 102, 0, ...]
    ↓ BERT 模型
logits: [-0.08, -0.38, ..., 1.68, ...]   ← 17 个原始得分
    ↓ softmax
概率:   [0.04, 0.03, ..., 0.24, ...]     ← 17 个概率
    ↓ argmax
类别 ID = 12,置信度 = 24.32%
    ↓ 查 categories.json
答案 = "你好呀~ 我是海小星..."

为什么"今天天气如何"被分类为"打招呼"?

BERT 分类模型不是"理解"问题,而是"模式匹配":
  ├── 训练数据中,"今天天气如何"可能和打招呼的样本更相似
  ├── 它没有"查天气"的工具能力,只能从 17 个类别中选最接近的
  └── 24.32% 的置信度说明模型也不太确定(不是特别有把握)

这就是 BERT 分类 + 查表的局限:
  只能从预设类别中选,不能真正"理解"和"执行"
  → 真正的天气查询需要 LLM + 工具调用(Function Calling)

代码实现(test_onnx.py 中的推理部分)

import onnxruntime as ort
import numpy as np

# 加载 ONNX 模型(也可以用 PyTorch 模型,代码结构类似)
session = ort.InferenceSession("model.onnx")

# 假设 encoding 是分词器的输出(见上一篇笔记)
input_ids = np.array([encoding.ids], dtype=np.int64)
attention_mask = np.array([encoding.attention_mask], dtype=np.int64)
token_type_ids = np.array([encoding.type_ids], dtype=np.int64)

# 模型推理
outputs = session.run(None, {
    "input_ids": input_ids,
    "attention_mask": attention_mask,
    "token_type_ids": token_type_ids,
})

# outputs[0] 就是 logits(每个类别的原始得分)
logits = outputs[0]

# softmax 转概率
probabilities = np.exp(logits) / np.sum(np.exp(logits))

# argmax 取类别
predicted_class = np.argmax(probabilities)
confidence = probabilities[predicted_class]

print(f"类别: {predicted_class}, 置信度: {confidence:.2%}")

一句话总结

BERT 推理 = 数字 → 答案 的翻译过程

输入:三个数组(input_ids、attention_mask、token_type_ids)
  ↓
模型推理:三个数组 → logits(每个类别的原始得分)
  ↓
softmax:logits → 概率(0~1,总和=1)
  ↓
argmax:概率 → 类别编号(得分最高的那个)
  ↓
查表:类别编号 → categories.json → 预设答案

关键概念:
  logits    → 模型输出的原始得分(正数=可能,负数=不太可能)
  softmax   → 把得分转为概率(方便理解"有多大把握")
  argmax    → 取概率最高的类别
  查表      → 类别编号对应的答案(是代码写的,不是 AI 生成的)

📝 创建:2026-09-14
🔄 标签:BERT、推理、softmax、argmax、分类、categories.json

相关笔记