PyTorch实现Faster R-CNN:Anchor、RPN与ROI Align原理及调参避坑
简介一份基于PyTorch的Faster R-CNN完整实现面向Python开发者、机器学习初学者及目标检测研究人员解决从区域提议到分类定位的端到端模型搭建问题。压缩包共89个文件以62个Python脚本为主涵盖模型定义、数据预处理、训练与评估等完整流程另有C/CUDA扩展用于加速ROI池化与NMS以及yml环境配置、Markdown说明等整体约475KB目录结构清晰便于按模块阅读与二次开发。已有4586人学习下载适合想深入理解RPN与检测网络协同机制并快速上手PyTorch工程实践的读者。资源不仅给出完整的训练与测试主流程还包含参数配置、网络结构、工具集及数据加载模块可据此调整anchor尺度、学习率与训练轮数在VOC等数据集上复现并改进目标检测精度。同时代码中的损失函数与训练循环展示了Faster R-CNN的优化细节有助于后续迁移到其他检测模型。对于希望掌握区域提议网络、RoI池化等核心原理的读者这份资源提供了可直接运行的参考实现。1. 用 PyTorch 实现 Faster R-CNN两阶段检测器的完整落地路径Faster R-CNN 是目标检测里最经典的两阶段模型。PyTorch 里虽然有一行代码就能调用的预训练接口但真正自己实现一遍才能理解 Anchor 生成、RPN 建议框筛选、ROI Align 特征归一化为什么这样设计。很多开发者拿预训练模型跑公开数据集顺手一换到自己的小数据集就失控多半是没搞清楚哪些参数要跟着数据分布改。下面从机制拆解讲起给出完整的 PyTorch 实现路径包括数据加载、模型构建、训练循环和关键参数设置最后用专门章节列出高频踩坑点与排查思路。适合正在上手目标检测、想摆脱调用黑匣子的开发者也适合被精度问题卡住的工程师。我自己就是沿着这条路径把一个小规模数据集上的 mAP 从 0.41 提到 0.68。2. Faster R-CNN 的核心机制拆解RPN、Anchors 与 ROI 头怎么协同2.1 Anchors 机制为什么一张图上要铺几万个候选框Faster R-CNN 的起点是 Anchor。所谓 Anchor就是预先定义好的固定大小矩形框以特征图每个位置为中心铺满整张图。常见配置是每个位置 3 种尺度128、256、512乘 3 种长宽比0.5、1.0、2.0一共 9 个 Anchor。输入图缩放到 800×600ResNet 骨干 stride 16 下采样后特征图是 50×37这张图上的 Anchor 总数约 16650 个。第一次接触的人通常会问为什么要这么暴力因为目标检测要同时回答「哪里有目标」和「目标是什么」两个问题Anchor 就是用密集网格枚举目标可能出现的位置和尺寸再让模型逐个判断置信度并回归精确偏移。这个设计的历史意义在于它把候选区域生成也变成了网络里可学习的一部分。在它之前候选区域要么靠滑动窗口要么靠传统图像算法都是与分类网络割裂的。Anchor 机制让「生成候选」和「分类回归」共享同一套特征训练时一次反向传播同时更新两边的权重这是两阶段检测器能端到端训练的基础。写代码时Anchor 生成是最容易出错的地方。最常见的错误是把 Anchor 直接写在原始图像坐标系里忘了特征图到原图还有一层映射关系。我习惯用向量化方式一次性生成 9 个基准 Anchor再通过特征图坐标做平移避免手写多层 for 循环import torch def generate_base_anchors(base_size16, ratios[0.5, 1.0, 2.0], scales[8, 16, 32]): 生成以原点为中心的 9 个基准 Anchor。 base_size: 特征图一个点对应的输入图步长通常等于骨干网络总 stride。 ratios: 宽高比scales: 相对 base_size 的尺度倍数。 base torch.tensor([1, 1, base_size, base_size], dtypetorch.float32) ws, hs [], [] for ratio in ratios: area base_size * base_size w torch.sqrt(area / ratio) h ratio * w ws.append(w) hs.append(h) ws torch.stack(ws) hs torch.stack(hs) scales_t torch.tensor(scales, dtypetorch.float32) ws (ws[:, None] * scales_t[None, :]).flatten() hs (hs[:, None] * scales_t[None, :]).flatten() # 输出坐标形式是 (x1, y1, x2, y2)中心在 (0, 0) return torch.stack([-ws/2, -hs/2, ws/2, hs/2], dim1)这段代码先按长宽比生成三种尺寸再乘上三种尺度得到 9 个以原点为中心的矩形框。注意输出的坐标是相对偏移量真正使用时要把特征图位置 (i, j) 映射回原图像素中心 (j × stride, i × stride)再加上这些偏移。我早期漏了平移这一步所有 Anchor 都堆在图像左上角训练时 RPN 的 loss 确实在降但检出来的框整体错位后来打印出前 20 个 Anchor 的坐标才定位到问题。提示调试 Anchor 时最直接的办法是把生成的 Anchor 叠加到原图上可视化确认覆盖范围和目标尺寸匹配再继续往下调网络。2.2 RPN 网络区域建议的生成与筛选逻辑RPN 是 Faster R-CNN 里负责「找候选框」的组件接在共享骨干网络之后。结构并不复杂先做一层 3×3 卷积聚合邻域特征然后分出两个 1×1 卷积分支。一个分支输出每个 Anchor 的前景背景得分共 2k 个值另一个分支输出 4k 个坐标回归偏移量k 在这里是每个位置的 Anchor 数通常为 9。RPN 的训练标签规则直接决定建议框质量与任意真实框 IoU 大于 0.7 的 Anchor 标为正样本与所有真实框 IoU 都小于 0.3 的标为负样本介于两者之间的丢弃不参与损失计算。这里有个容易被忽略的细节当某个真实框在整张图上找不到任何 IoU 超过 0.7 的 Anchor 时比如小目标或极端长宽比的目标要从剩余 Anchor 里挑 IoU 最高的那个强行标为正样本否则这个真实框在整个训练过程中永远不会被 RPN 看到。def match_anchors_to_gt(anchors, gt_boxes, pos_iou0.7, neg_iou0.3): 为每个 Anchor 分配训练标签。 anchors: [N, 4]gt_boxes: [M, 4]均为 (x1, y1, x2, y2)。 返回 labels: 1 前景, 0 背景, -1 忽略以及最佳匹配的真实框索引。 ious compute_iou(anchors, gt_boxes) # [N, M] max_iou, argmax_iou ious.max(dim1) labels torch.full((len(anchors),), -1, dtypetorch.long) labels[max_iou neg_iou] 0 labels[max_iou pos_iou] 1 # 兜底每个真实框至少要有一个正样本 Anchor否则该目标学不到 for gt_idx in range(len(gt_boxes)): best ious[:, gt_idx].argmax() labels[best] 1 return labels, argmax_iou其中 compute_iou 是批量计算的交并比函数用矩阵运算实现输入两个坐标张量输出 IoU 矩阵。匹配完成后还要做样本平衡我一般每张图固定采样 256 个 Anchor 参与 RPN 损失计算正负比例约 1:1正样本不够就用负样本补齐。如果某张图里目标极少正样本只有十几个硬凑 1:1 会引入大量低质量负样本这种情况我会把比例放宽到 1:2 或 1:3训练更稳。RPN 前向输出几千个建议框后要过一道 NMS 粗筛。训练阶段按得分取前 2000 个推理阶段取前 300 个这个数量直接影响后续 ROI 头的输入分布不能随手乱改。2.3 ROI Align 与分类回归头从特征图到最终检测结果RPN 给出的建议框大小不一而分类头要求固定尺寸输入中间需要一层特征归一化。最早论文用的是 ROI Pooling把每个建议框分成 7×7 网格做最大池化。问题在于网格边界很少落在整数坐标上取整会引入量化误差对小目标很不友好。后来的 ROI Align 用双线性插值采样解决这个问题PyTorch 里直接用 torchvision.ops.roi_align 就行。from torchvision.ops import roi_align def pool_roi_features(feature_map, proposals, output_size7, stride16): 把不同尺寸的建议框从特征图上抠出来统一成 7x7。 feature_map: [B, C, H, W]proposals: [N, 5]第 1 列是 batch 索引。 stride 用于把原图坐标映射到特征图坐标。 boxes proposals.clone() boxes[:, 1:] boxes[:, 1:] / stride # 原图坐标 - 特征图坐标 pooled roi_align(feature_map, boxes, output_size(output_size, output_size)) return pooled注意 roi_align 的第一个坐标列是 batch 索引后面四列是框坐标。这里我直接把原图坐标除以 stride等价于设置 spatial_scale1/stride两种写法都可以。ROI Align 的输出是 [N, C, 7, 7]展平后接两个全连接层一个是 K1 类分类一个是 4×(K1) 的框回归。第二阶段的标签匹配规则和 RPN 不同建议框与真实框 IoU 大于 0.5 为正样本大于等于 0.1 且小于 0.5 的作为负样本参与难样本挖掘每张图采样 512 个 ROI。整个模型的损失包括 RPN 分类、RPN 回归、ROI 分类、ROI 回归四部分。我习惯在日志里分开打印这四项因为它们的下降节奏完全不同RPN 的损失前几十步会快速下降ROI 头要慢得多。如果某项长时间不动排查范围能立刻缩小到对应环节。3. 从零搭建训练链路数据加载、模型构建与训练循环3.1 数据准备把标注转成 PyTorch 能吃的张量Faster R-CNN 的训练输入和普通分类网络有个关键区别除了图像本身还要给模型一组标注框和类别。PyTorch 检测模型接受的 targets 是一个列表每个元素是包含 boxes 和 labels 的字典。常见的 VOC 格式标注是 XML 文件第一步就是把它解析成张量。import xml.etree.ElementTree as ET import torch def parse_voc_xml(xml_path, class_to_idx): 解析单张 VOC XML返回 boxes 和 labels。 class_to_idx 是从类别名到整数索引的映射背景类 0 留空。 root ET.parse(xml_path).getroot() boxes, labels [], [] for obj in root.findall(object): name obj.find(name).text if name not in class_to_idx: continue bnd obj.find(bndbox) x1 float(bnd.find(xmin).text) y1 float(bnd.find(ymin).text) x2 float(bnd.find(xmax).text) y2 float(bnd.find(ymax).text) boxes.append([x1, y1, x2, y2]) labels.append(class_to_idx[name]) if not boxes: return None return { boxes: torch.tensor(boxes, dtypetorch.float32), labels: torch.tensor(labels, dtypetorch.int64), }解析本身不难真正的坑在后面的数据增强。训练时通常要随机翻转、缩放图像这些几何变换必须对标注框同步做否则框就是错位的。torchvision 提供了支持 box 的变换我一般自己写一个组合对象把图像和 boxes 一起变换。class BoxResize: 把图像统一缩放到指定尺寸并同步缩放标注框。 def __init__(self, size(800, 600)): self.size size def __call__(self, image, target): h_old, w_old image.shape[-2:] new_h, new_w self.size scale_y, scale_x new_h / h_old, new_w / w_old image torch.nn.functional.interpolate( image[None], sizeself.size, modebilinear, align_cornersFalse)[0] boxes target[boxes].clone() boxes[:, [0, 2]] * scale_x boxes[:, [1, 3]] * scale_y target[boxes] boxes return image, target这里直接把图像 resize 到目标尺寸是偷懒做法如果原始图像宽高比和 800×600 不一致会把目标拉伸变形。更稳妥的做法是保持宽高比缩放后做 padding再把 padding 的偏移量加回框坐标。拉伸在某些数据集上很致命比如细长的零件缺陷一拉伸就失去原有的长宽比特征。我建议在数据准备阶段就把「等比缩放 padding」写进去而不是偷懒直接 resize。3.2 构建模型基于预训练骨架的两阶段组装PyTorch 官方 torchvision 提供了现成的 Faster R-CNN 实现但它不是不可修改的黑匣子。正确的做法是把它当骨架按自己的类别数替换最后的分类头。下面的代码是标准的替换方式。from torchvision.models.detection import fasterrcnn_resnet50_fpn from torchvision.models.detection.faster_rcnn import FastRCNNPredictor def build_faster_rcnn(num_classes, pretrained_backboneTrue): num_classes 包含背景类比如 20 类目标就传 21。 model fasterrcnn_resnet50_fpn( weightsDEFAULT if pretrained_backbone else None) in_features model.roi_heads.box_predictor.cls_score.in_features model.roi_heads.box_predictor FastRCNNPredictor(in_features, num_classes) return model不同版本的 torchvision 接口有差异老版本用 pretrainedTrue新版本改用 weightsDEFAULT报错信息会直接提示不用死记。替换分类头之后backbone 和 RPN 的预训练参数还在只有最后的分类层是随机初始化的所以训练前几个 epoch 损失会有一个快速下降的过程那是新分类头在适应任务。如果你想更彻底地理解内部结构可以改模型骨架比如换成 MobileNet 或自定义的轻量骨干。torchvision 提供了把任意 backbone 接进 Faster R-CNN 的接口需要给 backbone 指定 out_channels 和返回多尺度特征图。这个改造适合推理性能受限的场景我在一个部署项目里就换过轻量骨架速度提升明显mAP 掉了不到两个点。3.3 训练循环损失计算、梯度回传与日志监控训练循环的核心代码其实不长。PyTorch 的检测模型在训练模式下传入 images 和 targets返回的是一个损失字典直接求和就能反向传播。def train_one_epoch(model, dataloader, optimizer, device, clip_norm10.0): model.train() epoch_loss 0.0 for images, targets in dataloader: images [img.to(device) for img in images] targets [{k: v.to(device) for k, v in t.items()} for t in targets] loss_dict model(images, targets) total_loss sum(v for v in loss_dict.values()) optimizer.zero_grad() total_loss.backward() # 梯度裁剪是训练检测模型的习惯操作防止 bbox 回归分支炸掉 torch.nn.utils.clip_grad_norm_(model.parameters(), clip_norm) optimizer.step() epoch_loss total_loss.item() return epoch_loss / len(dataloader)这里有两个前提。第一dataloader 的 collate_fn 需要自定义因为同一批图像尺寸可能不同不能直接堆成 batch 张量常见做法是把每张图单独放进一个列表。第二images 里的每张图要做归一化用 ImageNet 的 mean 和 std通道顺序是 CHW。我见过有人把归一化漏了训练 loss 完全不收敛。优化器我习惯用 SGDmomentum 0.9weight_decay 5e-4初始学习率按 batch size 线性缩放。日志方面除了总 loss我会额外记录 RPN 和 ROI 各自分类损失的均值能快速判断是哪个模块出了问题。训练过程要定期保存 checkpoint包括 model.state_dict、optimizer.state_dict 和当前 epoch方便中断后续训——这是踩过「断训全废」的坑之后养成的习惯。4. 决定收敛效果的关键参数Anchor、学习率与 NMS 的调参经验4.1 Anchor 尺度和长宽比怎么定很多人换数据集后直接用默认 Anchor 参数这是精度上不去的第一嫌疑。默认配置是给 VOC/COCO 这类通用数据集设计的目标尺寸从几十像素到几百像素都有。自己的数据集要先做一件事统计真实框的宽高分布。def analyze_gt_sizes(dataset): widths, heights [], [] for _, target in dataset: boxes target[boxes] w (boxes[:, 2] - boxes[:, 0]).tolist() h (boxes[:, 3] - boxes[:, 1]).tolist() widths.extend(w) heights.extend(h) import numpy as np return np.percentile(widths, [25, 50, 75]), np.percentile(heights, [25, 50, 75])拿到中位数和四分位数后把 scales 设置成能覆盖主要尺寸范围的值。比如中位数目标约 60 像素那 scales 用 [4, 8, 16] 比 [8, 16, 32] 合理因为后者的最小 Anchor 面积相当于 128×128比目标本身大得多IoU 永远够不到 0.7匹配阶段全靠兜底逻辑强拉正样本质量极差。长宽比同理细长目标多的数据集可以考虑 [0.25, 0.5, 1.0] 这类更偏极端的组合。这些参数不是拍脑袋定的PyTorch 实现里 Anchor 生成和匹配都是可插拔的函数改完 scales 不需要动网络结构。一个判断锚点设置是否合理的做法跑完第一个 epoch 后打印 RPN 正样本的 IoU 分布如果大量正样本的 IoU 集中在 0.7 附近而不是更高说明 Anchor 和目标的匹配质量不够需要调整尺度覆盖。4.2 学习率、Batch Size 与训练节奏检测任务比分类任务对学习率更敏感因为 bbox 回归分支的梯度尺度变化很大。我推荐的组合是SGD momentum 0.9 weight_decay 5e-4初始学习率 0.005batch size 8 时配合 500 步 warmup从 0.1 倍学习率线性升到初始值再开始正常训练。warmup 在检测任务里不是可选配置RPN 头刚开始随机初始化时如果直接上大步长bbox 回归的偏移量会在头几步爆炸后面很难拉回来。学习率衰减我一般用两步衰减在训练进度 60% 和 80% 处各乘 0.1也可以用余弦退火效果接近。关键观察指标是验证集 mAP 和训练 loss 的同步性。反直觉的经验是训练 loss 降得漂亮但 mAP 不涨问题往往不在学习率而在 RPN 的建议框质量或 ROI 头正样本不足这个在避坑章节展开。骨干网要不要冻结也是个常见纠结。显存不够时冻结某些 stage 是有用的但冻结后 BatchNorm 的统计量停留在预训练分布上如果你的数据分布和 ImageNet 差异大精度会掉。我一般先冻结 backbone 训练 5 个 epoch 让 RPN 和 ROI 头先站稳然后解冻整个网络用小学习率一起微调。4.3 NMS 与后处理参数NMS 和后处理相关的参数全都在模型的 rpn 和 roi_heads 配置里很多人只调阈值不看数量实际数量对结果影响更大。参数训练默认值推理常用值说明pre_nms_top_n20001000NMS 前保留的建议框数量小目标多时要加大post_nms_top_n2000300NMS 后保留的建议框数量训练时太少会饿着 ROI 头rpn_nms_thresh0.70.7RPN 的 NMS 阈值降低会抑制密集目标box_score_thresh0.050.5最终输出的置信度过滤推理时按业务需求调fg_iou_thresh0.5-ROI 头正样本的 IoU 阈值推理阶段的默认 score_thresh 是 0.05直接用于业务会发现一堆垃圾框。建议推理时调到 0.5 左右再根据误检率和召回率的权衡微调。post_nms_top_n 在推理时取 300 对大多数场景足够但如果一张图里有上百个密集小目标建议加到 500 以上否则最终输出框数被卡死NMS 之后留下来的高分框数量不够。5. 避坑指南五个折戟现场的现象、原因与解决5.1 损失变成 NaN训练直接崩掉现象第一个 epoch 还没跑完日志里 total_loss 就变成 NaN后续所有迭代全部 NaN只能重来。原因最常见是 bbox 回归分支的梯度爆炸。RPN 和 ROI 头的回归目标是归一化偏移量如果某个 Anchor 匹配到了一个尺寸极端不匹配的真实框偏移量会非常大乘以学习率后梯度直接溢出。另一种可能是图片里有全黑或全白区域归一化后出现除零不过检测任务里少见。解决我在训练循环里加 torch.nn.utils.clip_grad_norm_max_norm 设 10这是最直接的兜底。同时检查匹配函数如果 Anchor 和真实框算完 IoU 之后出现 x2 x1 或 y2 y1 的退化框直接过滤掉再进入损失计算。另外把起始学习率降到 1e-3 跑前几个 epoch确认稳定后再调回 5e-3这个组合我用了很久基本没有再炸过。5.2 RPN 输出全是背景一个前景都没有现象训练 loss 在正常下降但打印 RPN 的建议框得分所有框的置信度都在 0.3 以下整张图检不出任何目标。原因Anchor 尺度和数据集目标尺寸严重不匹配正样本数量接近于零。默认 scales 是 [8, 16, 32]对应原图目标 128 到 512 像素如果你的数据集目标只有二三十像素所有 Anchor 的 IoU 都低于 0.3匹配逻辑只能靠兜底强拉少数几个正样本RPN 学不到「什么是目标」。解决做一遍 4.1 的尺寸统计把 scales 改成 [2, 4, 8] 或 [4, 8, 16] 再训练。同时把 RPN 的 pos_iou 阈值从 0.7 降到 0.5小目标场景下 0.7 太苛刻。改完之后第一个 epoch 先看 RPN 正样本数量一张图至少要有几十个而不是个位数这是判断 Anchor 设置是否合理的快速指标。5.3 RPN 的 loss 在降但 mAP 纹丝不动现象前两百步 RPN loss 从 1.0 降到 0.2但验证集 mAP 一直在 0.3 附近波动无论怎么调学习率都没用。原因RPN 学得不错但 ROI 头没吃到足够的正样本。最典型的配置错误是训练时 post_nms_top_n 设成了 300和推理保持一致。假设一张图只有十几个小目标NMS 之后留下 300 个框里面正样本可能只有十几个再按 512 个 ROI 采样正样本占比过低分类头一直学不到「什么是目标」。解决训练时把 post_nms_top_n 提到 2000 到 4000让建议框有足够覆盖。同时确认 ROI 采样函数里启用了正负样本均衡通常强制 25% 到 50% 的正样本比例。我还在训练日志里加了 ROI 头正样本数量的监控低于 50 就报警这个数字是排查这类问题最快的入口。5.4 换数据集后检测框整体偏移现象类别分类是对的但框的位置系统性偏上或偏左一段距离像所有框一起平移了。原因数据预处理里做了 resize 或随机裁剪但标注框没有同步变换。尤其是自己写 Dataset 类的时候图像走了 transformsboxes 却直接用原始像素坐标训练和验证用的还可能是两套逻辑一个环节对不上全盘偏移。解决训练、验证、推理共用同一个预处理函数把图像变换和框变换打包成同一个可调用对象。我在实现了 BoxResize 之后又加了一步调试随机取一张训练图把变换后的 boxes 画在变换后的图像上肉眼确认框和物体贴合。这一步只要 15 秒却能省下几天排查时间我后来养成了每次改预处理都要画图验证的习惯。5.5 冻结骨干网后精度反而下降现象为了省显存把 backbone 的参数冻结只训练 RPN 和 ROI 头mAP 掉了 5 个点以上。原因骨干网络的 BatchNorm 统计量running_mean 和 running_var冻结在了预训练数据分布而你的数据分布有偏离冻结后特征分布整体错位下游两个头怎么学都补不回来。解决显存允许就解冻 backbone 一起微调骨干部分用 0.1 倍的学习率。显存实在不够考虑把 BatchNorm 替换成 GroupNorm或者换一个自带 GroupNorm 的骨干。我遇到这个场景是在一张 8G 显存的卡上训练检测模型最后用了 GroupNorm 方案精度只掉了不到一个点训练速度慢了一些——这是显存受限时的合理取舍。6. 让检测器真正可用推理脚本与交付前自检6.1 一个能直接用的推理函数训练完成后把模型导成推理脚本里面最容易翻车的不是模型本身而是推理时的预处理和训练时不一致。图像尺寸、归一化 mean/std、通道顺序任何一项对不上检测结果都会偏离预期。我一般把推理封装成一个函数输入已经完成预处理的图像张量输出过滤后的框、类别和置信度。def run_inference(model, image_tensor, device, score_thresh0.5): 单图推理返回过滤后的框、类别和置信度。 model.eval() with torch.no_grad(): pred model([image_tensor.to(device)])[0] scores pred[scores].cpu() keep scores score_thresh return (pred[boxes][keep].cpu().numpy(), pred[labels][keep].cpu().numpy(), scores[keep].numpy())torchvision 的推理输出已经做过 NMS 和置信度筛选默认阈值是 0.05所以我在外面再按业务阈值过滤一次。如果业务要求每张图最多输出固定数量的框直接按得分排序后截断不要动模型内部的 max_detections 参数这样以后调阈值不用重新导出模型。6.2 交付前过一遍自检清单交付模型前我会过一遍自检每一条都是真实踩过的教训。第一训练和推理的框坐标尺度是否一致比如训练时缩放到 800×600推理时也必须是同一尺寸否则框整体错位。第二类别索引有没有偏移背景类占 0第一个真实类别是 1索引混了会导致所有框标签错位。第三竖图和横图的 padding 路径是否共用同一套坐标映射分叉逻辑最容易出隐藏 bug。第四NMS 和置信度阈值是否按业务场景调过默认 0.05 直接用于生产会输出大量低置信度杂框。第五边缘框是否被裁剪出界clipping 参数在训练和推理时要保持一致。我还有一个固定习惯每次改完模型或预处理都拿同一张测试图跑一次对比框的数量和坐标变化。有一次只是归一化 mean 少写了一位小数所有置信度整体下降 0.1如果没有这张固定对比图问题会上线后才暴露。模型交付这种事慢就是快把验证链路铺好后面才不会在关键时刻翻车。希望帮到你。本文还有配套的精品资源点击获取