Fast R-CNN实战:从零搭建目标检测模型(附完整代码与训练技巧)
Fast R-CNN实战:从零搭建目标检测模型(附完整代码与训练技巧)
如果你已经对卷积神经网络(CNN)在图像分类上的应用驾轻就熟,那么目标检测可能是你下一个想要征服的领域。与单纯的“识别图中有什么”不同,目标检测要求模型不仅能认出物体,还要精准地框出它的位置。这听起来复杂,但Fast R-CNN的出现,极大地简化了这个流程,让开发者能够在一个统一的框架内,高效地完成检测任务。
这篇文章就是为你——一位有一定深度学习基础,希望将理论知识转化为实际项目能力的开发者——准备的实战指南。我们将彻底抛开那些繁琐的、多阶段的训练步骤,直接深入Fast R-CNN的核心,从数据准备、模型搭建、训练调优到最终推理,手把手带你走完全程。你会发现,借助现代深度学习框架,实现一个性能不俗的目标检测模型,并没有想象中那么遥不可及。我们的目标很明确:给你一套清晰、可复现的代码,以及那些在论文和官方教程里很少提及,却能让你少走弯路的实战技巧。
1. 环境准备与数据理解
在开始敲代码之前,一个稳定且版本匹配的开发环境是成功的基石。目标检测项目通常对计算资源有一定要求,因此合理配置环境能避免后续许多令人头疼的兼容性问题。
1.1 核心工具栈选择与安装
我强烈建议使用 PyTorch 作为本次实践的框架。它动态图的设计让模型调试和实验变得非常直观,社区活跃,相关实现也最为丰富。我们将基于PyTorch及其生态工具来构建整个项目。
首先,创建一个新的Conda环境来隔离依赖:
conda create -n fastrcnn python=3.8 -y
conda activate fastrcnn
接下来,安装PyTorch。请根据你的CUDA版本(如果有GPU)去PyTorch官网获取对应的安装命令。例如,对于CUDA 11.3:
pip install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu113
除了PyTorch,我们还需要几个关键的辅助库:
opencv-python: 用于图像读取和基础处理。scikit-image: 提供skimage.draw等工具,用于绘制边界框。matplotlib: 结果可视化。tqdm: 在训练循环中显示进度条,提升体验。pycocotools: 如果我们使用COCO格式的数据集,这个工具包用于评估指标计算。
你可以用以下命令一次性安装:
pip install opencv-python scikit-image matplotlib tqdm pycocotools
注意:
pycocotools在Windows上安装可能稍麻烦,如果遇到问题,可以尝试从这个非官方编译版本下载whl文件进行安装。
1.2 目标检测数据格式解析
数据是模型的燃料。目标检测的数据标注比分类复杂,因为它包含了类别标签和物体位置(通常是边界框)。目前主流有两种格式:
- PASCAL VOC格式:每个图像对应一个XML文件,里面记录了图像尺寸、每个物体的类别名和边界框坐标(
xmin, ymin, xmax, ymax)。结构清晰,易于人工阅读。 - COCO格式:使用一个大的JSON文件来管理整个数据集的标注信息。它包含了
images、annotations、categories等字段。COCO格式更紧凑,并且直接支持更复杂的标注如分割掩码。
为了通用性,我们这里实现一个简单的解析器,假设你使用的是类似VOC的格式,即每个标注是一个包含以下键的字典:
annotation = {
‘bbox‘: [x_min, y_min, width, height], # COCO常用格式
‘category_id‘: 1, # 类别索引,0通常为背景
‘image_id‘: ‘000001‘
}
在实际操作中,你需要将自己的数据集整理成这种结构。一个常见的做法是创建一个Dataset类,在__getitem__方法中返回图像张量、边界框坐标张量、类别标签张量以及图像原始尺寸等信息。
2. 模型架构深度拆解与实现
Fast R-CNN的精妙之处在于其简洁而高效的架构设计。它解决了R-CNN时代重复计算卷积特征的巨大开销,将整个流程一体化。下面我们来亲手搭建它的核心组件。
2.1 骨干网络与特征提取
Fast R-CNN本身不是一个全新的网络,而是一个框架。它需要一个强大的卷积神经网络(CNN)作为骨干来提取图像特征。在原始论文中,作者使用了在ImageNet上预训练的VGG16。今天,我们有更多更好的选择,如ResNet、MobileNet等。它们能提供更深的网络层次和更强的特征表示能力,同时训练起来也更稳定。
这里我们以ResNet-50为例。PyTorch的torchvision.models提供了预训练好的模型,我们可以方便地加载并对其进行改造。
import torch
import torchvision.models as models
import torch.nn as nn
class FastRCNNFeatureExtractor(nn.Module):
def __init__(self, backbone=‘resnet50‘, pretrained=True):
super(FastRCNNFeatureExtractor, self).__init__()
# 加载预训练的ResNet-50
if backbone == ‘resnet50‘:
resnet = models.resnet50(pretrained=pretrained)
# 移除最后的全连接层和平均池化层
# 我们将使用ResNet的conv1到layer4的所有卷积层
self.backbone = nn.Sequential(*list(resnet.children())[:-2])
# 获取骨干网络输出特征的通道数,ResNet-50是2048
self.out_channels = 2048
def forward(self, x):
# 输入x: [batch_size, 3, H, W]
features = self.backbone(x) # 输出: [batch_size, 2048, H‘, W‘]
return features
这段代码定义了一个特征提取器。它去掉了ResNet原始的分类头,只保留卷积部分。输出的features是一个特征图,其空间尺寸(H‘, W‘)是输入图像尺寸的1/32(因为ResNet有5次下采样)。这个特征图包含了图像的密集特征,后续的RoI池化层将从这里为每个候选区域“裁剪”出对应的特征块。
2.2 ROI池化层:统一特征尺寸的关键
这是Fast R-CNN的核心创新之一。由于选择性搜索(Selective Search)或区域提议网络(RPN)产生的候选框大小不一,而后续的全连接层要求固定长度的输入。RoI池化层的作用就是将任意大小的候选区域特征,池化到固定尺寸(如7x7)。
PyTorch在torchvision.ops中已经提供了roi_pool和roi_align(后者是Mask R-CNN的改进,效果通常更好)函数。但我们为了理解原理,可以简要了解其实现思想:
import torch
from torchvision.ops import roi_align
# 假设我们有一批特征图和一个候选框列表
batch_features = torch.randn(2, 2048, 25, 38) # [batch, channels, height, width]
# RoI的格式是 [batch_index, x1, y1, x2, y2]
rois = torch.tensor([
[0, 10, 20, 50, 80], # 图像0中的一个框
[1, 30, 40, 90, 120], # 图像1中的一个框
], dtype=torch.float32)
# 使用RoIAlign将每个RoI对应的特征区域池化到7x7大小
pooled_features = roi_align(
batch_features,
rois,
output_size=(7, 7),
spatial_scale=1.0/32.0, # 特征图相对于原图的缩放比例
sampling_ratio=2 # 每个bin内的采样点数
)
# pooled_features的形状: [2, 2048, 7, 7]
spatial_scale参数至关重要,它需要根据骨干网络的下采样倍数来设置。对于ResNet-50,从输入图像到layer4输出,总共下采样了32倍,所以spatial_scale=1/32。这个参数告诉RoI池化层,如何将基于原始图像坐标的RoI,映射到特征图上的坐标。
2.3 分类与回归头:双任务输出
经过RoI池化后,我们得到了形状为[num_rois, 2048, 7, 7]的特征。接下来,我们将其展平,并通过两个全连接层(通常称为“头部”)来分别执行分类和边界框回归。
class FastRCNNHead(nn.Module):
def __init__(self, in_channels=2048, num_classes=21, representation_size=1024):
super(FastRCNNHead, self).__init__()
# 两个全连接层,作为共享的特征表示
self.fc6 = nn.Linear(in_channels * 7 * 7, representation_size)
self.fc7 = nn.Linear(representation_size, representation_size)
self.relu = nn.ReLU(inplace=True)
# 分类头:预测每个RoI属于每个类别的概率(包括背景)
self.cls_score = nn.Linear(representation_size, num_classes)
# 回归头:预测每个类别的边界框偏移量 (dx, dy, dw, dh)
self.bbox_pred = nn.Linear(representation_size, num_classes * 4)
# 初始化权重
self._init_weights()
def _init_weights(self):
nn.init.normal_(self.cls_score.weight, mean=0, std=0.01)
nn.init.constant_(self.cls_score.bias, 0)
nn.init.normal_(self.bbox_pred.weight, mean=0, std=0.001)
nn.init.constant_(self.bbox_pred.bias, 0)
def forward(self, x):
# x: [N, 2048, 7, 7]
x = x.flatten(start_dim=1) # [N, 2048*7*7]
x = self.relu(self.fc6(x))
x = self.relu(self.fc7(x))
# 并行输出
cls_logits = self.cls_score(x) # [N, num_classes]
bbox_deltas = self.bbox_pred(x) # [N, num_classes * 4]
return cls_logits, bbox_deltas
这里有几个细节值得注意:
- 回归头的输出维度:是
num_classes * 4。这意味着我们为每一个类别都预测一组边界框调整参数(dx, dy, dw, dh)。在推理时,我们只使用被预测类别的对应那4个参数。这种设计让模型可以为不同类别的物体学习不同的回归策略。 - 权重初始化:回归头的权重初始化的标准差(0.001)比分类头(0.01)小。这是因为边界框回归是一个更精细的任务,我们希望初始的预测偏移量很小,避免训练初期的不稳定。
3. 训练流程与核心技巧
将各个模块组装起来后,我们就得到了完整的Fast R-CNN模型。但如何有效地训练它,才是项目成败的关键。这一部分我们将深入训练循环,讨论数据采样、损失函数设计和关键的调优技巧。
3.1 数据加载与候选区域生成
在训练阶段,我们需要为每张图像提供候选区域(Region Proposals)。在原始的Fast R-CNN中,使用的是选择性搜索(Selective Search)。虽然现在Faster R-CNN的RPN更流行,但为了理解经典流程,我们仍可以从SS开始。你可以使用selectivesearch这个Python包,或者使用OpenCV实现的版本。
然而,在训练时,我们不会使用SS生成的所有候选框(约2000个),因为大部分是负样本(背景)。Fast R-CNN采用了一种采样策略:每张图像采样64个RoI,其中正负样本比例约为1:3。正样本是与任意真实框的IoU大于0.5的候选框,负样本是IoU在[0.1, 0.5)之间的候选框(IoU小于0.1的被认为是“困难负样本”,在原始论文中被忽略)。
提示:在实际代码中,实现这个采样逻辑需要仔细处理。你需要计算所有候选框与所有真实框的IoU矩阵,然后为每个候选框分配一个标签(类别)和一个回归目标(与它IoU最大的真实框的偏移量)。这是一个计算密集但至关重要的步骤。
下面是一个简化的采样函数框架:
def sample_rois_for_training(proposals, gt_boxes, gt_labels, num_samples=64, pos_ratio=0.25):
"""
proposals: [N, 4] 候选框
gt_boxes: [M, 4] 真实框
gt_labels: [M] 真实标签
返回:采样后的rois,对应的标签,回归目标
"""
# 1. 计算IoU矩阵 [N, M]
iou_matrix = compute_iou(proposals, gt_boxes)
# 2. 为每个proposal分配最大IoU的真实框和标签
max_iou, max_idx = iou_matrix.max(dim=1)
assigned_labels = gt_labels[max_idx]
assigned_boxes = gt_boxes[max_idx]
# 3. 根据IoU阈值区分为正负样本
pos_mask = max_iou >= 0.5
neg_mask = (max_iou < 0.5) & (max_iou >= 0.1)
# 4. 采样
pos_indices = torch.where(pos_mask)[0]
neg_indices = torch.where(neg_mask)[0]
# ... 采样逻辑,确保总数和比例 ...
# 5. 计算回归目标 (tx, ty, tw, th)
targets = box_transform(proposals[sampled_indices], assigned_boxes[sampled_indices])
return sampled_rois, sampled_labels, targets
3.2 多任务损失函数
Fast R-CNN的损失函数是分类损失和回归损失的加权和,这是多任务学习的典型范式。
分类损失:使用交叉熵损失。对于每个采样到的RoI,我们都有一个真实的类别标签(0代表背景)。模型输出所有类别的得分,计算交叉熵。
回归损失:使用Smooth L1 Loss。这个损失函数对离群点(预测值与真实值差距过大)不那么敏感,比L2 Loss更鲁棒。它只在绝对误差大于1时表现为L1 Loss(线性),小于1时表现为L2 Loss(平方),从而在零点附近更平滑,梯度更稳定。
$$ \text{SmoothL1}(x) = \begin{cases} 0.5x^2 & \text{if } |x| < 1 \ |x| - 0.5 & \text{otherwise} \end{cases} $$
重要的是,回归损失只对正样本(非背景)计算。背景区域没有“正确”的边界框需要回归。此外,回归目标是针对特定类别的偏移量,但在计算损失时,我们只使用该RoI被分配的真实类别所对应的那4个预测值。
import torch.nn.functional as F
class FastRCNNLoss(nn.Module):
def __init__(self, num_classes):
super().__init__()
self.num_classes = num_classes
def forward(self, class_logits, box_regression, labels, regression_targets):
# 分类损失
classification_loss = F.cross_entropy(class_logits, labels)
# 回归损失:只计算正样本的
pos_mask = labels > 0 # 背景标签为0
sampled_pos_inds = torch.where(pos_mask)[0]
if sampled_pos_inds.numel() > 0:
# 获取正样本对应的预测回归值
# box_regression形状: [N, num_classes * 4]
# 我们需要为每个正样本,选取其对应标签类别的4个预测值
box_regression_pos = box_regression[sampled_pos_inds].view(-1, self.num_classes, 4)
# 一种高级索引方式获取对应类别的预测
box_regression_pos = box_regression_pos[torch.arange(len(sampled_pos_inds)), labels[sampled_pos_inds]]
regression_loss = F.smooth_l1_loss(
box_regression_pos,
regression_targets[sampled_pos_inds],
beta=1.0, # SmoothL1的参数
reduction=‘sum‘
) / max(1, sampled_pos_inds.numel()) # 平均到正样本数量
else:
regression_loss = torch.tensor(0.0, device=class_logits.device)
# 总损失
total_loss = classification_loss + regression_loss
return total_loss, classification_loss, regression_loss
3.3 训练策略与调优技巧
有了数据和损失函数,就可以开始训练了。但直接训练可能效果不佳,这里分享几个经过实践验证的技巧:
-
分层学习率(Layer-wise Learning Rate):骨干网络(如ResNet)是在ImageNet上预训练的,其特征提取能力已经很强。我们不想用太大的学习率破坏这些好的底层特征。因此,可以为骨干网络设置一个较小的学习率(例如基础学习率的0.1倍),而为新添加的头部网络设置较大的学习率。在PyTorch中,这可以通过为优化器传递不同的参数组来实现。
-
热身(Warmup):在训练初期,模型参数还处于随机状态,直接使用较大的学习率可能导致训练不稳定。可以采用线性热身策略,在最初的几个epoch里,将学习率从0线性增加到预设值。
-
梯度裁剪(Gradient Clipping):特别是当批次大小(batch size)设置得比较大时,梯度可能会爆炸。在
loss.backward()之后,optimizer.step()之前,加入torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=10)可以限制梯度范数,稳定训练。 -
数据增强:虽然原始Fast R-CNN论文中只使用了水平翻转,但在实际项目中,适度的数据增强能显著提升模型泛化能力。例如,可以随机调整亮度、对比度、饱和度,或者进行小尺度的随机缩放和裁剪。但要注意,裁剪不能把目标物体裁掉太多。
-
正样本的困难挖掘:在训练后期,模型对简单的负样本(纯背景)已经能很好地区分。这时,损失主要来自那些与物体相似、难以区分的背景区域(困难负样本)。可以在训练过程中,定期根据当前模型的预测,选择那些被错误分类为前景的负样本,加入到后续的训练中,这能有效提升分类器的判别能力。
一个训练循环的核心代码结构如下:
model.train()
for epoch in range(num_epochs):
for images, targets in data_loader: # targets包含gt_boxes, gt_labels
# 1. 生成候选区域 (例如用SS)
proposals = selective_search(images)
# 2. 采样RoI并计算回归目标
sampled_rois, sampled_labels, reg_targets = sample_rois(proposals, targets)
# 3. 前向传播
features = backbone(images)
pooled_features = roi_align(features, sampled_rois)
cls_logits, bbox_deltas = head(pooled_features)
# 4. 计算损失
loss, cls_loss, reg_loss = loss_fn(cls_logits, bbox_deltas, sampled_labels, reg_targets)
# 5. 反向传播与优化
optimizer.zero_grad()
loss.backward()
# 梯度裁剪
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=10)
optimizer.step()
# 6. 学习率调度(如StepLR)
scheduler.step()
4. 模型推理与性能优化
训练完成后,我们需要将模型部署到推理阶段。与训练不同,推理时我们不再采样,而是需要处理一张图像上的所有候选区域,并输出最终的检测结果。这个过程涉及置信度阈值过滤、非极大值抑制等后处理步骤。
4.1 完整推理流程
推理流程可以概括为以下几个步骤:
- 图像预处理:将输入图像缩放到固定尺寸(如短边600像素,长边不超过1000像素),同时记录缩放比例。图像像素值归一化到[0, 1]并减去均值、除以标准差。
- 生成候选区域:使用选择性搜索或一个轻量级的区域提议方法,为整张图像生成约1000-2000个候选框。
- 模型前向传播:
- 将整张图像输入骨干网络,得到特征图。
- 将所有候选框映射到特征图上,通过RoI池化得到每个框的特征。
- 将特征输入分类回归头,得到每个框对所有类别的得分
cls_scores和边界框偏移bbox_deltas。
- 后处理:
- 解码边界框:利用预测的偏移量
bbox_deltas和原始的候选框坐标,计算出调整后的预测框坐标。公式是编码过程的逆运算。 - 应用分类置信度阈值:对于每个预测框,取得分最高的类别作为其预测类别,并过滤掉得分低于阈值(如0.7)的预测。这能去掉大量不可靠的检测。
- 非极大值抑制(NMS):这是关键一步。对于每个类别,将所有预测框按置信度排序。然后从最高置信度的框开始,移除所有与其IoU超过一定阈值(如0.5)的、置信度更低的框。这能消除同一个物体上的多个重复框。
- 解码边界框:利用预测的偏移量
def detect(model, image, proposal_generator, score_thresh=0.7, nms_thresh=0.5):
model.eval()
with torch.no_grad():
# 1. 预处理图像,得到tensor和缩放信息
processed_img, scale_factor = preprocess(image)
# 2. 生成候选框(基于原始图像尺寸)
proposals = proposal_generator(image) # [N, 4] (x1, y1, x2, y2)
# 将proposals缩放到预处理后图像的尺度上
scaled_proposals = proposals * scale_factor
# 在proposals前添加batch索引(0)
batch_indices = torch.zeros((len(scaled_proposals), 1), device=scaled_proposals.device)
rois = torch.cat([batch_indices, scaled_proposals], dim=1)
# 3. 模型前向
features = model.backbone(processed_img.unsqueeze(0))
pooled_features = roi_align(features, rois, spatial_scale=model.spatial_scale)
cls_logits, bbox_deltas = model.head(pooled_features)
# 应用softmax得到概率
cls_probs = F.softmax(cls_logits, dim=-1)
# 4. 后处理
detections = []
# 解码边界框,需要原始proposals(原始图像尺度)
pred_boxes = decode_boxes(proposals, bbox_deltas.view(-1, model.num_classes, 4))
# 遍历所有类别(跳过背景0)
for class_id in range(1, model.num_classes):
# 获取该类别的概率和对应的预测框
class_scores = cls_probs[:, class_id]
class_boxes = pred_boxes[:, class_id, :]
# 根据置信度阈值过滤
keep = class_scores > score_thresh
if keep.sum() == 0:
continue
scores_keep = class_scores[keep]
boxes_keep = class_boxes[keep]
# 执行NMS
nms_keep = nms(boxes_keep, scores_keep, nms_thresh)
final_boxes = boxes_keep[nms_keep]
final_scores = scores_keep[nms_keep]
# 保存结果
for box, score in zip(final_boxes, final_scores):
detections.append({
‘bbox‘: box.cpu().numpy().tolist(),
‘score‘: score.item(),
‘category_id‘: class_id
})
return detections
4.2 性能瓶颈分析与优化
当你跑通整个流程后,可能会发现推理速度不尽如人意。我们来分析一下瓶颈在哪里,以及如何优化。
瓶颈1:区域提议生成。选择性搜索在CPU上运行,速度很慢(每张图2-3秒),这成为了整个系统的瓶颈。这也是Faster R-CNN用RPN(区域提议网络)替代它的原因。优化方案:可以尝试以下一种或多种方法:
- 使用更快的提议方法,如EdgeBoxes。
- 将选择性搜索的结果预先计算并保存下来,但这只适用于静态数据集。
- 最佳实践:直接引入一个轻量级的RPN。即使不训练完整的Faster R-CNN,你也可以单独训练一个RPN来生成提议,速度能提升一个数量级。
瓶颈2:全连接层计算。正如原始论文指出的,对于2000个RoI,每个都要经过两个大的全连接层(如4096维),计算量巨大。论文中提出用奇异值分解(SVD) 来加速全连接层。
- 原理:将一个大的全连接层权重矩阵$W$(形状
[M, N])分解为三个小矩阵的乘积:$W ≈ U \Sigma V^T$。我们可以用两个更小的全连接层(fc1: M->K,fc2: K->N,其中K是较小的秩)来近似原层,从而减少计算量。 - PyTorch实现:你可以使用
torch.svd对训练好的全连接层权重进行分解,然后用两个新的线性层替换它。这通常在模型部署阶段进行。
def compress_fc_layer(fc_layer, rank=256):
W = fc_layer.weight.data # [output_dim, input_dim]
U, S, Vh = torch.svd(W)
# 取前rank个奇异值
U_r = U[:, :rank]
S_r = torch.diag(S[:rank])
V_r = Vh.t()[:, :rank]
# 构建两个小层
fc1 = nn.Linear(W.size(1), rank, bias=False)
fc2 = nn.Linear(rank, W.size(0), bias=True)
fc1.weight.data = (S_r @ V_r.t()).t()
fc2.weight.data = U_r.t()
fc2.bias.data = fc_layer.bias.data
return nn.Sequential(fc1, fc2)
瓶颈3:RoI池化与特征提取。虽然Fast R-CNN已经共享了卷积特征,但RoI池化本身和后续的逐RoI处理仍有开销。对于实时应用,可以考虑:
- 使用更轻量的骨干网络,如MobileNetV3或EfficientNet-Lite。
- 减少生成的候选区域数量(例如,通过更严格的NMS过滤提议框)。
- 使用TensorRT、ONNX Runtime等推理引擎对模型进行图优化和量化,能极大提升在GPU或边缘设备上的速度。
4.3 模型评估与常见问题排查
训练完成后,如何知道模型的好坏?我们需要在独立的验证集上进行评估。目标检测常用的评估指标是平均精度(Average Precision, AP) 和平均精度均值(mAP)。
- AP:针对单个类别,计算在不同置信度阈值下的精度-召回率(Precision-Recall)曲线下的面积。
- mAP:对所有类别的AP取平均值。
你可以使用pycocotools中的COCOeval工具来计算mAP,它已经是业界的标准。你需要将模型的预测结果转换成COCO评估所需的格式。
在评估时,你可能会遇到一些典型问题:
- mAP很低:检查数据标注是否正确,特别是边界框坐标。检查训练损失是否在正常下降。可能是学习率太大或太小,或者正负样本采样比例失衡。
- 召回率低(很多物体检不出来):可能是候选区域生成的质量太差,没有覆盖到物体。尝试增加候选区域的数量,或者改进提议算法。也可能是分类置信度阈值设得太高。
- 精度低(很多误检):可能是负样本不够或不够“困难”,导致分类器无法区分背景和物体。尝试在训练中加入困难负样本挖掘。也可能是NMS的阈值设得太低,或者需要提高分类置信度阈值。
- 定位不准(边界框漂移):回归损失可能没有有效收敛。检查回归目标的计算是否正确。尝试调整回归损失的权重(在总损失中的占比),或者使用GIoU、DIoU等更先进的回归损失函数替代Smooth L1。
调试模型是一个需要耐心和实验的过程。我的经验是,从一个简单的小数据集(如PASCAL VOC的少数几类)开始,确保整个流程在这个小任务上能work,然后再扩展到更复杂的数据集和场景。每次只改变一个变量(如学习率、数据增强策略),并观察评估指标的变化,这样才能清晰地知道什么方法有效。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐

所有评论(0)