first commit
This commit is contained in:
481
tokenizer.md
Normal file
481
tokenizer.md
Normal file
@@ -0,0 +1,481 @@
|
||||
# DETR 的视觉 token 化过程说明
|
||||
|
||||
本文基于当前项目代码 `app/detector.py` 中的实现说明 DETR 的“token 化”过程。
|
||||
|
||||
当前代码使用的是 Hugging Face Transformers:
|
||||
|
||||
```python
|
||||
self.processor = DetrImageProcessor.from_pretrained(model_name)
|
||||
self.model = DetrForObjectDetection.from_pretrained(model_name)
|
||||
```
|
||||
|
||||
默认模型为:
|
||||
|
||||
```text
|
||||
facebook/detr-resnet-50
|
||||
```
|
||||
|
||||
需要注意:DETR 这里没有文本 tokenizer。它处理的是图像,因此所谓“token 化”指的是把图像经过 CNN backbone 后得到的二维视觉特征图,展开成 Transformer 可以处理的一维视觉 token 序列。
|
||||
|
||||
## 当前代码中的入口
|
||||
|
||||
在 `app/detector.py` 中,检测入口是:
|
||||
|
||||
```python
|
||||
image = Image.fromarray(frame_rgb)
|
||||
inputs = self.processor(images=image, return_tensors="pt")
|
||||
inputs = {key: value.to(self.device) for key, value in inputs.items()}
|
||||
|
||||
outputs = self.model(**inputs)
|
||||
```
|
||||
|
||||
这里分成两部分:
|
||||
|
||||
1. `DetrImageProcessor`:做图像预处理。
|
||||
2. `DetrForObjectDetection`:在模型内部完成视觉特征提取、flatten、位置编码、Transformer 编码解码和目标检测。
|
||||
|
||||
## 总体流程
|
||||
|
||||
完整流程可以理解为:
|
||||
|
||||
```text
|
||||
OpenCV RGB 帧
|
||||
↓
|
||||
PIL Image
|
||||
↓
|
||||
DetrImageProcessor 图像预处理
|
||||
↓
|
||||
pixel_values: [batch, 3, H, W]
|
||||
pixel_mask: [batch, H, W]
|
||||
↓
|
||||
ResNet-50 backbone 提取视觉特征
|
||||
↓
|
||||
feature map: [batch, 2048, H', W']
|
||||
↓
|
||||
1×1 convolution 投影通道
|
||||
↓
|
||||
projected feature map: [batch, 256, H', W']
|
||||
↓
|
||||
flatten 空间维度 H' × W'
|
||||
↓
|
||||
visual tokens: [batch, H'×W', 256]
|
||||
↓
|
||||
加入二维位置编码
|
||||
↓
|
||||
Transformer Encoder
|
||||
↓
|
||||
Object Queries + Transformer Decoder
|
||||
↓
|
||||
类别 logits + 边界框 boxes
|
||||
↓
|
||||
post_process_object_detection 还原到原图坐标
|
||||
```
|
||||
|
||||
## 第 1 步:图像预处理
|
||||
|
||||
代码:
|
||||
|
||||
```python
|
||||
inputs = self.processor(images=image, return_tensors="pt")
|
||||
```
|
||||
|
||||
`DetrImageProcessor` 主要做这些事情:
|
||||
|
||||
- 调整图像尺寸。
|
||||
- 转换为 PyTorch tensor。
|
||||
- 归一化像素值。
|
||||
- 生成 `pixel_values`。
|
||||
- 必要时生成 `pixel_mask`,用于标记 padding 区域。
|
||||
|
||||
输出通常包含:
|
||||
|
||||
```python
|
||||
{
|
||||
"pixel_values": Tensor[batch, 3, H, W],
|
||||
"pixel_mask": Tensor[batch, H, W]
|
||||
}
|
||||
```
|
||||
|
||||
其中:
|
||||
|
||||
- `pixel_values` 是送入模型的图像张量。
|
||||
- `pixel_mask` 用于告诉模型哪些区域是真实图像,哪些区域是 padding。
|
||||
|
||||
这一步不是文本 token 化,不会产生 `input_ids`、`attention_mask` 这类 NLP tokenizer 输出。
|
||||
|
||||
## 第 2 步:ResNet-50 提取特征图
|
||||
|
||||
模型内部首先使用 ResNet-50 backbone 处理图像:
|
||||
|
||||
```text
|
||||
pixel_values: [batch, 3, H, W]
|
||||
↓ ResNet-50
|
||||
feature map: [batch, 2048, H', W']
|
||||
```
|
||||
|
||||
`H'` 和 `W'` 是下采样后的空间尺寸。ResNet 通常会把图像下采样约 32 倍。
|
||||
|
||||
例如输入图像尺寸为:
|
||||
|
||||
```text
|
||||
[batch, 3, 800, 1280]
|
||||
```
|
||||
|
||||
经过 ResNet 后,特征图可能接近:
|
||||
|
||||
```text
|
||||
[batch, 2048, 25, 40]
|
||||
```
|
||||
|
||||
这里的每个空间位置 `(y, x)` 都是一个高层视觉特征向量。
|
||||
|
||||
## 第 3 步:1×1 卷积投影通道
|
||||
|
||||
ResNet 输出通道数通常是 `2048`,而 DETR Transformer 默认隐藏维度通常是 `256`。
|
||||
|
||||
因此模型会使用一个 `1×1 convolution` 做通道投影:
|
||||
|
||||
```text
|
||||
[batch, 2048, H', W']
|
||||
↓ 1×1 conv
|
||||
[batch, 256, H', W']
|
||||
```
|
||||
|
||||
这一步不会改变空间大小,只改变每个空间位置的特征维度。
|
||||
|
||||
可以理解为:
|
||||
|
||||
```text
|
||||
每个网格点的 2048 维向量 → 256 维向量
|
||||
```
|
||||
|
||||
## 第 4 步:flatten 成视觉 token 序列
|
||||
|
||||
这是“视觉 token 化”的核心步骤。
|
||||
|
||||
投影后的特征图形状是:
|
||||
|
||||
```text
|
||||
[batch, 256, H', W']
|
||||
```
|
||||
|
||||
模型会把二维空间维度 `H' × W'` 展开成一维序列:
|
||||
|
||||
```text
|
||||
[batch, 256, H', W']
|
||||
↓ flatten H' 和 W'
|
||||
[batch, 256, H'×W']
|
||||
↓ 调整维度顺序
|
||||
[batch, H'×W', 256]
|
||||
```
|
||||
|
||||
如果特征图是:
|
||||
|
||||
```text
|
||||
[batch, 256, 25, 40]
|
||||
```
|
||||
|
||||
那么 token 数是:
|
||||
|
||||
```text
|
||||
25 × 40 = 1000
|
||||
```
|
||||
|
||||
最终得到:
|
||||
|
||||
```text
|
||||
[batch, 1000, 256]
|
||||
```
|
||||
|
||||
也就是说:
|
||||
|
||||
```text
|
||||
每个特征图网格位置 = 1 个视觉 token
|
||||
每个视觉 token = 1 个 256 维向量
|
||||
```
|
||||
|
||||
伪代码可以写成:
|
||||
|
||||
```python
|
||||
# x: [batch, 256, h, w]
|
||||
x = x.flatten(2) # [batch, 256, h*w]
|
||||
x = x.transpose(1, 2) # [batch, h*w, 256]
|
||||
```
|
||||
|
||||
原始 DETR 论文和部分 PyTorch 实现中也常见 sequence-first 格式:
|
||||
|
||||
```python
|
||||
# x: [batch, 256, h, w]
|
||||
x = x.flatten(2) # [batch, 256, h*w]
|
||||
x = x.permute(2, 0, 1) # [h*w, batch, 256]
|
||||
```
|
||||
|
||||
两种写法本质相同,只是 Transformer 接口期望的维度顺序不同。
|
||||
|
||||
## 第 5 步:加入二维位置编码
|
||||
|
||||
Transformer 本身不理解图像中的二维空间位置。
|
||||
|
||||
flatten 后,模型只看到一串 token:
|
||||
|
||||
```text
|
||||
token_0, token_1, token_2, ..., token_N
|
||||
```
|
||||
|
||||
如果不加入位置编码,模型不知道某个 token 原来位于图像左上角、中心还是右下角。
|
||||
|
||||
因此 DETR 会为特征图每个 `(y, x)` 位置生成二维位置编码:
|
||||
|
||||
```text
|
||||
position encoding: [batch, 256, H', W']
|
||||
```
|
||||
|
||||
然后同样 flatten:
|
||||
|
||||
```text
|
||||
[batch, 256, H', W']
|
||||
↓
|
||||
[batch, H'×W', 256]
|
||||
```
|
||||
|
||||
Transformer Encoder 接收的是:
|
||||
|
||||
```text
|
||||
visual token + positional encoding
|
||||
```
|
||||
|
||||
位置编码让模型知道 token 的空间布局。
|
||||
|
||||
## 第 6 步:Transformer Encoder 处理视觉 token
|
||||
|
||||
经过 flatten 和位置编码后,视觉 token 序列进入 Transformer Encoder:
|
||||
|
||||
```text
|
||||
[batch, H'×W', 256]
|
||||
↓ Transformer Encoder
|
||||
[batch, H'×W', 256]
|
||||
```
|
||||
|
||||
Encoder 会通过 self-attention 建模图像中不同区域之间的关系。
|
||||
|
||||
例如:
|
||||
|
||||
- 车头区域可以关注车身区域。
|
||||
- 道路区域可以影响车辆判断。
|
||||
- 远处小目标可以和周围上下文一起被理解。
|
||||
|
||||
## 第 7 步:Object Queries 和 Transformer Decoder
|
||||
|
||||
DETR 与传统检测器不同,它不是先生成大量 anchor box。
|
||||
|
||||
它使用一组可学习的 object queries。常见数量是:
|
||||
|
||||
```text
|
||||
100 个 object queries
|
||||
```
|
||||
|
||||
这些 queries 进入 Transformer Decoder,并关注 Encoder 输出的视觉 token:
|
||||
|
||||
```text
|
||||
object queries: [batch, 100, 256]
|
||||
encoder tokens: [batch, H'×W', 256]
|
||||
↓ Transformer Decoder
|
||||
object features: [batch, 100, 256]
|
||||
```
|
||||
|
||||
每个 query 最终预测一个候选目标:
|
||||
|
||||
```text
|
||||
类别 + 边界框
|
||||
```
|
||||
|
||||
因此 DETR 输出通常可以理解为:
|
||||
|
||||
```text
|
||||
最多 100 个候选目标
|
||||
```
|
||||
|
||||
每个候选目标会包含:
|
||||
|
||||
- 类别 logits。
|
||||
- 归一化边界框。
|
||||
|
||||
## 第 8 步:后处理成车辆检测结果
|
||||
|
||||
当前代码中的后处理是:
|
||||
|
||||
```python
|
||||
target_sizes = torch.tensor([image.size[::-1]], device=self.device)
|
||||
results = self.processor.post_process_object_detection(
|
||||
outputs,
|
||||
target_sizes=target_sizes,
|
||||
threshold=self.confidence,
|
||||
)[0]
|
||||
```
|
||||
|
||||
这一步会:
|
||||
|
||||
- 把模型输出的归一化框还原为原图坐标。
|
||||
- 根据置信度阈值过滤低分检测。
|
||||
- 返回 `scores`、`labels`、`boxes`。
|
||||
|
||||
然后代码过滤车辆类别:
|
||||
|
||||
```python
|
||||
label_name = self.model.config.id2label[label.item()]
|
||||
if label_name not in self.vehicle_labels:
|
||||
continue
|
||||
```
|
||||
|
||||
默认车辆类别为:
|
||||
|
||||
```text
|
||||
car, motorcycle, bus, truck, bicycle
|
||||
```
|
||||
|
||||
最终输出格式:
|
||||
|
||||
```python
|
||||
{
|
||||
"label": "car",
|
||||
"score": 0.9132,
|
||||
"box": [x1, y1, x2, y2]
|
||||
}
|
||||
```
|
||||
|
||||
## 与文本 tokenizer 的区别
|
||||
|
||||
| 对比项 | 文本 tokenizer | DETR 视觉 token 化 |
|
||||
| --- | --- | --- |
|
||||
| 输入 | 文本字符串 | 图像张量 |
|
||||
| 输出 | token id 序列 | 视觉特征向量序列 |
|
||||
| token 来源 | 词、子词、字符片段 | CNN 特征图空间网格 |
|
||||
| token 内容 | 离散整数 id | 连续浮点向量 |
|
||||
| 位置编码 | 一维位置编码 | 二维图像位置编码 |
|
||||
| 当前代码中的类 | 无 | `DetrImageProcessor` + `DetrForObjectDetection` |
|
||||
|
||||
## 为什么说它是 patch-like features
|
||||
|
||||
它们可以叫做 `patch-like features`,但不是 ViT 那种直接切原图 patch。
|
||||
|
||||
ViT 通常是:
|
||||
|
||||
```text
|
||||
原图 -> 切成 16×16 patch -> 线性投影 -> token
|
||||
```
|
||||
|
||||
DETR-ResNet 是:
|
||||
|
||||
```text
|
||||
原图 -> ResNet 特征图 -> 每个特征图网格点 -> token
|
||||
```
|
||||
|
||||
因此 DETR 的每个 token:
|
||||
|
||||
- 对应特征图上的一个空间位置。
|
||||
- 感受野来自 ResNet 深层网络。
|
||||
- 通常覆盖原图中一片较大的区域。
|
||||
- 相邻 token 的感受野会重叠。
|
||||
|
||||
## 一个具体尺寸例子
|
||||
|
||||
假设预处理后图像大小为:
|
||||
|
||||
```text
|
||||
800 × 1280
|
||||
```
|
||||
|
||||
ResNet-50 下采样约 32 倍:
|
||||
|
||||
```text
|
||||
H' = 800 / 32 ≈ 25
|
||||
W' = 1280 / 32 ≈ 40
|
||||
```
|
||||
|
||||
Backbone 输出:
|
||||
|
||||
```text
|
||||
[batch, 2048, 25, 40]
|
||||
```
|
||||
|
||||
1×1 卷积投影:
|
||||
|
||||
```text
|
||||
[batch, 256, 25, 40]
|
||||
```
|
||||
|
||||
flatten:
|
||||
|
||||
```text
|
||||
[batch, 1000, 256]
|
||||
```
|
||||
|
||||
所以该图像大约会生成:
|
||||
|
||||
```text
|
||||
1000 个视觉 token
|
||||
```
|
||||
|
||||
每个 token 是:
|
||||
|
||||
```text
|
||||
256 维连续向量
|
||||
```
|
||||
|
||||
## 当前项目中的实际检测路径
|
||||
|
||||
当前项目的整体路径是:
|
||||
|
||||
```text
|
||||
RTSP/HLS 视频帧
|
||||
↓
|
||||
OpenCV 读取 BGR frame
|
||||
↓
|
||||
cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
||||
↓
|
||||
Image.fromarray(frame_rgb)
|
||||
↓
|
||||
DetrImageProcessor 图像预处理
|
||||
↓
|
||||
DetrForObjectDetection 内部完成视觉 token 化和目标检测
|
||||
↓
|
||||
post_process_object_detection
|
||||
↓
|
||||
过滤车辆类别
|
||||
↓
|
||||
OpenCV OSD 画框
|
||||
↓
|
||||
FastAPI /video 输出动态打标画面
|
||||
```
|
||||
|
||||
对应代码位置:
|
||||
|
||||
- `app/detector.py`:DETR 图像预处理、模型推理、后处理。
|
||||
- `app/stream_worker.py`:视频帧读取、推理调用、OSD 画框。
|
||||
- `app/main.py`:MJPEG 视频流和检测结果接口。
|
||||
|
||||
## 小结
|
||||
|
||||
当前项目中的 DETR 不包含文本 tokenizer。
|
||||
|
||||
它的视觉 token 化具体是:
|
||||
|
||||
```text
|
||||
图像经过 ResNet-50 得到二维特征图
|
||||
↓
|
||||
1×1 卷积把通道投影到 Transformer hidden size
|
||||
↓
|
||||
把 H' × W' 个空间位置 flatten 成 H'×W' 个视觉 token
|
||||
↓
|
||||
给每个 token 加二维位置编码
|
||||
↓
|
||||
送入 Transformer Encoder
|
||||
```
|
||||
|
||||
因此,“token 数”主要由输入分辨率和 backbone 下采样比例决定:
|
||||
|
||||
```text
|
||||
视觉 token 数 ≈ (输入高度 / 32) × (输入宽度 / 32)
|
||||
```
|
||||
|
||||
实际数量会受到图像预处理 resize、padding 和特征图尺寸取整影响。
|
||||
Reference in New Issue
Block a user