赞
踩
我们的视觉世界本质上是长尾和开放的:我们日常生活中视觉类别的频率分布是长尾的,有一些普通的类和许多更罕见的类,我们在一个开放的世界中不断遇到新的视觉概念。
虽然自然数据分布包含head、tail和open类,但现有的分类方法主要在封闭环境中关注head和tail类。传统的深度学习模式善于捕捉head类的大数据;最近,针对tail类别的小样本学习方法被开发出来。
不平衡分类、少量样本学习和开集识别,这三个密切相关的任务被孤立地研究。表1总结了它们的区别。
一个适用的系统应该能在几个常见的和许多罕见的类目中进行分类,仅仅从几个已知的实例中概括出单个类目的概念,并且在一个从未见过的实例中承认新颖性。论文中正式研究了自然数据环境下的开放长尾识别(Open Long-Tailed Recognition, OLTR),OLTR被定义为从长尾和开放端分布式数据中学习,并在一个平衡测试集上评估分类精度,该测试集包括head、tail和open类。
论文中开发出一种OLTR算法,该算法将图像映射到一个特征空间,使得视觉概念可以基于一个学习的度量很容易地相互关联,该度量尊重封闭世界分类,同时承认开放世界的新颖性。OLTR模型有两个主要模块:动态元嵌入(dynamic meta embedding)和调制注意(modulated attention)。前者在头尾类之间联系和传递知识,后者保持头尾类之间的区别。
动态元嵌入结合了直接图像特征和相关的记忆特征,特征范数表示对已知类的熟悉程度。
卷积神经网络CNN的softmax层被用来分类,CNN的倒数第二层被当做特征,最后一层被当做线性分类器。特征和分类器以端到端的方式从大数据中联合训练。让 u d i r e c t u^{direct} udirect表示从输入图像中提取的直接特征,最终的分类准确率很大程度上取决于这个直接特征的质量。
虽然前馈CNN分类器能够很好地处理大的训练数据,但它缺乏来自尾类小数据足够的监督更新。我们提议用一个在记忆模块中连接视觉概念的特征记忆 u m e m o r y u^{memory} umemory来丰富直接特征 u d i r e c t u^{direct} udirect。让 u m e t a u^{meta} umeta表示生成的特征元嵌入,它被反馈到最后一层进行分类。记忆特征 u m e m o r y u^{memory} umemory和元嵌入 u m e t a u^{meta} umeta都依赖于直接特征 u d i r e c t u^{direct} udirect。
与直接功能不同,记忆特征从训练类别中捕获视觉概念,这些概念是用一个更浅的模型从记忆中检索出来的。
学习视觉记忆(Learning Visual Memory):我们借鉴类结构分析方法(class structure analysis),采用discriminative centroids作为基本构造块。让 M M M表示所有训练数据的视觉记忆, M = { c i } i = 1 K M=\left\{c_{i}\right\}_{i=1}^{K} M={ci}i=1K,其中 k k k是训练类别的个数。它和直接特征 { u d i r e c t } \left\{u^{direct} \right\} {udirect}一起被共同学习,同时它考虑了类内紧密性和类间区分性。
我们分两步计算质心。第一步是领域抽样,在训练过程中,我们同时抽样类内和类间的样本,组成一个mini-batch,这些样本按它们的类标签分组,每个组的质心 c i c_{i} ci由该mini-batch的直接特征进行更新。第二步是传播,我们交替地更新直接特征 u d i r e c t u^{direct} udirect和质心,以最小化每个直接特征和其组质心之间的距离,并最大化到其他质心的距离。
run_networks.py 中计算质心的代码如下。仅仅在训练的第二阶段中计算初始质心。从下面代码中,我们可以看出,每个类别的质心为数据集中类别对应图片的特征图的平均值。
def centroids_cal(self, data):
centroids = torch.zeros(self.training_opt['num_classes'],
self.training_opt['feature_dim']).cuda()
print('Calculating centroids.')
for model in self.networks.values():
model.eval()
# Calculate initial centroids only on training data.
with torch.set_grad_enabled(False):
for inputs, labels, _ in tqdm(data):
inputs, labels = inputs.to(self.device), labels.to(self.device)
# Calculate Features of each training data
self.batch_forward(inputs, feature_ext=True)
# Add all calculated features to center tensor
for i in range(len(labels)):
label = labels[i]
centroids[label] += self.features[i]
# Average summed features with class count
centroids /= torch.tensor(class_count(data)).float().unsqueeze(1).cuda()
return centroids
def class_count (data):
labels = np.array(data.dataset.labels)
class_data_num = []
for l in np.unique(labels):
class_data_num.append(len(labels[labels == l]))
return class_data_num
合成记忆功能(Composing Memory Feature):对于输入图像,当没有足够的训练数据(如在tail类中)来学习更好时,
u
m
e
m
o
r
y
u^{memory}
umemory应增强其直接特征。与记忆中的质心相关联的记忆特征,将知识传递给tail类:
u
m
e
m
o
r
y
=
o
T
M
:
=
∑
i
=
1
K
o
i
c
i
,
(
1
)
u^{memory} = o^{T}M := \sum_{i=1}^{K} o_{i}c_{i}, \qquad (1)
umemory=oTM:=i=1∑Koici,(1)
o ∈ R K o \in \mathbb{R}^{K} o∈RK是从直接特征中产生幻觉(hallucinated)系数。我们使用一个轻量级的神经网络从直接特征中获得系数, o = T h a l ( u d i r e c t ) o = T_{hal}\left( u^{direct} \right) o=Thal(udirect)。
获得动态元嵌入(Obtaining Dynamic Meta-Embedding): u m e t a u^{meta} umeta将直接特征和记忆特征相结合,并被馈送到分类器以进行最终的类预测(图3):
u m e t a = ( 1 / γ ) ⋅ ( u d i r e c t + e ⊗ u m e m o r y ) , ( 2 ) u^{meta} = \left( 1 / \gamma \right) \cdot \left( u^{direct} + e \otimes u^{memory} \right), \qquad (2) umeta=(1/γ)⋅(udirect+e⊗umemory),(2)
其中 ⨂ \bigotimes ⨂表示按元素乘法。 γ > 0 \gamma > 0 γ>0似乎是封闭世界分类任务的冗余标量。然而,在OLTR设置中,它在区分训练类实例和开放集实例方面起着重要作用。 γ \gamma γ测量输入直接特征 u d i r e c t u^{direct} udirect到记忆 M M M的可达性[—直接特征和辨别质心之间的最小距离。
γ = r e a c h a b i l i t y ( u d i r e c r , M ) = m i n i ∥ u d i r e c t − c i ∥ , ( 3 ) \gamma = reachability(u^{direcr}, M) = min_{i} \| u^{direct} - c_{i} \| , \qquad (3) γ=reachability(udirecr,M)=mini∥udirect−ci∥,(3)
当 γ \gamma γ很小时,输入很可能属于从中可导出质心的训练类,并且将一个较大的可达性权重(reachability weight) 1 / γ 1/ \gamma 1/γ分配给所得到的元嵌入 u m e t a u^{meta} umeta。否则,嵌入将极度缩小到几乎都是零的向量。这样的属性对于编码开放类很有用。
我们现在描述等式(2)中的概念选择器 e e e。直接特性通常对于数据丰富的头类足够好,而记忆特性对于数据贫乏的尾类更为重要。为了以更柔和的方式自适应地选择它们,我们学习了一个带有 t a n h ( ⋅ ) tanh \left( \cdot \right) tanh(⋅)激活函数的轻量级网络 T s e l ( ⋅ ) T_{sel}\left( \cdot \right) Tsel(⋅)。
e = t a n h ( T s e l ( u d i r e c t ) ) e = tanh \left( T_{sel}\left(u^{direct}\right)\right) e=tanh(Tsel(udirect))
models/MetaEmbeddingClassifier.py代码将动态元嵌入和余弦分类器合并成元嵌入分类器。概念选择器 e e e和幻觉系数 o o o是一个简单的全连接层。下述代码第36行中的 x x x为论文中的 u m e t a u^{meta} umeta, u m e t a u^{meta} umeta中融合了直接特征和infused 特征。
class MetaEmbedding_Classifier(nn.Module):
def __init__(self, feat_dim=2048, num_classes=1000):
super(MetaEmbedding_Classifier, self).__init__()
self.num_classes = num_classes
# 幻觉系数
self.fc_hallucinator = nn.Linear(feat_dim, num_classes)
# 概念选择器
self.fc_selector = nn.Linear(feat_dim, feat_dim)
# 余弦分类器
self.cosnorm_classifier = CosNorm_Classifier(feat_dim, num_classes)
def forward(self, x, centroids, *args):
# storing direct feature
direct_feature = x.clone()
# batch和feature大小
batch_size = x.size(0)
feat_size = x.size(1)
# set up visual memory
x_expand = x.clone().unsqueeze(1).expand(-1, self.num_classes, -1)
# 质心 M
centroids_expand = centroids.clone().unsqueeze(0).expand(batch_size, -1, -1)
keys_memory = centroids.clone()
# computing reachability 可达性权重
# gamma
dist_cur = torch.norm(x_expand - centroids_expand, 2, 2)
values_nn, labels_nn = torch.sort(dist_cur, 1)
scale = 10.0
reachability = (scale / values_nn[:, 0]).unsqueeze(1).expand(-1, feat_size)
# computing memory feature by querying and associating visual memory
# v_memory = o * M
values_memory = self.fc_hallucinator(x.clone())
values_memory = values_memory.softmax(dim=1)
memory_feature = torch.matmul(values_memory, keys_memory)
# computing concept selector
concept_selector = self.fc_selector(x.clone())
concept_selector = concept_selector.tanh() # e = tanh(fc(x))
# u_meta
x = reachability * (direct_feature + concept_selector * memory_feature)
# storing infused feature
infused_feature = concept_selector * memory_feature
# 余弦分类
logits = self.cosnorm_classifier(x)
return logits, [direct_feature, infused_feature]
def create_model(feat_dim=2048, num_classes=1000, stage1_weights=False, dataset=None, test=False, *args):
print('Loading Meta Embedding Classifier.')
clf = MetaEmbedding_Classifier(feat_dim, num_classes)
if not test:
if stage1_weights:
assert(dataset)
print('Loading %s Stage 1 Classifier Weights.' % dataset)
clf.fc_hallucinator = init_weights(model=clf.fc_hallucinator,
weights_path='./logs/%s/stage1/final_model_checkpoint.pth' % dataset,
classifier=True)
else:
print('Random initialized classifier weights.')
return clf
通过聚集从头类和尾类的知识数据获得视觉记忆。然后存储在记忆中的视觉概念会作为关联记忆特征,以增强原始直接特征。概念选择器控制被注入记忆特征的数量和类型。既然头类已经有丰富的直接观察,只需要少量的记忆特征被注入。相反,尾部类缺少观察,相关的在记忆中的视觉概念的功能是非常有益的。
通过计算开放类与视觉记忆的可达性来校准开放类的置信度。
调制注意力,以鼓励不同类别的样本使用不同的上下文。首先,我们通过自相关从输入的特征图中计算出一个自注意图 S A ( f ) SA\left( f \right) SA(f)。它被用作上下文信息并添加到(通过skip连接)原始特征图。然后将调制注意力 M A ( f ) MA \left( f \right) MA(f)作为条件空间注意力应用于自我注意图: S A ( f ) ⨂ M A ( f ) SA\left( f \right) \bigotimes MA \left( f \right) SA(f)⨂MA(f),它允许样本选择不同的空间上下文。最后的注意力特征图变成:
f a t t = f + M A ( f ) ⊗ S A ( f ) f^{att} = f + MA \left( f \right) \otimes SA\left( f \right) fatt=f+MA(f)⊗SA(f)
其中 f f f是CNN中的特征映射, S A ( ⋅ ) SA\left( \cdot \right) SA(⋅)是自注意操作, M A ( ⋅ ) MA \left( \cdot\right) MA(⋅)是具有softmax规范化的条件注意函数。这种调制的注意力可以插入CNN的任何特征层。
在layers/ModulatedAttLayer.py代码中, S A ( ⋅ ) SA\left( \cdot \right) SA(⋅)采用的是 f f f函数为嵌入高斯的Non local Neural Networks非局部运算函数。公式如下所示。调制注意力 M A ( f ) MA \left( f \right) MA(f)采用一个全连接层。
y
i
=
1
C
(
x
)
∑
∀
j
f
(
x
i
,
x
j
)
g
(
x
j
)
f
(
x
i
,
x
j
)
=
e
θ
(
x
i
)
T
ϕ
(
x
j
)
C
(
x
)
=
∑
∀
j
f
(
x
i
,
x
j
)
g
g
g,
θ
\theta
θ和
ϕ
\phi
ϕ都可以用一个简单的1*1卷积函数模拟。
# TODO: implement dot_product and other non-local formats
class ModulatedAttLayer(nn.Module):
def __init__(self, in_channels, reduction = 2, mode='embedded_gaussian'):
super(ModulatedAttLayer, self).__init__()
self.in_channels = in_channels
self.reduction = reduction
self.inter_channels = in_channels // reduction
self.mode = mode
assert mode in ['embedded_gaussian']
# g, theta和phi函数
self.g = nn.Conv2d(self.in_channels, self.inter_channels, kernel_size = 1)
self.theta = nn.Conv2d(self.in_channels, self.inter_channels, kernel_size = 1)
self.phi = nn.Conv2d(self.in_channels, self.inter_channels, kernel_size = 1)
self.conv_mask = nn.Conv2d(self.inter_channels, self.in_channels, kernel_size = 1, bias=False)
self.relu = nn.ReLU(inplace=True)
self.avgpool = nn.AvgPool2d(7, stride=1)
self.fc_spatial = nn.Linear(7 * 7 * self.in_channels, 7 * 7)
self.init_weights()
def init_weights(self):
msra_list = [self.g, self.theta, self.phi]
for m in msra_list:
nn.init.kaiming_normal_(m.weight.data)
m.bias.data.zero_()
self.conv_mask.weight.data.zero_()
# 嵌入高斯
def embedded_gaussian(self, x):
# embedded_gaussian cal self-attention, which may not strong enough
batch_size = x.size(0)
# y = 1/C(x) * sum(f) * g
# g
g_x = self.g(x.clone()).view(batch_size, self.inter_channels, -1)
g_x = g_x.permute(0, 2, 1)
# 1/C(x) * sum(f)
theta_x = self.theta(x.clone()).view(batch_size, self.inter_channels, -1)
theta_x = theta_x.permute(0, 2, 1)
phi_x = self.phi(x.clone()).view(batch_size, self.inter_channels, -1)
map_t_p = torch.matmul(theta_x, phi_x) #f = theta * phi
mask_t_p = F.softmax(map_t_p, dim=-1)
# y
map_ = torch.matmul(mask_t_p, g_x)
map_ = map_.permute(0, 2, 1).contiguous()
map_ = map_.view(batch_size, self.inter_channels, x.size(2), x.size(3))
mask = self.conv_mask(map_) # 1*1
x_flatten = x.view(-1, 7 * 7 * self.in_channels)
# 条件空间注意力
spatial_att = self.fc_spatial(x_flatten)
spatial_att = spatial_att.softmax(dim=1)
spatial_att = spatial_att.view(-1, 7, 7).unsqueeze(1)
spatial_att = spatial_att.expand(-1, self.in_channels, -1, -1)
final = spatial_att * mask + x
return final, [x, spatial_att, mask]
def forward(self, x):
if self.mode == 'embedded_gaussian':
output, feature_maps = self.embedded_gaussian(x)
else:
raise NotImplemented("The code has not been implemented.")
return output, feature_maps
动态元嵌入结合输入图像计算的直接特征和与视觉记忆相关的诱导特征两部分,在头尾类之间联系和传递知识,提高识别的鲁棒性。调制注意力保持头尾类之间的区别性,提高识别灵敏度。图4使用t-SNE可视化了普通ResNet模型和动态元嵌入模型的头类和尾类特征的紧凑感。从下图中可以看出,动态元嵌入由于引入视觉记忆,与直接特征一起被学习,同时考虑到类内紧密性和类间区分性,得到更紧凑的头类和尾类。
我们采用余弦分类器来产生最终的分类结果。具体来说,我们规范化元嵌入 { u n m e t a } \left\{u_{n}^{meta} \right\} {unmeta},其中 n n n表示第n-th输入以及分类器(无偏置项) ϕ ( ⋅ ) \phi \left( \cdot \right) ϕ(⋅)的n-th权重向量 { ω i } i = 1 K \left\{ \omega_{i} \right\}_{i=1}^{K} {ωi}i=1K
元嵌入的标准化策略是一个非线性挤压函数(non-linear squashing function),它确保小幅度的向量被压缩到几乎为零,而大幅度的向量被标准化到略小于1的长度。这个函数有助于放大可达性 γ \gamma γ的影响
u n m e t a = ∥ u n m e t a ∥ 2 1 + ∥ u n m e t a ∥ 2 ⋅ u n m e t a ∥ u n m e t a ∥ ω k = ω k ∥ ω k ∥ u_{n}^{meta} = \frac{\left \| u_{n}^{meta} \right \|^{2}}{1 + \left \| u_{n}^{meta} \right \|^{2} } \cdot \frac{u_{n}^{meta}}{\left \| u_{n}^{meta} \right \|} \\ \omega_{k} = \frac{\omega_{k} }{\left \| \omega_{k} \right \|} unmeta=1+∥unmeta∥2∥unmeta∥2⋅∥unmeta∥unmetaωk=∥ωk∥ωk
所有的模块都是可微的,OLTR模型可以通过交替更新质心 { c i } i = 1 K \left\{ c_{i} \right\}_{i=1}^{K} {ci}i=1K和动态元嵌入 u n m e t a u_{n}^{meta} unmeta来进行端到端的训练. 最终损失函数 L L L由交叉熵分类损失 L C E L_{CE} LCE和embeddings与质心之间的large-margin L L M L_{LM} LLM损失组合。在我们的实验中,通过观察验证集上的准确度曲线,将 λ \lambda λ其设置为0.1。
L = ∑ n = 1 N L C E ( u n m e t a , y n ) + λ ⋅ L L M ( u n m e t a , { c i } i = 1 K ) L = \sum_{n=1}^{N} L_{CE} \left( u_{n}^{meta}, y_{n} \right) + \lambda \cdot L_{LM}\left( u_{n}^{meta}, \left\{ c_{i} \right\}_{i=1}^{K} \right) L=n=1∑NLCE(unmeta,yn)+λ⋅LLM(unmeta,{ci}i=1K)
loss/DiscCentroidsLoss.py 中定义了DiscCentroidsLoss。
class DiscCentroidsLoss(nn.Module):
def __init__(self, num_classes, feat_dim, size_average=True):
super(DiscCentroidsLoss, self).__init__()
self.num_classes = num_classes
self.centroids = nn.Parameter(torch.randn(num_classes, feat_dim))
self.disccentroidslossfunc = DiscCentroidsLossFunc.apply
self.feat_dim = feat_dim
self.size_average = size_average
def forward(self, feat, label):
batch_size = feat.size(0)
#############################
# calculate attracting loss #
#############################
feat = feat.view(batch_size, -1)
# To check the dim of centroids and features
if feat.size(1) != self.feat_dim:
raise ValueError("Center's dim: {0} should be equal to input feature's \
dim: {1}".format(self.feat_dim,feat.size(1)))
batch_size_tensor = feat.new_empty(1).fill_(batch_size if self.size_average else 1)
loss_attract = self.disccentroidslossfunc(feat.clone(), label, self.centroids.clone(), batch_size_tensor).squeeze()
############################
# calculate repelling loss #
#############################
# distmat = feat^2 + centroid^2 - 2 * feat * centroid
distmat = torch.pow(feat.clone(), 2).sum(dim=1, keepdim=True).expand(batch_size, self.num_classes) + \
torch.pow(self.centroids.clone(), 2).sum(dim=1, keepdim=True).expand(self.num_classes, batch_size).t()
distmat.addmm_(1, -2, feat.clone(), self.centroids.clone().t())
classes = torch.arange(self.num_classes).long().cuda()
labels_expand = label.unsqueeze(1).expand(batch_size, self.num_classes)
mask = labels_expand.eq(classes.expand(batch_size, self.num_classes))
distmat_neg = distmat
distmat_neg[mask] = 0.0
# margin = 50.0
margin = 10.0
loss_repel = torch.clamp(margin - distmat_neg.sum() / (batch_size * self.num_classes), 0.0, 1e6)
# loss = loss_attract + 0.05 * loss_repel
loss = loss_attract + 0.01 * loss_repel
return loss
class DiscCentroidsLossFunc(Function):
@staticmethod
def forward(ctx, feature, label, centroids, batch_size):
ctx.save_for_backward(feature, label, centroids, batch_size)
centroids_batch = centroids.index_select(0, label.long())
return (feature - centroids_batch).pow(2).sum() / 2.0 / batch_size
@staticmethod
def backward(ctx, grad_output):
feature, label, centroids, batch_size = ctx.saved_tensors
centroids_batch = centroids.index_select(0, label.long())
diff = centroids_batch - feature
# init every iteration
counts = centroids.new_ones(centroids.size(0))
ones = centroids.new_ones(label.size(0))
grad_centroids = centroids.new_zeros(centroids.size())
counts = counts.scatter_add_(0, label.long(), ones)
grad_centroids.scatter_add_(0, label.unsqueeze(1).expand(feature.size()).long(), diff)
grad_centroids = grad_centroids/counts.view(-1, 1)
return - grad_output * diff / batch_size, None, grad_centroids / batch_size, None
def create_loss (feat_dim=512, num_classes=1000):
print('Loading Discriminative Centroids Loss.')
return DiscCentroidsLoss(num_classes, feat_dim)
**数据集:**论文策划了三个开放的长尾基准(long-tailed benchmark),分别是ImageNet LT(以对象为中心)、Places LT(以场景为中心)和MS1M-LT(以人脸为中心)。论文复现时,我使用了ImageNet LT和Places LT数据集。
网络架构:论文使用scratch ResNet-10[18]作为ImageNet-LT的主干网络。论文使用预先训练的ResNet-152[18]作为Places-LT的主干网。
评估指标: 我们评估每个方法在闭集(测试集不包含未知类)和开集(测试集包含未知类)设置下的性能,以突出它们的差异。在每种设置下,除了所有类的总体top-1分类准确率外,我们还计算了三个不相交子集的准确率:many-shot classes(每个类有超过100个训练样本)、medium-shot classes(每个类有20~100个训练样本)和few-shot classes(低于20个训练样本)。这有助于我们了解每种方法的详细特征。对于开放集设置,F-meausre也被报告为精度和召回的平衡处理。对于确定开放类,概率阈值最初设置为0.1。
训练分为两个阶段,第一阶段就是普通的backbone网络加softmax分类器,第二阶段加入MetaEmbedding_Classifier和ModulatedAttLayer,使用第一阶段的backbone网络参数进行训练。以ImageNet LT数据集为例,训练命令如下所示:
第一阶段训练命令:
python main.py --config ./config/ImageNet_LT/stage_1.py
第二阶段训练命令:
python main.py --config ./config/ImageNet_LT/stage_2_meta_embedding.py
测试闭集:
python main.py --config ./config/ImageNet_LT/stage_2_meta_embedding.py --test
测试开集命令:
python main.py --config ./config/ImageNet_LT/stage_2_meta_embedding.py --test_open
ImageNet闭集测试集中包含50000张图片(ILSVRC2012_val),开集测试机中总共有68000张图片(open类别有18000张),open类来自于ILSVRC2010_val。Place365闭集测试集中包含36500张图片,开集测试机中总共有43100张图片(open类别有6600张)。
测试时,由于训练集中存在many-shot classes、medium-shot classes和few-shot classes等不同的类别,需要加载训练数据。测试many-shot classes、medium-shot classes和few-shot classes准确率的代码如下:
def shot_acc (preds, labels, train_data, many_shot_thr=100, low_shot_thr=20):
# 训练标签
training_labels = np.array(train_data.dataset.labels).astype(int)
# 预测
preds = preds.detach().cpu().numpy()
# 测试标签
labels = labels.detach().cpu().numpy()
train_class_count = []
test_class_count = []
class_correct = []
for l in np.unique(labels):
# 训练集每类别的个数,用于指定类别属于many-shot,median-shot和low-shot哪一类
train_class_count.append(len(training_labels[training_labels == l]))
# 测试集每类别的个数
test_class_count.append(len(labels[labels == l]))
# 每类别正确的数量
class_correct.append((preds[labels == l] == labels[labels == l]).sum())
many_shot = []
median_shot = []
low_shot = []
for i in range(len(train_class_count)):
if train_class_count[i] >= many_shot_thr:
many_shot.append((class_correct[i] / test_class_count[i]))
elif train_class_count[i] <= low_shot_thr:
low_shot.append((class_correct[i] / test_class_count[i]))
else:
median_shot.append((class_correct[i] / test_class_count[i]))
return np.mean(many_shot), np.mean(median_shot), np.mean(low_shot)
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。