【人脸识别】MTCNN + Arcface全流程详解 Pytorch代码 损失函数发展
目录:
- 人脸识别介绍
- 损失函数发展
- Softmax loss
- Center loss
- Triplet loss
- L-softmax loss
- SphereFace(A-Softmax loss)
- CosFace(AM-Softmax loss)
- ArcFace loss
- 人脸识别流程
- 相似度
- 生成训练图片
- 数据采样
- 网络模型
- 训练
- 使用
- 摄像头实时检测
人脸识别介绍
MTCNN 实现人脸检测,回答了“是不是人脸”和“人脸在哪”的问题,接下来人脸识别要解决的就是“人脸是谁”。
人脸识别是目标识别中的一种,本质上也是分类问题,只不过是同类(人脸)中的细分,因为人脸之间相似度很大,这对损失函数的分类能力提出了更高的要求。
损失函数发展
下面介绍分类损失函数的主要类型和发展历程,及部分pytorch代码。
效果图来自 MNIST 数据集,将网络模型倒数第二层输出通道数设为2,将二维分类特征可视化即可。
Softmax loss
经典的分类损失 Softmax loss,将正确类别的预测概率最大化。但这种方式只考虑了能否正确分类,没有考虑类间距离。
Center loss
Center loss 在 Softmax loss 基础上增加了 L C L_C LC项,给每个类都设置一个中心 c y i c_{yi} cyi,让该类尽量向中心靠拢,在保证分类的同时,最小化类内距离。
需要注意的是:
- Center loss 本身没有分类功能,需要配合 Softmax loss,不能单独使用。
- 中心 c y i c_{yi} cyi初始化是随机值,之后随着学习到的特征进行实时更新。
- 计算每一类的中心损失时,需要除以该类样本数计算均值,防止因样本失衡导致的不同类别梯度更新不同步。
- 参数 λ \lambda λ控制中心损失优化力度, λ \lambda λ越大区分度越高,但在人脸识别中,经验值一般取0.003即可。
Center loss 在人脸识别上的效果还是不错的,但还有许多不足:
- 类内距优化效果还不理想。
类内距还是较大,当类别较多时,无法清晰区分特征。 - 类别多时,对硬件要求较高。
每个类别需要维护一个中心点,当类别很多时计算量大。 - L2范数的离群点难以优化。
因为中心损失计算的是每一类损失的均值,离群点导致loss较大,难以下降,同理在loss下降过程中,离群点的优化力度不够,相对仍然离中心很远。 - 只适用于同类样本间差异较小的数据。
将同类样本向一个中心点优化的前提是,这一类样本间相似度较大,中心点可以代表这一类样本的特征,如果差异很大,就相当于有很多离群点,自然难以优化。可以抽象理解为:一个人的一堆人脸取均值大概还能看出是人脸,而各种类别的狗取均值就完全认不出是什么了。
def center_loss(feature, label, lambdas):"""计算中心损失:param feature: 网络输出特征 (N, 2):param label: 分类标签 如 tensor([0, 2, 1, 0, 1]):param lambdas: 参数 控制中心损失大小 即类内间距:return:"""device = torch.device("cuda" if torch.cuda.is_available() else "cpu")# 随机生成一组中心点参数 如 (C, 2)center = nn.Parameter(torch.randn(int(max(label).item() + 1), feature.shape[1]), requires_grad=True).to(device)# 根据标签索引 生成与feature相对应的中心点 如 (N, 2)center_exp = center.index_select(0, label.long())# 统计标签中各分类的个数 如 tensor([3, 2]) 代表类别0的样本有3个 类别1的样本有2个count = torch.histc(label, bins=int(max(label).item() + 1), min=0, max=int(max(label).item()))# 根据标签索引 生成与feature相对应的各类样本数count_exp = count.index_select(dim=0, index=label.long())loss = lambdas / 2 * torch.mean(torch.div(torch.sum(torch.pow(feature - center_exp, 2), dim=1), count_exp))return loss
网络模型倒数第二层输出二维特征,计算 CenterLoss,加上最后一层10分类 Softmax loss 作为总loss。
我的训练效果:
Triplet loss
三元组损失函数,三元组由Anchor, Negative, Positive组成。如上图,Anchor 和 Positive 是同类样本,经过学习尽量靠近(减小同类距离),Anchor 和 Negative 尽量远离(增大不同类间距离)。
其中第一项为类内距 ,第二项为类间距,超参数 α \alpha α 表示两种距离的目标差距。
Triplet Loss 在 Center loss 之前是人脸识别的常用损失函数,缺点是在样本数增多的时候,样本对的组合数量会指数级激增,非常耗时,因此诞生了下列几种新的损失算法。
L-softmax loss
首先调整 Softmax Loss,将卷积运算转化为向量积:
忽略偏置b后损失函数变为:
其中 c o s θ cosθ cosθ 改成了 c o s ( m θ ) cos(mθ) cos(mθ), m > 1 m>1 m>1。
以二分类举栗,优化目标发生了如下变化,相当于增加决策余量 margin,加大了学习难度,m越大难度越大。
由下图可以看到,无论 ||W1|| 和 ||W2|| 的关系如何,L-softmax loss 都可以压缩类内距增大类间距。
SphereFace(A-Softmax loss)
A-Softmax loss 在 L-softmax loss 基础上将权重归一化: ∣ ∣ W ∣ ∣ = 1 ||W|| = 1 ∣∣W∣∣=1,导致特征上的点映射到单位超球面上,这样模型的预测仅取决于 W W W 和 X X X 之间的角度,只需要保证 c o s ( θ 1 ) < c o s ( m θ 2 ) cos(\theta_1) < cos(m\theta_2) cos(θ1)<cos(mθ2),不用再考虑 ||W1|| 和 ||W2|| 的大小关系。
CosFace(AM-Softmax loss)
人脸识别是根据两个特征向量之间的余弦相似度计算的。这表明,特征向量 x x x 的范数是对评分没有贡献,于是,令 ∣ ∣ W ∣ ∣ = 1 ||W|| = 1 ∣∣W∣∣=1, ∣ ∣ x ∣ ∣ = s \displaystyle ||x||=s ∣∣x∣∣=s , s s s只是作为一个缩放系数,影响输出的损失值。
式子中减去余弦边缘项 m m m,作用和 A-Softmax loss 中一样, c o s cos cos函数在 ( 0 , π ) (0, \pi) (0,π)区间内递减,因此 c o s ( θ ) cos(\theta) cos(θ)增大角度 θ \theta θ,相当于减小余弦值。
ArcFace loss
A-Softmax loss 和 AM-Softmax loss 最大不同在于一个是角度距离,一个是余弦距离。
优化角度距离比优化余弦距离对分类的影响更直接,因此 ArcFace loss 还是选择增大角度 θ \theta θ,但不是通过乘 m m m而是加 m m m,避免了倍角计算在反向传播时的求导麻烦。
需要注意:上述几种对 Softmax loss 的改造,相当于在分子分母上同时减小一个值,破坏了 Softmax 总和为1的特性。
class ArcNet(nn.Module):def __init__(self, feature_dim=2, cls_dim=10):"""生成参数W (2, 10) 与最后一层权重shape相同:param feature_dim: 倒数第二层特征维度:param cls_dim: 最后一层分类置信度维度"""super(ArcNet, self).__init__()self.W = nn.Parameter(torch.randn(feature_dim, cls_dim), requires_grad=True)def forward(self, feature, m=1, s=10):"""计算Arc-Softmax:param feature: 倒数第二层 特征层输出:param m: 增大角度:param s: 缩放比例:return: (N, 10)"""# 特征x (N, 2) 和参数w (2, 10) 在特征维度上 标准化x = F.normalize(feature, dim=1)w = F.normalize(self.W, dim=0)# 向量x与w的余弦相似度cosa = torch.matmul(x, w) / s# 反余弦 算出角度θa = torch.acos(cosa)arcsoftmax = torch.exp(s * torch.cos(a + m)) / (torch.sum(torch.exp(s * cosa), dim=1, keepdim=True)- torch.exp(s * cosa) + torch.exp(s * torch.cos(a + m)))return arcsoftmax
ArcNet 作为网络模型的最后一层参与训练, W W W是训练参数。代码实现和网上常见版本不太一样,常见代码是根据分类标签,找到要优化的角度向量,然后替换。而上述代码不管标签,统一优化所有类别的角度向量,虽然从理论角度有一点点不合理,但代码简洁了许多,也能实现效果。
我的训练效果:
人脸识别流程
利用 MTCNN + Arcface loss 实现人脸识别的总体流程:
- 训练特征提取器
a. 创建特征提取网络(如ResNet)。
b. 准备训练数据集(开源数据集如:VGG-Face 2、MS-Celeb-1M等等)。
c. 设计损失函数(Arcface loss)。
d. 训练网络使其获得特征提取能力。 - 创建人脸特征库
a. 通过MTCNN提取或者其他途径获得人脸框。
b. 将人脸框传入特征提取器提取人脸特征。
c. 将每个人脸特征和对应标签存入人脸特征库。 - 检测目标人脸
a. 通过 MTCNN 获得当前画面所有人脸框。
b. 将每个人脸框传入特征提取器提取人脸特征。
c. 将每个人脸特征和人脸特征库里的特征一一对比(余弦相似度)。
d. 如果最大的相似度大于阈值,则认为是同一个人脸,否则为陌生人。
相似度
先解释一下人脸识别中的相似度,我们衡量2个向量的相似程度,常见指标就是欧氏距离和余弦相似度。欧氏距离不用解释了,余弦相似度就是计算两个向量间的夹角的余弦值,余弦距离=1-余弦相似度。总体来说,欧氏距离体现数值上的绝对差异,而余弦距离体现方向上的相对差异。
公式就不做详细推导了,向量在归一化之后, 欧 氏 距 离 = 2 − 2 × 余 弦 相 似 度 欧氏距离=\sqrt{2-2×余弦相似度} 欧氏距离=2−2×余弦相似度 。
因此有如下结论:向量在归一化之后,欧式距离和余弦距离可近似等价。我们在人脸识别中计算的余弦相似度,就是归一化之后的。
生成训练图片
我使用VGG-Face2数据集,含9131个人(训练8631,测试500),共331万张图片。
人脸标签csv文件
我们需要训练的只是人脸图片,读取标签人脸坐标,转成正方形框后裁下人脸,根据我使用的网络模型,图片尺寸resize成(112, 112),保存。
from PIL import Image
import os
import csvd = 'train'
main_dir = r"F:\AI Dataset\VGG-Face2\data\{}".format(d)
f = open(r'F:\AI Dataset\VGG-Face2\meta\bb_landmark\loose_bb_{}.csv'.format(d), 'r')
reader = csv.reader(f)
img_list = list(reader)[1:]
for i in range(len(img_list)):name, x, y, w, h = img_list[i]img_path = os.path.join(main_dir, name + '.jpg')ori_image = Image.open(img_path)x1, y1, x2, y2 = int(x), int(y), int(x) + int(w), int(y) + int(h)l = max(int(w), int(h))if l < 100:continuecx = (x1 + x2) // 2cy = (y1 + y2) // 2x1 = cx - l // 2x2 = x1 + ly1 = cy - l // 2y2 = y1 + limg = ori_image.crop((x1, y1, x2, y2)).resize((112, 112))img.save(r"C:\dataset\{}_crop\{}\{}".format(d, name.split('/')[0], name.split('/')[1] + '.jpg'))
代码比较简单,只需要注意两点:
- 人脸框转正方形有可能超出原图区域,超出部分自动填充为黑色。
我代码中没有做处理,如果想要避免这种情况的话,可以选择根据越界的尺寸将框偏移回原图,实现并不复杂。也可以选择简单地将越界的框舍弃,毕竟偏移后的框人脸特征还是有些不一样的,数据集每个人平均有300+张图片,可以挥霍一点点。 - 舍弃了人脸尺寸小于100的框。
这么做的原因是人脸识别的分类难度较大,需要比较丰富的人脸特征,如果原人脸尺寸过小,网络难以学习。这个筛选尺寸的阈值自己给定,也可以给112,但不能太小。
我实现的只是人脸识别流程,如果想要提升识别精度,可以尝试各种Data Augmentation(数据增强)。如果和我一样电脑配置奇差,可以把生成人脸图片保存在C盘(一般是固态硬盘),可以提升一些训练速度。
数据采样
from torch.utils.data import Dataset
from torchvision import transforms
from PIL import Image
import osclass MyDataset(Dataset):def __init__(self, main_dir, person_num, is_train=True):self.main_dir = main_dird = "train" if is_train else "test"self.dir = os.path.join(main_dir, d)self.dataset = []for i, person in enumerate(os.listdir(self.dir)):if i == person_num:breakfor img_name in os.listdir(os.path.join(self.dir, person)):img_path = os.path.join(self.dir, person, img_name)self.dataset.append([img_path, i])def __len__(self):return len(self.dataset)def __getitem__(self, index):img_path, label = self.dataset[index]tf = transforms.Compose([transforms.ToTensor(),transforms.Normalize((0.5, ), (0.5, ))])image_data = tf(Image.open(img_path))return image_data, label
没什么好讲的,甩代码。整个人脸训练数据集近9000个人,全部训练可能太多了,参数person_num用来控制采样人数。参数is_train控制样本来自训练数据集还是测试数据集。
网络模型
import torchvision.models as models
from torch import nn
import torch
from torch.nn import functional as Fclass ArcNet(nn.Module):def __init__(self, feature_num, cls_num):super(ArcNet, self).__init__()self.w = nn.Parameter(torch.randn((feature_num, cls_num)), requires_grad=True)self.func = nn.Softmax()def forward(self, x, s=64, m=0.5):x_norm = F.normalize(x, dim=1)w_norm = F.normalize(self.w, dim=0)cosa = torch.matmul(x_norm, w_norm) / sa = torch.acos(cosa)arcsoftmax = torch.exp(s * torch.cos(a + m)) / (torch.sum(torch.exp(s * cosa), dim=1, keepdim=True) - torch.exp(s * cosa) + torch.exp(s * torch.cos(a + m)))return arcsoftmaxclass FaceNet(nn.Module):def __init__(self):super(FaceNet, self).__init__()self.sub_net = nn.Sequential(models.mobilenet_v2(pretrained=True),)self.feature_net = nn.Sequential(nn.BatchNorm1d(1000),nn.PReLU(),nn.Linear(1000, 512, bias=False),)self.arc_net = ArcNet(512, 500)def forward(self, x):y = self.sub_net(x)feature = self.feature_net(y)return feature, self.arc_net(feature)def encode(self, x):return self.feature_net(self.sub_net(x))
ArcNet网络同上文所述,主网络FaceNet我选择调用了Pytorch内置的mobilenet_v2,可以加载预训练参数,缩短训练时间,训练精度比较高,但实际识别效果不好说……
encode()方法用来提取512维的人脸特征向量,为了保证识别效果,一般特征维数不低于128,维数越大特征越丰富,但识别的时候计算量越大,速度越慢,因此一般也不会高于512。
最后输出的500指的分类数,和训练时采样的person_num一致。
训练
损失
人脸识别是分类问题,损失函数自然选择交叉熵损失。
Pytorch中的 nn.CrossEntropyLoss() = torch.log(torch.softmax(x))+nn.NLLLoss()。
因为我们加Arcfaceloss时,用ArcNet代替了原来普通的Softmax函数,因此在训练时不能直接调用CrossEntropyLoss()函数,改为将输出经log()函数后用NLLLoss()处理。
问题
实现中一些问题我是从0开始考虑的,比如最重要的一个:
特征提取器什么时候算训练好了,用什么指标衡量?
因为这次的训练目标和往常的分类项目是不同的,我们并不是只追求训练数据的分类精度,而是网络的人脸特征提取能力,这二者并不完全等价。
因此在验证集的设置上我做了改动。正常的验证集应该是将同一个人的人脸图片划分成训练和验证两部分,一般以预测的accuracy(准确度)或者mAP作为度量指标。而为了衡量特征提取能力,我选择模仿真实使用中的识别要求进行验证。
验证步骤:
- 加载测试集中的n个人的人脸图片,这些图片不在训练的人脸类别之中。
- 找到每个人的第一张人脸图片,作为这个人的识别标签。用这一轮训练好的网络encode()方法得到这n个人第一张人脸的512维特征,相当于特征库。
- 将加载的所有人脸图片都通过网络提取特征,所有人脸特征和2中的n个特征标签依次计算相似度,每一张图片得到n个相似度,取其中最大相似度对应的特征标签,作为该张图片的预测类别,以此计算accuracy。
这样验证其实还算不上非常严谨,依然有些小问题。比如只选了第一张图片作为标签,可能有偶然因素,不够准确。我们希望不仅仅预测正确,而且预测正确的最大相似度与其他相似度的差距能拉开,理想效果是同一个人的相似度趋近于1,非同一个人的相似度趋近于0,这种差距的衡量我在验证中尚未考虑。但相对普通验证方法,已经明显提升了参考价值。
另外还有一个问题,参与训练的类别数定多少合适?显然类别越多,每轮训练时间越长,训练难度越大,训练精度越低,而特征提取能力未必更好。如果没有真实的人脸识别项目经验,这个类别数就近似于一个超参数了,只能去试……按照我这台老年机艰难的实验结果,类别数最好不要超过2000。
import torch
import torch.nn as nn
from torch.utils.data import DataLoader
from net import FaceNet
import os
import numpy as np
from dataset import MyDataset
import matplotlib.pyplot as pltdef gen_lib(net):# 找到每个类别的第一张人脸 作为模板lib = []n = 0for e in valid_dataset:if e[1] == n:lib.append(net.encode(e[0][None, ...].to(device)).squeeze())n += 1return torch.stack(lib)if __name__ == '__main__':save_path = "params/net_face.pth"train_dataset = MyDataset(r"C:\vgg_face2", person_num=2000, is_train=True)train_loader = DataLoader(train_dataset, batch_size=150, shuffle=True, num_workers=4, pin_memory=True)valid_dataset = MyDataset(r"C:\vgg_face2", person_num=200, is_train=False)valid_loader = DataLoader(valid_dataset, batch_size=150, shuffle=False, num_workers=4, pin_memory=True)device = torch.device("cuda" if torch.cuda.is_available() else "cpu")net = FaceNet().to(device)if os.path.exists(save_path):net.load_state_dict(torch.load(save_path))else:print("NO Param")lossfn_cls = nn.NLLLoss()optimzer = torch.optim.Adam(net.parameters())train_acc_list = []valid_acc_list = []epoch = 1while True:print("epoch:", epoch)net.train()correct = 0for i, (x, y) in enumerate(train_loader):x = x.to(device)y = y.to(device)feature, cls = net(x)loss = lossfn_cls(torch.log(cls), y)optimzer.zero_grad()loss.backward()optimzer.step()correct += (torch.argmax(cls, 1) == y).sum().item()if i % 300 == 0:print("progress:\t{:.3f}".format((i * train_loader.batch_size + y.shape[0]) / len(train_dataset)))train_acc = torch.true_divide(correct, len(train_dataset))train_acc_list.append(train_acc)print("train acc:\t{:.2f}%".format(train_acc * 100))net.eval()correct = 0with torch.no_grad():feature_lib = nn.functional.normalize(gen_lib(net), dim=1)for i, (x, y) in enumerate(valid_loader):x = x.to(device)y = y.to(device)feature, cls = net(x)# (batch, 512) × (512, cls) = (batch, cls)features = nn.functional.normalize(feature)m = torch.matmul(features, feature_lib.T)correct += (m.argmax(dim=1) == y).sum().item()valid_acc = torch.true_divide(correct, len(valid_dataset))valid_acc_list.append(valid_acc)print("valid acc:\t{:.2f}%".format(valid_acc * 100))save_path = "params/net_face_{}.pth".format(epoch)torch.save(net.state_dict(), save_path)plt.clf()plt.plot(train_acc_list, label='train_acc')plt.plot(valid_acc_list, label='valid_acc')plt.title('accuracy')plt.legend()plt.savefig('graph/acc_{}'.format(epoch))epoch += 1
上图为2000个人训练,200个人验证的效果。因为类别数有差距,训练和验证精度的相对关系没有意义,我们只需要看单条曲线即可。根据验证精度的曲线,约在5轮之后就开始下降了,我实际使用也确实是5轮左右的效果比较好。由此也验证了:训练精度不等价于特征提取能力。
使用
验证过程和实际的使用过程已经比较相近了,区别就在于特征库的建立和读取,以及真实特征库中一个人可能不止保存了一个特征向量,此时如何对比识别的问题。
对于特征库的形式,我选择用最简单的Python字典结构实现,Python 3.6版本以后字典变成有序了,键和值分别转成列表后保持了一一对应关系,方便我们取值计算并找到对应类别。
为了提升检测速度,相似度的计算一定要用矩阵并行完成,不能用循环。同时多个向量要能得到同一个类别。我用的也是最简单的办法,保存字典如{“张三_0”:向量0,“张三_1”:向量1,……},预测时得到键"张三_1"后做字符串分割就能得到"张三"。
识别代码如下,boxes为当前图片经MTCNN后得到的数个检测框,net为已经训练好的网络模型,lib为读取的字典特征库。如果最大的相似度低于0.75(给定阈值),则认为当前人脸不在特征库中,预测值为“陌生人”。得到的预测值列表people和检测框列表boxes索引一致,画图时对应取出即可
def recognize(image, boxes):people = []tf = transforms.Compose([transforms.Resize((112, 112)),transforms.ToTensor(),transforms.Normalize((0.5,), (0.5,))])for box in boxes:x1 = int(box[0])y1 = int(box[1])x2 = int(box[2])y2 = int(box[3])l = max(x2 - x1, y2 - y1)cx = (x1 + x2) // 2cy = (y1 + y2) // 2_x1 = cx - l // 2_x2 = _x1 + l_y1 = cy - l // 2_y2 = _y1 + limg_crop = image.crop((_x1, _y1, _x2, _y2))face_feature = nn.functional.normalize(net.encode(tf(img_crop).to(device)[None, ...]))current_feature = nn.functional.normalize(torch.tensor(list(lib.values())).to(device), dim=1)similarity = torch.matmul(face_feature, current_feature.T)[0]max_score = torch.max(similarity).item()print(max_score)if max_score < 0.75:print(similarity)people.append("陌生人")else:idx = torch.argmax(similarity).item()pred_person = list(lib.keys())[idx].split('_')[0] + ' ' + str(np.round(max_score, 2))torch.set_printoptions(sci_mode=False)print(similarity)people.append(pred_person)return people
摄像头实时检测
实现视频检测比较简单,用OpenCV读取摄像头视频,得到每帧图像后,再按上述的单张图片识别流程就好了。
为了实现实时,就比较困难了,需要对速度做各种优化。比如视频检测中其实没必要对每一帧都做识别,一般每秒识别3次就基本能达到要求了,每一帧都识别的话会非常耗时。对于速度的优化可以从很多方面入手,如网络模型优化、特征库优化、MTCNN图像金字塔优化、多线程优化等等,比较复杂,这里就不详述了。我在老年机上实现的摄像头检测,平均每秒在21帧左右,勉强可以算实时。
本人的绝世容颜不能轻露,还是祭出小团团叭~
【人脸识别】MTCNN + Arcface全流程详解 Pytorch代码 损失函数发展相关推荐
- 计算机辅助测试普通话考试流程,必看!普通话考试全流程详解!
原标题:必看!普通话考试全流程详解! 2019 普通话考试指南 现在距离11月9号普通话考试只有3天了,但是小伙伴们,你们知道考试流程和检测的方式吗?所以今天我就来为第一次参加普通话考试的同学来科普一 ...
- 嘉立创电路板制作过程全流程详解(二):沉铜、线路
上一篇文章,我们了解了第1道和第2道工序,MI和钻孔,这篇文章,我们将了解第3道工序和第4道工序:沉铜和线路. 看上一篇文章,点击这里: 嘉立创电路板制作过程全流程详解(一):MI.钻孔 第3道工序: ...
- 嘉立创电路板制作过程全流程详解(五):测试、锣边、V-CUT、QC、发货
第1篇文章,点击这里:嘉立创电路板制作过程全流程详解(一):MI.钻孔 第2篇文章,点击这里:嘉立创电路板制作过程全流程详解二:沉铜.线路 第3篇文章,点击这里:嘉立创电路板制作过程全流程详解三:图电 ...
- 163邮箱域名大全,163邮箱注册申请全流程详解!
163邮箱域名大全,163邮箱注册申请全流程详解.从免费版到收费版邮箱,用了20年,电子邮箱行业发展至今,越来越的个人白领倾向选择付费邮箱.最近听说TOM VIP邮箱刚上线了163vip.com全新后 ...
- 嘉立创电路板制作过程全流程详解(四):阻焊、字符、喷锡或沉金
第1篇文章,点击这里:嘉立创电路板制作过程全流程详解(一):MI.钻孔 第2篇文章,点击这里:嘉立创电路板制作过程全流程详解二:沉铜.线路 第3篇文章,点击这里:嘉立创电路板制作过程全流程详解三:图电 ...
- 企业级superset阿里云ESC搭建全流程详解
企业级superset阿里云ESC搭建全流程详解 你好! 作为一名大数据猿来说.你可能会需要轻量级大数据bi工具superset来帮助你快速实现数据的可视化展示.那么我们以阿里云云服务器ESC举例,帮 ...
- 付呗聚合支付快速教程 分账篇③——多商户模式下分账提现全流程详解
文章目录 一.前文 二.资金流转详解 2.1 各方关系 2.2 资金空间流转 2.3 资金时间流转 2.2 资金时空流转 三.付呗接口 四.软件流程 4.1 支付下单流程 4.2 支付查询流程 4.3 ...
- webpack loader配置全流程详解
前言 1.主要目的为稍微梳理从配置到装载的流程.另外详解当然要加点源码提升格调(本人菜鸟,有错还请友善指正) 2.被的WebPack打包的文件,都被转化为一个模块,比如import './xxx/x. ...
- 开发提交审核流程_小游戏上线发布全流程详解?
5G时代小游戏群雄并起 5G时代到来,各大超级App都推出了小游戏模式来把流量变现,如微信小游戏,QQ小游戏,抖音小游等. 之前个人开发者在国内上线游戏需要版号,到国外上线又不熟悉.现在微信/QQ/抖 ...
最新文章
- asp.net core中负载均衡场景下http重定向https的问题
- c语言中如何取消最后一个空格,新人提问:如何将输出时每行最后一个空格删除...
- java lambda表达式详解_Java8新特性:Lambda表达式详解
- python爬取抖音用户数据的单位是_爬取并分析一下B站的最热视频排行榜,看看大家都喜欢看什么视频...
- Objective-c编程语言(一):The Objective-C Programming Language:Introduction
- 流程控制语句反汇编(1)(Debug版)
- 必须收藏的MATLAB画图指南
- 计算机笔记本怎么保存文件,怎么将电脑上的文件保存在QQ邮箱?
- php表单提交的时候验证失败,解决有时首页表单提交“安全验证失败,请刷新页面后重新提交!”问题...
- Vue Router 路由(路由守卫)---route
- 程序员自编 “购房宝典” 火爆 GitHub !
- CCCF“CNCC2017特邀报告”丘成桐:现代几何学与计算机科学
- 第一篇数模论文——估计出租车的总数
- AMD重新进入核心竞争领域
- 高密度无线覆盖解决方案
- turtle库基本介绍
- oracle SQL update一次性修改多个字段,数据来源于另一张表
- 【软件质量保证与测试】2.4软件测试与软件开发的关系
- push代码到git上报错failed to push some refs to ‘远程仓库地址‘问题
- 清理计算机磁盘碎片,电脑磁盘碎片清理我帮你
热门文章
- 如何使用Blender制作卡通三渲二效果
- 【51单片机】160个51单片机案例,适用于初学者。
- hash哈希算法,MD5、SHA1、SHA512、SHA256
- OpenCV__Python sobel算子_教程19
- dlp数据防泄漏(dlp数据防泄漏系统可以监控个人电脑吗)
- 深度补全(Sparsity Invariant CNNs)-论文阅读-翻译
- Tableau百分比完成进度条制作
- 深度剖析“App Store”模式(《通信世界周刊》版)
- Java 中正则表达式的详解
- 摘:轻而易举拥有xp风格的界面