🎯 计算机视觉面试题库¶
⚠️ 时效性说明:本章涉及前沿模型/价格/榜单等信息,可能随版本快速变化;请以论文原文、官方发布页和 API 文档为准。
覆盖CNN基础、检测/分割、Transformer、多模态、模型部署等高频面试题,每题含详细解答与代码示例。
📐 一、卷积与基础计算¶
Q1:卷积层输出尺寸如何计算?¶
标准公式:
- \(W\):输入尺寸,\(K\):卷积核大小,\(P\):padding,\(S\):stride
示例计算:
| 输入 | 卷积核 | Padding | Stride | 输出 |
|---|---|---|---|---|
| 224 | 7 | 3 | 2 | 112 |
| 112 | 3 | 1 | 1 | 112 |
| 56 | 3 | 1 | 2 | 28 |
def conv_output_size(w, k, p, s):
"""计算卷积输出尺寸"""
return (w - k + 2 * p) // s + 1
# ResNet第一层: 224 -> 112
print(conv_output_size(224, 7, 3, 2)) # 112
面试官追问:转置卷积(反卷积)的输出尺寸公式?
Q2:感受野(Receptive Field)如何计算?¶
递推公式:
def compute_receptive_field(layers):
"""
layers: list of (kernel_size, stride)
"""
rf = 1
stride_prod = 1
for k, s in layers:
rf = rf + (k - 1) * stride_prod
stride_prod *= s
return rf
# VGG16前3个卷积块
layers = [(3,1), (3,1), (2,2), (3,1), (3,1), (2,2), (3,1), (3,1), (3,1), (2,2)]
print(f"感受野: {compute_receptive_field(layers)}") # 44
关键结论: - 堆叠小卷积核(3×3)的感受野等价于大卷积核,但参数更少 - 两个3×3卷积 = 一个5×5感受野,参数量 \(2 \times 3^2 = 18\) vs \(5^2 = 25\) - 空洞卷积(Dilated Conv)可以不增加参数地扩大感受野
Q3:1×1卷积的作用是什么?¶
三大核心作用:
- 通道降维/升维:调整特征通道数,减少计算量
- 跨通道信息交互:对同一位置不同通道做线性组合
- 增加非线性:配合激活函数增强表达能力
import torch.nn as nn
# NiN中的1x1卷积: 256通道 -> 64通道
bottleneck = nn.Sequential(
nn.Conv2d(256, 64, kernel_size=1), # 参数: 256*64 = 16384
nn.BatchNorm2d(64),
nn.ReLU(inplace=True),
nn.Conv2d(64, 64, kernel_size=3, padding=1), # 参数: 64*64*9 = 36864
nn.BatchNorm2d(64),
nn.ReLU(inplace=True),
nn.Conv2d(64, 256, kernel_size=1), # 参数: 64*256 = 16384
)
# 总参数: 69632 vs 直接3x3: 256*256*9 = 589824 (节省88%)
Q4:深度可分离卷积(Depthwise Separable Conv)原理?¶
两步分解:
- Depthwise Conv:每个通道单独卷积,\(C_{in}\) 个 \(K \times K \times 1\) 卷积核
- Pointwise Conv:\(C_{out}\) 个 \(1 \times 1 \times C_{in}\) 卷积核
参数量对比:
| 类型 | 参数量 | 以3×3, 64→128为例 |
|---|---|---|
| 标准卷积 | \(K^2 \cdot C_{in} \cdot C_{out}\) | 73,728 |
| 深度可分离 | \(K^2 \cdot C_{in} + C_{in} \cdot C_{out}\) | 8,768 |
| 压缩比 | \(\approx \frac{1}{C_{out}} + \frac{1}{K^2}\) | 8.4× |
class DepthwiseSeparableConv(nn.Module): # 继承nn.Module定义网络层
def __init__(self, in_ch, out_ch, kernel_size=3, stride=1, padding=1):
super().__init__() # super()调用父类方法
self.depthwise = nn.Conv2d(in_ch, in_ch, kernel_size, stride, padding, groups=in_ch)
self.pointwise = nn.Conv2d(in_ch, out_ch, 1)
def forward(self, x):
return self.pointwise(self.depthwise(x))
🏗️ 二、经典网络架构¶
Q5:ResNet为什么有效?Skip Connection的原理?¶
核心答案:
- 缓解梯度消失:梯度可以通过shortcut直接回传,保证深层网络可训练
- 恒等映射学习:网络只需学残差 \(F(x) = H(x) - x\),趋近于0比学恒等映射更容易
- 集成学习效应:ResNet可视为多条不同深度路径的隐式集成
数学推导:
因为包含 \(1\),梯度不会消失为 \(0\)。
class BasicBlock(nn.Module):
def __init__(self, in_planes, planes, stride=1):
super().__init__()
self.conv1 = nn.Conv2d(in_planes, planes, 3, stride, 1, bias=False)
self.bn1 = nn.BatchNorm2d(planes)
self.conv2 = nn.Conv2d(planes, planes, 3, 1, 1, bias=False)
self.bn2 = nn.BatchNorm2d(planes)
self.shortcut = nn.Sequential()
if stride != 1 or in_planes != planes:
self.shortcut = nn.Sequential(
nn.Conv2d(in_planes, planes, 1, stride, bias=False),
nn.BatchNorm2d(planes)
)
def forward(self, x):
out = F.relu(self.bn1(self.conv1(x))) # F.xxx PyTorch函数式API
out = self.bn2(self.conv2(out))
out += self.shortcut(x) # Skip Connection
return F.relu(out)
面试官追问:Pre-Activation ResNet vs 标准ResNet的区别? - Pre-Act: BN→ReLU→Conv,最后一层输出不经过ReLU,信息更通畅
Q6:BatchNorm在训练和推理时有什么区别?¶
训练时: - 使用当前mini-batch的均值 \(\mu_B\) 和方差 \(\sigma_B^2\) - 维护全局running mean和running var(指数移动平均)
推理时: - 使用训练积累的running mean和running var - BN层可融入卷积层,零额外开销
class BatchNorm2d(nn.Module):
def __init__(self, num_features, eps=1e-5, momentum=0.1):
super().__init__()
self.gamma = nn.Parameter(torch.ones(num_features))
self.beta = nn.Parameter(torch.zeros(num_features))
self.register_buffer('running_mean', torch.zeros(num_features))
self.register_buffer('running_var', torch.ones(num_features))
self.eps = eps
self.momentum = momentum
def forward(self, x):
if self.training:
mean = x.mean(dim=(0, 2, 3)) # 对N,H,W维度求均值
var = x.var(dim=(0, 2, 3), unbiased=False)
# 更新running统计量
self.running_mean = (1 - self.momentum) * self.running_mean + self.momentum * mean
self.running_var = (1 - self.momentum) * self.running_var + self.momentum * var
else:
mean = self.running_mean
var = self.running_var
x_norm = (x - mean[None,:,None,None]) / torch.sqrt(var[None,:,None,None] + self.eps)
return self.gamma[None,:,None,None] * x_norm + self.beta[None,:,None,None]
BN融合到卷积(推理加速):
面试官追问:BN在batch_size=1时失效怎么办? - 使用 GroupNorm / LayerNorm / InstanceNorm 替代
Q7:各种Normalization方法对比?¶
| 方法 | 归一化维度 | 适用场景 | batch依赖 |
|---|---|---|---|
| BatchNorm | (N, H, W) | CNN主流 | ✅ |
| LayerNorm | (C, H, W) | Transformer/RNN | ❌ |
| InstanceNorm | (H, W) | 风格迁移 | ❌ |
| GroupNorm | (C//G, H, W) | 小batch CNN | ❌ |
🔍 三、目标检测¶
Q8:Anchor-based vs Anchor-free检测器的区别?¶
| 特性 | Anchor-based | Anchor-free |
|---|---|---|
| 代表模型 | Faster R-CNN, YOLOv3-v5 | FCOS, CenterNet, YOLOv8 |
| 先验框 | 需预定义anchor尺寸/比例 | 不需要 |
| 正负样本 | IoU阈值分配 | 几何关系分配 |
| 超参数 | 多(anchor尺寸/比例/数量) | 少 |
| 回归目标 | 相对anchor的偏移 | 点到边界的距离 |
| 泛化性 | 受anchor设计影响 | 通常更好 |
FCOS的核心思想:
# Anchor-free: 直接回归每个点到4个边界的距离
# 对于特征图上每个位置(x, y), 回归 (l, t, r, b)
def fcos_target(gt_boxes, feature_points):
"""
gt_boxes: (N, 4) -> (x1, y1, x2, y2)
feature_points: (H*W, 2) -> (cx, cy)
"""
l = feature_points[:, 0:1] - gt_boxes[:, 0:1] # 到左边界
t = feature_points[:, 1:2] - gt_boxes[:, 1:2] # 到上边界
r = gt_boxes[:, 2:3] - feature_points[:, 0:1] # 到右边界
b = gt_boxes[:, 3:4] - feature_points[:, 1:2] # 到下边界
return torch.stack([l, t, r, b], dim=-1) # torch.stack沿新维度拼接张量
Q9:NMS及其变体(Soft-NMS, DIoU-NMS)原理?¶
标准NMS流程:
def nms(boxes, scores, iou_threshold=0.5):
"""标准NMS"""
order = scores.argsort(descending=True)
keep = []
while order.numel() > 0:
i = order[0].item() # 将单元素张量转为Python数值
keep.append(i)
if order.numel() == 1:
break
ious = compute_iou(boxes[i].unsqueeze(0), boxes[order[1:]]) # unsqueeze增加一个维度
mask = ious.squeeze() <= iou_threshold # squeeze压缩维度
order = order[1:][mask]
return keep
Soft-NMS:不直接删除,而是衰减分数:
def soft_nms(boxes, scores, sigma=0.5, score_threshold=0.001):
"""Soft-NMS Gaussian"""
keep = []
while scores.max() > score_threshold:
i = scores.argmax().item()
keep.append(i)
ious = compute_iou(boxes[i].unsqueeze(0), boxes)
decay = torch.exp(-(ious ** 2) / sigma)
scores = scores * decay.squeeze()
scores[i] = 0
return keep
DIoU-NMS:用DIoU替代IoU,考虑中心距离:
优点:密集遮挡场景下保留效果更好。
面试官追问:NMS的时间复杂度?如何加速? - 标准NMS: \(O(N^2)\),可用CUDA并行加速或按类别并行处理
Q10:IoU及其变体(GIoU, DIoU, CIoU)的区别?¶
| 指标 | 公式核心 | 解决的问题 |
|---|---|---|
| IoU | \(\frac{\lvert A \cap B\rvert}{\lvert A \cup B\rvert}\) | 基础重叠度 |
| GIoU | \(IoU - \frac{\lvert C \setminus (A \cup B)\rvert}{\lvert C\rvert}\) | 无重叠时梯度为0 |
| DIoU | \(IoU - \frac{d^2}{c^2}\) | 中心距离 |
| CIoU | \(DIoU - \alpha v\) | 长宽比一致性 |
其中 \(v = \frac{4}{\pi^2}(\arctan\frac{w^{gt}}{h^{gt}} - \arctan\frac{w}{h})^2\)
Q11:FPN(特征金字塔网络)的设计思想?¶
核心思想:自顶向下路径 + 横向连接,融合多尺度特征
C5 (1/32) → P5 ←→ 检测大目标
↓ 上采样+横向连接
C4 (1/16) → P4 ←→ 检测中目标
↓
C3 (1/8) → P3 ←→ 检测小目标
↓
C2 (1/4) → P2 ←→ 检测更小目标
class FPN(nn.Module):
def __init__(self, in_channels_list, out_channels=256):
super().__init__()
self.lateral_convs = nn.ModuleList([
nn.Conv2d(c, out_channels, 1) for c in in_channels_list
])
self.output_convs = nn.ModuleList([
nn.Conv2d(out_channels, out_channels, 3, padding=1)
for _ in in_channels_list
])
def forward(self, features): # [C2, C3, C4, C5]
laterals = [conv(f) for conv, f in zip(self.lateral_convs, features)] # zip按位置配对
# 自顶向下融合
for i in range(len(laterals) - 2, -1, -1):
laterals[i] += F.interpolate(laterals[i + 1], scale_factor=2, mode='nearest')
return [conv(lat) for conv, lat in zip(self.output_convs, laterals)]
🤖 四、Transformer在CV中的应用¶
Q12:ViT(Vision Transformer)的核心设计?¶
关键步骤:
- 图像分patch:\(224 \times 224\) 切成 \(16 \times 16\) 的patch → \(14 \times 14 = 196\) 个token
- Patch Embedding:线性投影到D维
- 加CLS token + 位置编码
- 标准Transformer Encoder
class ViT(nn.Module):
def __init__(self, img_size=224, patch_size=16, dim=768, depth=12, heads=12, num_classes=1000):
super().__init__()
num_patches = (img_size // patch_size) ** 2 # 196
patch_dim = 3 * patch_size ** 2 # 768
self.patch_embed = nn.Sequential(
nn.Conv2d(3, dim, kernel_size=patch_size, stride=patch_size), # 等价于线性投影
)
self.cls_token = nn.Parameter(torch.randn(1, 1, dim))
self.pos_embed = nn.Parameter(torch.randn(1, num_patches + 1, dim))
self.transformer = nn.TransformerEncoder(
nn.TransformerEncoderLayer(d_model=dim, nhead=heads, dim_feedforward=dim*4, batch_first=True),
num_layers=depth
)
self.head = nn.Linear(dim, num_classes)
def forward(self, x):
B = x.shape[0]
x = self.patch_embed(x).flatten(2).transpose(1, 2) # (B, 196, 768)
cls = self.cls_token.expand(B, -1, -1)
x = torch.cat([cls, x], dim=1) # (B, 197, 768) # torch.cat沿已有维度拼接张量
x = x + self.pos_embed
x = self.transformer(x)
return self.head(x[:, 0]) # CLS token分类
面试官追问:ViT的计算复杂度?
对于224图像+16patch: \(N = 196\),可接受。对于高分辨率需要Swin Transformer的窗口注意力。
Q13:DETR的核心创新是什么?¶
核心创新: 1. 端到端检测:无需NMS、无需Anchor、无需手工后处理 2. 集合预测:用二分图匹配(Hungarian算法)做标签分配 3. Object Queries:可学习的检测query替代anchor
匈牙利匹配损失:
面试官追问:DETR的缺点? - 训练收敛慢(500 epochs vs FRCNN 12 epochs) - 小目标检测效果差 → 后续 Deformable DETR 改进
Q14:SAM(Segment Anything)的架构设计?¶
三大组件:
| 组件 | 功能 | 技术 |
|---|---|---|
| Image Encoder | 图像特征提取 | MAE预训练的ViT-H |
| Prompt Encoder | 编码prompt | 点/框/文本/掩码 |
| Mask Decoder | 生成分割掩码 | 双向Cross-Attention |
关键设计: - Promptable分割:支持点、框、文本等多种prompt - 一次提取图像特征,多次解码不同prompt → 实时交互 - 训练数据:SA-1B数据集,11M图像+1B掩码
🌐 五、多模态模型¶
Q15:CLIP的训练方式和原理?¶
对比学习框架:
InfoNCE损失(对称):
def clip_loss(image_features, text_features, temperature=0.07):
"""CLIP对比学习损失"""
# 归一化
image_features = F.normalize(image_features, dim=-1)
text_features = F.normalize(text_features, dim=-1)
# 计算相似度矩阵 (B, B)
logits = image_features @ text_features.T / temperature
# 对称交叉熵
labels = torch.arange(len(logits), device=logits.device)
loss_i2t = F.cross_entropy(logits, labels) # F.cross_entropy PyTorch函数式交叉熵损失
loss_t2i = F.cross_entropy(logits.T, labels)
return (loss_i2t + loss_t2i) / 2
面试官追问:CLIP的zero-shot分类怎么做? - 构造文本 "a photo of a {class}" → 计算图文相似度 → 选最大值
Q16:GPT-4V等多模态大模型的架构思路?¶
主流架构模式:
- 视觉编码器 + 投影层 + LLM(LLaVA风格)
-
ViT提取视觉token → MLP投影 → 拼接到文本token
-
交叉注意力融合(Flamingo风格)
-
在LLM层间插入Cross-Attention层
-
统一tokenizer(将图像离散化为token)
关键面试题:视觉token如何与文本token对齐?
class VisionLanguageModel(nn.Module):
def __init__(self, vision_encoder, llm, projection_dim):
super().__init__()
self.vision_encoder = vision_encoder # CLIP ViT
self.projector = nn.Linear(vision_encoder.hidden_dim, llm.hidden_dim)
self.llm = llm
def forward(self, images, text_tokens):
# 视觉特征 → 投影到LLM空间
vis_features = self.vision_encoder(images) # (B, N_vis, D_vis)
vis_tokens = self.projector(vis_features) # (B, N_vis, D_llm)
# 拼接视觉token和文本token
text_embeds = self.llm.embed_tokens(text_tokens)
inputs = torch.cat([vis_tokens, text_embeds], dim=1)
return self.llm(inputs_embeds=inputs)
⚡ 六、模型部署与优化¶
Q17:模型量化(Quantization)的原理和方法?¶
量化类型:
| 类型 | 时机 | 精度 | 适用场景 |
|---|---|---|---|
| PTQ(训练后量化) | 训练后 | 略有损失 | 快速部署 |
| QAT(量化感知训练) | 训练时模拟 | 几乎无损 | 精度敏感 |
| 动态量化 | 推理时 | 中等 | 权重量化为主 |
INT8量化公式:
import torch.quantization as quant
# 训练后静态量化 (PTQ)
model.eval() # eval()评估模式
model.qconfig = quant.get_default_qconfig('x86')
model_prepared = quant.prepare(model)
# 用校准数据集收集统计量
with torch.no_grad(): # 禁用梯度计算,节省内存
for data in calibration_loader:
model_prepared(data)
model_quantized = quant.convert(model_prepared)
# 模型大小减少约4倍,推理速度提升2-4倍
面试官追问:量化对不同层的影响? - 第一层和最后一层对量化最敏感,通常保持FP16 - 深层特征量化鲁棒性更好
Q18:ONNX导出和TensorRT部署流程?¶
import torch
import onnxruntime as ort
# 1. PyTorch → ONNX
dummy_input = torch.randn(1, 3, 224, 224)
torch.onnx.export(
model, dummy_input, "model.onnx",
input_names=["input"],
output_names=["output"],
dynamic_axes={"input": {0: "batch"}, "output": {0: "batch"}},
opset_version=17
)
# 2. ONNX Runtime 推理
session = ort.InferenceSession("model.onnx", providers=['CUDAExecutionProvider'])
result = session.run(None, {"input": input_numpy})
# 3. TensorRT 优化(命令行)
# trtexec --onnx=model.onnx --saveEngine=model.trt --fp16 --workspace=4096
部署性能对比(ResNet50, batch=1, V100):
| 框架 | 延迟 | 吞吐量 |
|---|---|---|
| PyTorch FP32 | 8.2ms | 122 fps |
| ONNX Runtime | 4.1ms | 243 fps |
| TensorRT FP16 | 1.5ms | 667 fps |
| TensorRT INT8 | 0.9ms | 1111 fps |
Q19:知识蒸馏(Knowledge Distillation)原理?¶
核心思想: 用大模型(Teacher)的soft label指导小模型(Student)训练。
其中 \(p^T = \text{softmax}(z / T)\),\(T\) 为温度系数。
def distillation_loss(student_logits, teacher_logits, labels, T=4.0, alpha=0.5):
"""知识蒸馏损失"""
# Hard label loss
hard_loss = F.cross_entropy(student_logits, labels)
# Soft label loss (KL散度)
soft_student = F.log_softmax(student_logits / T, dim=-1)
soft_teacher = F.softmax(teacher_logits / T, dim=-1)
soft_loss = F.kl_div(soft_student, soft_teacher, reduction='batchmean') * (T ** 2)
return alpha * hard_loss + (1 - alpha) * soft_loss
🧩 七、数据增强与训练技巧¶
Q20:常用数据增强方法及其原理?¶
几何变换类: - 随机翻转、旋转、裁剪、缩放 - 仿射变换、透视变换
像素变换类: - 颜色抖动(Color Jitter) - 随机擦除(Random Erasing) - Cutout、CutMix、MixUp
高级增强:
import torchvision.transforms as T
# MixUp
def mixup(x, y, alpha=0.2):
lam = np.random.beta(alpha, alpha)
idx = torch.randperm(x.size(0))
mixed_x = lam * x + (1 - lam) * x[idx]
y_a, y_b = y, y[idx]
return mixed_x, y_a, y_b, lam
# CutMix
def cutmix(x, y, alpha=1.0):
lam = np.random.beta(alpha, alpha)
B, _, H, W = x.shape
cut_h = int(H * np.sqrt(1 - lam))
cut_w = int(W * np.sqrt(1 - lam))
cx, cy = np.random.randint(W), np.random.randint(H)
x1 = np.clip(cx - cut_w // 2, 0, W)
y1 = np.clip(cy - cut_h // 2, 0, H)
x2 = np.clip(cx + cut_w // 2, 0, W)
y2 = np.clip(cy + cut_h // 2, 0, H)
idx = torch.randperm(B)
x[:, :, y1:y2, x1:x2] = x[idx, :, y1:y2, x1:x2]
lam = 1 - (y2-y1)*(x2-x1) / (H*W)
return x, y, y[idx], lam
面试官追问:MixUp和CutMix哪个效果好? - CutMix通常更好,因为保留了局部空间信息,且对遮挡有鲁棒性
Q21:混合精度训练(AMP)原理?¶
from torch.amp import autocast, GradScaler
scaler = GradScaler()
for data, target in dataloader:
optimizer.zero_grad() # 清零梯度
with autocast('cuda'): # FP16前向传播
output = model(data)
loss = criterion(output, target)
scaler.scale(loss).backward() # 缩放梯度防止下溢
scaler.step(optimizer)
scaler.update()
关键点: - 前向用FP16(减少显存+加速计算),反向梯度FP32(保持精度) - Loss Scaling防止FP16梯度下溢 - 显存节省约50%,速度提升约2倍
🔬 八、经典问题速答¶
Q22:为什么CNN鲁棒性不如Transformer?¶
- CNN归纳偏置(局部性+平移不变性)在小数据上有利,但限制了全局建模
- Transformer通过全局注意力学习更robust的特征
- 但需要更多数据/预训练
Q23:过拟合和欠拟合的判别与解决?¶
| 问题 | 表现 | 解决方案 |
|---|---|---|
| 过拟合 | 训练acc高,验证acc低 | 正则化/Dropout/数据增强/Early Stop |
| 欠拟合 | 训练acc低 | 增加模型容量/减少正则化/更多训练 |
Q24:目标检测中正负样本不平衡怎么办?¶
- Focal Loss:\(FL = -\alpha_t (1-p_t)^\gamma \log(p_t)\),抑制易分类样本
- OHEM:在线困难样本挖掘
- 正负样本比例控制:如1:3
Q25:mAP(mean Average Precision)如何计算?¶
- 每类按置信度排序 → 计算 Precision-Recall 曲线
- AP = PR曲线下面积(11点插值或所有点插值)
- mAP = 所有类别AP的平均
📝 面试备考清单¶
- 卷积输出尺寸 / 感受野计算(手写)
- ResNet / BatchNorm 原理(深度理解)
- Anchor-based vs Anchor-free(对比分析)
- NMS及变体(代码实现)
- IoU / GIoU / DIoU / CIoU(公式推导)
- ViT / DETR / SAM(架构和创新点)
- CLIP / 多模态模型(对比学习+融合)
- 量化 / 蒸馏 / 部署(工程实践)
- 数据增强 / 混合精度(训练技巧)
- mAP / FPS / FLOPs(评估指标)
💡 高频出题公司参考:字节跳动(视觉大模型/多模态)、商汤/旷视(检测分割)、大疆(部署优化)、腾讯(视频理解)