首页
壁纸
留言板
友链
更多
统计归档
Search
1
TensorBoard:训练日志及网络结构可视化工具
12,588 阅读
2
主板开机跳线接线图【F_PANEL接线图】
7,033 阅读
3
Linux使用V2Ray 原生客户端
6,149 阅读
4
移动光猫获取超级密码&开启公网ipv6
4,676 阅读
5
NVIDIA 显卡限制功率
3,131 阅读
好物分享
实用教程
linux使用
wincmd
学习笔记
mysql
java学习
nginx
综合面试题
大数据
网络知识
linux
放码过来
python
javascript
java
opencv
蓝桥杯
leetcode
深度学习
开源模型
相关知识
数据集和工具
模型轻量化
语音识别
计算机视觉
杂七杂八
硬件科普
主机安全
嵌入式设备
其它
bug处理
登录
/
注册
Search
标签搜索
好物分享
学习笔记
linux
MySQL
nvidia
typero
内网穿透
webdav
vps
java
cudann
gcc
cuda
树莓派
CNN
图像去雾
ssh安全
nps
暗通道先验
阿里云
jupiter
累计撰写
354
篇文章
累计收到
71
条评论
首页
栏目
好物分享
实用教程
linux使用
wincmd
学习笔记
mysql
java学习
nginx
综合面试题
大数据
网络知识
linux
放码过来
python
javascript
java
opencv
蓝桥杯
leetcode
深度学习
开源模型
相关知识
数据集和工具
模型轻量化
语音识别
计算机视觉
杂七杂八
硬件科普
主机安全
嵌入式设备
其它
bug处理
页面
壁纸
留言板
友链
统计归档
搜索到
62
篇与
的结果
2022-01-16
TensorBoard:训练日志及网络结构可视化工具
1.安装pip install tensorboard2.可视化标量数据(loss和accuracy)from torch.utils.tensorboard import SummaryWriter import random # 实例化TensorBoard logs_writer = SummaryWriter('./logs') # 可视化标量数据 for epoch_id in range(100): logs_writer.add_scalar("train/loss",random.random(),epoch_id) logs_writer.add_scalar("train/accuracy",random.random(),epoch_id) logs_writer.add_scalar("test/loss",random.random(),epoch_id) logs_writer.add_scalar("test/accuracy",random.random(),epoch_id)tensorboard --logdir=logs --bind_all3.可视化网络结构""" 可视化网络结构 """ import torch import torch.nn as nn from torch.utils.tensorboard import SummaryWriter # 实例化TensorBoard writer = SummaryWriter('./model_vis') # 构建网络模型 - 使用自定义类 class Digit_Rec(nn.Module): def __init__(self): super(Digit_Rec,self).__init__() self.conv1 = nn.Conv2d(1,10,5) #1:灰度图片的通道,10:输出通道,5:kernel self.relu1 = nn.ReLU() self.max_pool = nn.MaxPool2d(2,2) self.conv2 = nn.Conv2d(10,20,3) #10:输入通道,20:输出通道,3:Kernel self.relu2 = nn.ReLU() self.fc1 = nn.Linear(20*10*10,500) # 20*10*10:输入通道,500:输出通道 self.relu3 = nn.ReLU() self.fc2 = nn.Linear(500,10) # 500:输入通道,10:输出通道 self.relu4 = nn.ReLU() self.softmax = nn.Softmax(dim=1) def forward(self,x): batch_size = x.size(0) # x的格式:batch_size x 1 x 28 x 28 拿到了batch_size x = self.conv1(x) # 输入:batch*1*28*28 输出:batch*10*24*24 x = self.relu1(x) x = self.max_pool(x) # 输入:batch*10*24*24输出:batch*10*12*12 x = self.conv2(x) x = self.relu2(x) x = x.view(batch_size,-1) #fatten 展平 -1自动计算维度,20*10*10=2000 x = self.fc1(x) # 输入:batch*2000 输出:batch*500 x = self.relu3(x) x = self.fc2(x) # 输入:batch*500 输出:batch*10 x = self.relu4(x) output = self.softmax(x) # 计算分类后,每个数字的概率值 return output model = Digit_Rec() model = Digit_Rec() images = torch.randn(1, 1, 28, 28) writer.add_graph(model, images) writer.close()tensorboard --logdir=model_vis --bind_all参考资料Pytorch中使用tensorboard学习笔记(2)记录损失loss和准确率accuracypytorch中使用tensorboard绘制Accuracy/Loss曲线(train和test显示在同一幅图中)pytorch中使用TensorBoard进行可视化Loss及特征图
2022年01月16日
12,588 阅读
4 评论
0 点赞
2022-01-08
语音识别:使用torchaudio快速实现音频特征提取
1.fbank特征import torch.nn as nn import torchaudio class ExtractAudioFeature(nn.Module): def __init__(self, feat_type="fbank", feat_dim=40): super(ExtractAudioFeature, self).__init__() self.feat_type = feat_type self.extract_fn = torchaudio.compliance.kaldi.fbank if feat_type == "fbank" else torchaudio.compliance.kaldi.mfcc self.num_mel_bins = feat_dim def forward(self, filepath): waveform, sample_rate = torchaudio.load(filepath) y = self.extract_fn(waveform, num_mel_bins=self.num_mel_bins, channel=-1, sample_frequency=sample_rate, frame_length=25, #每帧的时长 frame_shift=10, dither=0) return y.transpose(0, 1).unsqueeze(0).detach() extracter = ExtractAudioFeature("fbank",feat_dim=40) wav = "./data/wav/day0914_990.wav" wav_feature = extracter(wav) print(wav_feature.shape)torch.Size([1, 40, 489]) # 40:特征维度 # 489:音频帧数=音频时长/25ms查看图示import matplotlib.pyplot as plt plt.figure(dpi=200) plt.xticks([]) plt.yticks([]) plt.imshow(wav_feature[0]) plt.show()2.mfcc特征import torch.nn as nn import torchaudio class ExtractAudioFeature(nn.Module): def __init__(self, feat_type="mfcc", feat_dim=13): super(ExtractAudioFeature, self).__init__() self.feat_type = feat_type self.extract_fn = torchaudio.compliance.kaldi.fbank if feat_type == "fbank" else torchaudio.compliance.kaldi.mfcc self.num_mel_bins = feat_dim def forward(self, filepath): waveform, sample_rate = torchaudio.load(filepath) y = self.extract_fn(waveform, num_mel_bins=self.num_mel_bins, channel=-1, sample_frequency=sample_rate, frame_length=25, #每帧的时长 frame_shift=10, dither=0) return y.transpose(0, 1).unsqueeze(0).detach() extracter = ExtractAudioFeature("mfcc",feat_dim=13) wav = "./data/wav/day0914_990.wav" wav_feature = extracter(wav) print(wav_feature.shape)torch.Size([1, 13, 489]) # 13:特征维度 # 489:音频帧数=音频时长/25ms查看图示import matplotlib.pyplot as plt plt.figure(dpi=200) plt.xticks([]) plt.yticks([]) plt.imshow(wav_feature[0]) plt.show()参考资料https://github.com/neil-zeng/asr
2022年01月08日
1,532 阅读
0 评论
0 点赞
2022-01-08
pytorch 逐层加载模型参数
pytorch 逐层加载模型参数1.现象描述当对模型结构的某些层做了微小调整之后,导致该层参数的shape发生了微小变化导致无法对整个模型进行加载,这时可以考虑通过逐层加载的方式来跳过某些层完成对保存好的模型的加载。2.代码实现# 模型参数逐层加载 ckpt_state_dict = torch.load("ckpt/best_att_wer_0.015267175572519083.pth")["model"] model_state_dict = model.state_dict() for key in model_state_dict.keys(): if model_state_dict[key].shape == ckpt_state_dict[key].shape: model_state_dict[key] = ckpt_state_dict[key] model.load_state_dict(model_state_dict)
2022年01月08日
701 阅读
0 评论
0 点赞
2022-01-07
Yolov5s:Yolov5sv6.0网络结构分析与实现
1.参考网络结构图(v5.0的)2. 配置文件解析原始配置文件yolov5s.yaml# YOLOv5 by Ultralytics, GPL-3.0 license # Parameters nc: 80 # number of classes depth_multiple: 0.33 # model depth multiple width_multiple: 0.50 # layer channel multiple anchors: - [10,13, 16,30, 33,23] # P3/8 - [30,61, 62,45, 59,119] # P4/16 - [116,90, 156,198, 373,326] # P5/32 # YOLOv5 v6.0 backbone backbone: # [from, number, module, args] [[-1, 1, Conv, [64, 6, 2, 2]], # 0-P1/2 [-1, 1, Conv, [128, 3, 2]], # 1-P2/4 [-1, 3, C3, [128]], [-1, 1, Conv, [256, 3, 2]], # 3-P3/8 [-1, 6, C3, [256]], [-1, 1, Conv, [512, 3, 2]], # 5-P4/16 [-1, 9, C3, [512]], [-1, 1, Conv, [1024, 3, 2]], # 7-P5/32 [-1, 3, C3, [1024]], [-1, 1, SPPF, [1024, 5]], # 9 ] # YOLOv5 v6.0 head head: [[-1, 1, Conv, [512, 1, 1]], [-1, 1, nn.Upsample, [None, 2, 'nearest']], [[-1, 6], 1, Concat, [1]], # cat backbone P4 [-1, 3, C3, [512, False]], # 13 [-1, 1, Conv, [256, 1, 1]], [-1, 1, nn.Upsample, [None, 2, 'nearest']], [[-1, 4], 1, Concat, [1]], # cat backbone P3 [-1, 3, C3, [256, False]], # 17 (P3/8-small) [-1, 1, Conv, [256, 3, 2]], [[-1, 14], 1, Concat, [1]], # cat head P4 [-1, 3, C3, [512, False]], # 20 (P4/16-medium) [-1, 1, Conv, [512, 3, 2]], [[-1, 10], 1, Concat, [1]], # cat head P5 [-1, 3, C3, [1024, False]], # 23 (P5/32-large) [[17, 20, 23], 1, Detect, [nc, anchors]], # Detect(P3, P4, P5) ]配置文件解析Model( (model): Sequential( (0): Conv(3,32,6,2,2) # 3x640x640-->32x320x320 (1): Conv(32,64,3,2) # 32x320x320-->64x160x160 (2): C3(64,64) # 64x160x160-->64x160x160 (3): Conv(64,128,3,2) # 64x160x160-->128x80x80 #P3 (4): C3(128,128) # 128x80x80-->128x80x80 (5): Conv(128,256,3,2) # 128x80x80-->256x40x40 #P4 (6): C3(256,256) # 256x40x40-->256x40x40 (7): Conv(256,512,3,2) # 256x40x40-->512x20x20 #P5 (8): SPP(512,512,[5, 9, 13]) # 512x20x20-->512x20x20 (9): C3(512,512) # 512x20x20-->512x20x20 #P6 (10): Conv(512,256,1,1) # 512x20x20-->256x20x20 (11): nn.Upsample(None, 2, 'nearest') # 256x20x20-->256x40x40 (12): Concat() # [x,p4]==>512x40x40 (13): C3(512,256) # 512x40x40-->256x40x40 (14): Conv(256,128) # 256x40x40-->128x40x40 (15): nn.Upsample(None, 2, 'nearest') # 128x40x40-->128x80x80 (16): Concat() # [x,p3]==>256x80x80 (17): C3(256,128) # 256x80x80-->128x80x80 #out1 (18): Conv(128,128,3,2) # 128x80x80-->128x40x40 (19): Concat() # [x,p4]==>384x40x40 (20): C3(384,256) # 384x40x40-->256x40x40 #out2 (21): Conv(256,256,3,2) # 256x40x40-->256x20x20 (22): Concat() # [x,p5]==>768x20x20 (23): C3(768,512) # 768x20x20 -->512x20x20 #out3 (24): Detect( (0): Conv2d(128, 255) # 128x80x80-->((cls_num+4+1)*anchor_num)x80x80 #out1_detect==>[3, 80, 80, 85] (1): Conv2d(256, 255) # 256x40x40-->((cls_num+4+1)*anchor_num)x40x40 #out2_detect==>[3, 40, 40, 85] (2): Conv2d(512, 255) # 512x20x20-->((cls_num+4+1)*anchor_num)x20x20 #out3_detect==>[3, 20, 20, 85] ) ) )3.代码实现3.1 公共基本块import torch import torch.nn as nn import warnings class Conv(nn.Module): # 标准卷积 def __init__(self, c1, c2, k=1, s=1, p=None, g=1, act=True): # ch_in, ch_out, kernel, stride, padding, groups super().__init__() self.conv = nn.Conv2d(c1, c2, k, s, autopad(k, p), groups=g, bias=False) self.bn = nn.BatchNorm2d(c2) self.act = nn.SiLU() if act is True else (act if isinstance(act, nn.Module) else nn.Identity()) def forward(self, x): return self.act(self.bn(self.conv(x))) def forward_fuse(self, x): return self.act(self.conv(x)) def forward_fuse(self, x): return self.act(self.conv(x)) class Bottleneck(nn.Module): # 标准bottleneck def __init__(self, c1, c2, shortcut=True, g=1, e=0.5): # ch_in, ch_out, shortcut, groups, expansion super().__init__() c_ = int(c2 * e) # hidden channels self.cv1 = Conv(c1, c_, 1, 1) self.cv2 = Conv(c_, c2, 3, 1, g=g) self.add = shortcut and c1 == c2 def forward(self, x): return x + self.cv2(self.cv1(x)) if self.add else self.cv2(self.cv1(x)) class C3(nn.Module): # CSP Bottleneck with 3 convolutions def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5): # ch_in, ch_out, number, shortcut, groups, expansion super().__init__() c_ = int(c2 * e) # hidden channels self.cv1 = Conv(c1, c_, 1, 1) self.cv2 = Conv(c1, c_, 1, 1) self.cv3 = Conv(2 * c_, c2, 1) # act=FReLU(c2) self.m = nn.Sequential(*[Bottleneck(c_, c_, shortcut, g, e=1.0) for _ in range(n)]) # self.m = nn.Sequential(*[CrossConv(c_, c_, 3, 1, g, 1.0, shortcut) for _ in range(n)]) def forward(self, x): return self.cv3(torch.cat((self.m(self.cv1(x)), self.cv2(x)), dim=1)) class SPPF(nn.Module): # Spatial Pyramid Pooling - Fast (SPPF) layer for YOLOv5 by Glenn Jocher def __init__(self, c1, c2, k=5): # equivalent to SPP(k=(5, 9, 13)) super().__init__() c_ = c1 // 2 # hidden channels self.cv1 = Conv(c1, c_, 1, 1) self.cv2 = Conv(c_ * 4, c2, 1, 1) self.m = nn.MaxPool2d(kernel_size=k, stride=1, padding=k // 2) def forward(self, x): x = self.cv1(x) with warnings.catch_warnings(): warnings.simplefilter('ignore') # suppress torch 1.9.0 max_pool2d() warning y1 = self.m(x) y2 = self.m(y1) return self.cv2(torch.cat([x, y1, y2, self.m(y2)], 1)) class Concat(nn.Module): # 沿维度连接张量列表 def __init__(self, dimension=1): super().__init__() self.d = dimension def forward(self, x): return torch.cat(x, self.d) def autopad(k, p=None): # kernel, padding # 计算然卷积结果与输入具有相同大小的padding if p is None: p = k // 2 if isinstance(k, int) else [x // 2 for x in k] # auto-pad return p3.2 backboneclass Yolov5sV6Backbone(nn.Module): def __init__(self): super(Yolov5sV6Backbone,self).__init__() self.backbone_part1 = nn.Sequential( Conv(3,32,6,2,2), # 0 Conv(32,64,3,2), # 1 C3(64,64), #2 Conv(64,128,3,2) #3 ) self.backbone_part2 = nn.Sequential( C3(128,128), # 4_1 Conv(128,256,3,2) # 5 ) self.backbone_part3 = nn.Sequential( C3(256,256), # 6 Conv(256,512,3,2) # 7 ) self.backbone_part4 = nn.Sequential( C3(512,512), # 8 SPPF(512,512,5), # 9 ) def forward(self,x): p3 = self.backbone_part1(x) p4 = self.backbone_part2(p3) p5 = self.backbone_part3(p4) p6 = self.backbone_part4(p5) return p3,p4,p5,p6调用测试backbone = Yolov5sV6Backbone() fake_input = torch.rand(1,3,640,640) p3,p4,p5,p6 = backbone(fake_input) print(p3.shape,p4.shape,p5.shape,p6.shape)torch.Size([1, 128, 80, 80]) torch.Size([1, 256, 40, 40]) torch.Size([1, 512, 20, 20]) torch.Size([1, 512, 20, 20])3.3 headclass Yolov5sV6Head(nn.Module): def __init__(self): super(Yolov5sV6Head,self).__init__() self.head_part1 = nn.Sequential( Conv(512,256,1,1), # 10 nn.Upsample(None, 2, 'nearest') # 11 ) self.head_concat1 =Concat() # 12 self.head_part2 = nn.Sequential( C3(512,256), # 13 Conv(256,128), # 14 nn.Upsample(None, 2, 'nearest') # 15 ) self.head_concat2 = Concat() # 16 self.head_out1 = C3(256,128) # 17 # 128x80x80 self.head_part3 = Conv(128,128,3,2) # 18 self.head_concat3 = Concat() # 19 self.head_out2 = C3(384,256) # 20 self.head_part4 = Conv(256,256,3,2) # 21 self.head_concat4 = Concat() # 22 self.head_out3 = C3(768,512) # 23 # 512x40x40 def forward(self,p3,p4,p5,x): x = self.head_part1(x) x = self.head_concat1([x,p4]) x = self.head_part2(x) x = self.head_concat2([x,p3]) out1 = self.head_out1(x) x = self.head_part3(out1) x = self.head_concat3([x,p4]) out2 = self.head_out2(x) x = self.head_part4(out2) x = self.head_concat4([x,p5]) out3 = self.head_out3(x) return out1,out2,out3调用测试backbone = Yolov5sV6Backbone() head = Yolov5sV6Head() fake_input = torch.rand(1,3,640,640) p3,p4,p5,p6 = backbone(fake_input) out1,out2,out3 = head(p3,p4,p5,p6) print(out1.shape,out2.shape,out3.shape)3.4 detect 部分class Yolov5sV6Detect(nn.Module): stride = None # strides computed during build def __init__(self, nc=80, anchors=(), ch=[128,256,512], inplace=True): # detection layer super(Yolov5sV6Detect,self).__init__() self.nc = nc # number of classes self.no = nc + 5 # number of outputs per anchor self.nl = len(anchors) # number of detection layers self.na = len(anchors[0]) // 2 # number of anchors self.grid = [torch.zeros(1)] * self.nl # init grid self.anchor_grid = [torch.zeros(1)] * self.nl # init anchor grid self.register_buffer('anchors', torch.tensor(anchors).float().view(self.nl, -1, 2)) # shape(nl,na,2) self.m = nn.ModuleList(nn.Conv2d(x, self.no * self.na, 1) for x in ch) # output conv self.inplace = inplace # use in-place ops (e.g. slice assignment) def forward(self, x): z = [] # inference output for i in range(self.nl): x[i] = self.m[i](x[i]) # conv bs, _, ny, nx = x[i].shape # x(bs,255,20,20) to x(bs,3,20,20,85) x[i] = x[i].view(bs, self.na, self.no, ny, nx).permute(0, 1, 3, 4, 2).contiguous() if not self.training: # inference if self.grid[i].shape[2:4] != x[i].shape[2:4]: self.grid[i], self.anchor_grid[i] = self._make_grid(nx, ny, i) y = x[i].sigmoid() if self.inplace: y[..., 0:2] = (y[..., 0:2] * 2 - 0.5 + self.grid[i]) * self.stride[i] # xy y[..., 2:4] = (y[..., 2:4] * 2) ** 2 * self.anchor_grid[i] # wh else: # for YOLOv5 on AWS Inferentia https://github.com/ultralytics/yolov5/pull/2953 xy = (y[..., 0:2] * 2 - 0.5 + self.grid[i]) * self.stride[i] # xy wh = (y[..., 2:4] * 2) ** 2 * self.anchor_grid[i] # wh y = torch.cat((xy, wh, y[..., 4:]), -1) z.append(y.view(bs, -1, self.no)) return x if self.training else (torch.cat(z, 1), x) def _make_grid(self, nx=20, ny=20, i=0): d = self.anchors[i].device if check_version(torch.__version__, '1.10.0'): # torch>=1.10.0 meshgrid workaround for torch>=0.7 compatibility yv, xv = torch.meshgrid([torch.arange(ny, device=d), torch.arange(nx, device=d)], indexing='ij') else: yv, xv = torch.meshgrid([torch.arange(ny, device=d), torch.arange(nx, device=d)]) grid = torch.stack((xv, yv), 2).expand((1, self.na, ny, nx, 2)).float() anchor_grid = (self.anchors[i].clone() * self.stride[i]) \ .view((1, self.na, 1, 1, 2)).expand((1, self.na, ny, nx, 2)).float() return grid, anchor_grid调用测试anchors = [ [10,13, 16,30, 33,23], [30,61, 62,45, 59,119], [116,90, 156,198, 373,326] ] backbone = Yolov5sV6Backbone() head = Yolov5sV6Head() detect = Yolov5sV6Detect(nc=80,anchors=anchors) fake_input = torch.rand(1,3,640,640) p3,p4,p5,p6 = backbone(fake_input) out1,out2,out3 = head(p3,p4,p5,p6) out1,out2,out3 = detect([out1,out2,out3]) print(out1.shape,out2.shape,out3.shape)torch.Size([1, 3, 80, 80, 85]) torch.Size([1, 3, 40, 40, 85]) torch.Size([1, 3, 20, 20, 85])3.5 整体组装class Yolov5sV6(nn.Module): def __init__(self,nc=80,anchors=()): super(Yolov5sV6,self).__init__() self.backbone = Yolov5sV6Backbone() self.head = Yolov5sV6Head() self.detect = Yolov5sV6Detect(nc,anchors) def forward(self,x): p3,p4,p5,p6 = self.backbone(x) out1,out2,out3 = self.head(p3,p4,p5,p6) out1,out2,out3 = self.detect([out1,out2,out3]) return out1,out2,out3调用测试anchors = [ [10,13, 16,30, 33,23], [30,61, 62,45, 59,119], [116,90, 156,198, 373,326] ] yolov5s = Yolov5sV6(nc=80,anchors=anchors) fake_input = torch.rand(1,3,640,640) out1,out2,out3 = yolov5s(fake_input) print(out1.shape,out2.shape,out3.shape)torch.Size([1, 3, 80, 80, 85]) torch.Size([1, 3, 40, 40, 85]) torch.Size([1, 3, 20, 20, 85])4.模型复杂度分析模型名Input模型大小全精度模型大小半精度参数量FLOPSbackbone640x64026.0MB13.1MB3.80M4.42GFLOPShead640x64011.5MB5.78MB3.00M2.79GFLOPSdetect640x640897KB450KB0.23M0.37GFLOPSYolov5s640x64026.9M13.5M7.03M7.58GFLOPS参考资料https://github.com/ultralytics/yolov5YOLOv5代码详解(common.py部分)Yolov5从入门到放弃(一)---yolov5网络架构YOLOV5网络结构
2022年01月07日
2,116 阅读
0 评论
0 点赞
2021-12-30
端侧CNN参数压缩和加速(模型轻量化)技术
1.简介1.1 应用背景与云端的GPU设备相比,端侧的设备在成本和功耗方面具有明显优势。但在端侧的设备具有更小的内存,更小的算力,这对移动端的模型带来了更高的要求,端测模型需要满足模型尺寸小、计算复杂度低、电池耗电量低、部署灵活等条件。设备类型设备名显存/内存浮点算力GPURTX 308010G29117 GFLOPSGPURTX 2080 Ti11G16626 GFLOPS端侧设备树莓派4B2G/4G/8G13.5 GFLOPS1.2 模型压缩和加速介绍模型压缩和加速是两个不同的话题,压缩重点在于减少网络参数量,加速则侧重在降低计算复杂度、提升并行能力等。有时候压缩并不一定能带来加速的效果,有时候又是相辅相成的。模型压缩和加速可以从多个角度来优化。算法层压缩加速。这个维度主要在算法应用层,也是大多数算法工程师的工作范畴。主要包括结构优化(如矩阵分解、分组卷积、小卷积核等)、量化与定点化、模型剪枝、模型蒸馏等。框架层加速。这个维度主要在算法框架层,比如tf-lite、NCNN、MNN等。主要包括编译优化、缓存优化、稀疏存储和计算、NEON指令应用、算子优化等硬件层加速。这个维度主要在AI硬件芯片层,目前有GPU、FPGA、ASIC等多种方案,各种TPU、NPU就是ASIC这种方案,通过专门为深度学习进行芯片定制,大大加速模型运行速度。本文将重点针对算法层压缩加速进行介绍。2.算法层压缩加速2.1 轻量架构设计/结构优化2.1.1 分组卷积在分组卷积(Group convolution)中,输入特征图尺寸为 $H \times W \times c_1$,将输入特征图按照通道数分成 $g$组,则每组输入特征图的尺寸为$H \times W \times \frac{c_1}{g}$,对应的卷积核尺寸为 $h_1 \times w_1 \times \frac{c_1}{g}$ ,每组输出特征图尺寸为$H \times W \times \frac{c_2}{g}$,将$g$组结果拼接(concat),得到最终输出特征图尺寸为 $H \times W \times c_2$ ,因此分组卷积层的参数量为:$$ h_1 \times w_1 \times \frac{c_1}{g} \times \frac{c_2}{g} \times g = h_1 \times w_1 \times c_1 \times c_2 \times \frac{1}{g} $$即分组卷积的参数量是标准卷积的$\frac{1}{g}$。2.1.2 MobileNet结构MobileNetv1 论文该论文最大的创新点是,提出了深度可分离卷积(depthwise separable convolution)。同时作者还使用了ReLU6激活函数来替代ReLU激活函数。标准卷积在标准卷积中,输入特征图尺寸为 $H \times W \times c_1$,卷积核尺寸为 $h_1 \times w_1 \times c_1$ ,输出特征图尺寸为 $H \times W \times c_2$ ,标准卷积层的参数量为:$$ (h_1 \times w_1 \times c_1) \times c_2 $$运算量约为(忽略偏置计算):$$ w \times h \times c \times w_1 \times h_1 \times c_1 $$深度可分离卷积深度可分离卷(Depthwise separable convolution)积由逐通道卷积和逐点卷积两个部分组合而成,用来提取特征feature map。相比常规的卷积操作,其参数数量和运算成本比较低。逐通道卷积逐通道卷积(Depthwise Convolution)的一个卷积核负责一个通道,一个通道只被一个卷积核卷积。该阶段等价于$g=c_1$的组卷积,其参数量为:$$ h_1 \times w_1 \times 1 \times c_1 $$计算量为:$$ h_1 \times w_1 \times c_1 \times h_2 \times w_2 $$逐点卷积逐点卷积(Pointwise Convolution)的运算与常规卷积运算非常相似,它的卷积核的尺寸为 1×1×M,M为上一层的通道数。所以这里的卷积运算会将上一步的map在深度方向上进行加权组合,生成新的Feature map。有几个卷积核就有几个输出Feature map该阶段按普通卷积的方式计算可得参数量为:$$ 1 \times 1 \times c_1 \times c_2 $$计算量为:$$ h_2 \times w_2 \times c_1 \times c_2 $$总参数量与计算量分析则总的参数量为:$$ h_1 \times w_1 \times c_2 + c_1 \times c_2 $$即深度可分离卷积的参数量是标准卷积的$$ \frac{h_1 \times w_1 \times c_1 + c_1 \times c_2}{(h_1 \times w_1 \times c_1) \times c_2} = \frac{1}{c_2}+\frac{1}{h_1 \times w_1} $$总的计算量为:$$ h_1 \times w_1 \times c_1 \times h_2 \times w_2 +h_2 \times w_2 \times c_1 \times c_2 $$即深度可分离卷积的的计算量是标准卷积的:$$ \frac{h_1 \times w_1 \times c_1 \times h_2 \times w_2 +h_2 \times w_2 \times c_1 \times c_2}{h_1 \times w_1 \times c_1 \times h_2 \times w_2} = \frac{1}{c_2}+\frac{1}{h_1 \times w_1} $$ReLU6$$ ReLU = max(0,x) \\ ReLU6 = max(max(0,x),6) $$上图左边是普通的ReLU,对于大于0的值不进行处理,右边是ReLU6,当输入的值大于6的时候,返回6,relu6“具有一个边界”。作者认为ReLU6作为非线性激活函数,在低精度计算下具有更强的鲁棒性。(这里所说的“低精度”,我看到有人说不是指的float16,而是指的定点运算(fixed-point arithmetic))MobileNetv2 论文有人在实际使用深度可分离卷积的时候, 发现深度卷积部分的卷积核比较容易训废掉:训完之后发现深度卷积训出来的卷积核有不少是空的(参数都为0了),作者认为这是ReLU激活函数的锅。因此,作者将V2在V1的基础上,引入了Inverted Residuals(倒残差模块)和Linear Bottlenecks。简单来说,就是当低维信息映射到高维,经过ReLU后再映射回低维时,若映射到的维度相对较高,则信息变换回去的损失较小;若映射到的维度相对较低,则信息变换回去后损失很大,如下图所示。因此,作者认为对低维度做ReLU运算,很容易造成信息的丢失。而在高维度进行ReLU运算的话,信息的丢失则会很少。另外一种解释是,高维信息变换回低维信息时,相当于做了一次特征压缩,会损失一部分信息,而再进过ReLU后,损失的部分就更加大了。作者为了这个问题,就将ReLU替换成线性激活函数。Inverted Residuals/倒残差模块残差模块:输入首先经过$1 \times 1$的卷积进行压缩,然后使用$3 \times 3$的卷积进行特征提取,最后在用$1 \times 1$的卷积把通道数变换回去。整个过程是“压缩-卷积-扩张”。这样做的目的是减少$3 \times 3$模块的计算量,提高残差模块的计算效率。倒残差模块:输入首先经过$1 \times 1$的卷积进行通道扩张,然后使用$3 \times 3$的depthwise卷积,最后使用$1 \times 1$的pointwise卷积将通道数压缩回去。整个过程是“扩张-卷积-压缩”。为什么这么做呢?因为depthwise卷积不能改变通道数,因此特征提取受限于输入的通道数,所以将通道数先提升上去。文中的扩展因子为6。Linear Bottlenecks这个模块是为了解决上面提出的那个低维-高维-低维的问题,即将最后一层的ReLU替换成线性激活函数,而其他层的激活函数依然是ReLU6。如下图所示。当stride=1时,输入首先经过$1 \times 1$的卷积进行通道数的扩张,此时激活函数为ReLU6;然后经过$3 \times 3$的depthwise卷积,激活函数是ReLU6;接着经过$1 \times 1$的pointwise卷积,将通道数压缩回去,激活函数是linear;最后使用shortcut,将两者进行相加。而当stride=2时,由于input和output的特征图的尺寸不一致,所以就没有shortcut了。MobileNetv3 论文mobilenet V3并没有惊艳的结构提出,主要是一些tricks的应用和结合。v3版本结合了v1的深度可分离卷积、v2的Inverted Residuals和Linear Bottleneck、h-swish非线性变换、SE模块,利用NAS(神经结构搜索)来搜索网络的配置和参数。引入SE结构在bottlenet结构中加入了SE结构,并且放在了depthwise filter之后,如下图:因为SE结构会消耗一定的时间,所以作者在含有SE的结构中,将expansion layer的channel变为原来的1/4,这样作者发现,即提高了精度,同时还没有增加时间消耗。修改尾部结构在mobilenetv2中,在avg pooling之前,存在一个$1 \times 1$的卷积层,目的是提高特征图的维度,更有利于结构的预测,但是这其实带来了一定的计算量了,所以这里作者修改了,将其放在avg pooling的后面,首先利用avg pooling将特征图大小由$7 \times 7$降到了$1 \times 1$,降到$1 \times 1$后,然后再利用$1 \times 1$提高维度,这样就减少了$7 \times 7=49$倍的计算量。为了进一步的降低计算量,作者直接去掉了前面纺锤型卷积的$3 \times 3$以及$1 \times 1$卷积,进一步减少了计算量,就变成了如上图第二行所示的结构,作者将其中的$3 \times 3$以及$1 \times 1$去掉后,精度并没有得到损失。这里降低了大约15ms的速度。修改channel数量修改头部卷积核channel数量,mobilenet v2中使用的是$32 \times 3 \times 3$,作者发现,其实32可以再降低一点,所以这里作者改成了16,在保证了精度的前提下,降低了3ms的速度。非线性变换(新激活函数)的改变谷歌提出了一种新出的激活函数swish x(如下) 能有效改进网络精度,但是计算量太大了。$$ swish(x)=x \cdot \sigma(x) = x \cdot sigmoid(x) $$作者使用h-swish来近似替代sigmoid,进行了速度优化,具体如下:$$ h-swish(x)=x \frac{ReLU6(x+3)}{6} $$利用ReLU有几点好处:1.可以在任何软硬件平台进行计算2.量化的时候,它消除了潜在的精度损失,使用h-swish替换swish,在量化模式下回提高大约15%的效率,另外,h-swish在深层网络中更加明显。2.1.3 IGC结构IGCv1 论文它的重点在于提出了一个新颖的构建块--交错组卷积。交错组卷积它是一种交错组卷积块的堆叠结构。每个块包含两个组卷积: 第一次组卷积核第二次组卷积,所谓组卷积就是在卷积时有两个分组。第一次组卷积是把输入通道划分为L个组,每个组包含M个通道,然后将L个分组卷积的结果拼接在一起得到新的输入其仍然有LxM个通道之后打乱顺序第二次组卷积划分了M个组,每个组包含L个通道,且这L个通道来自第一次分组卷积时的不同组。同时第一次卷积做的是空间域卷积,第二次做的是逐点卷积。比起普通卷积,在网络参数量和计算复杂度不变的同时,使得网络变得更宽了。IGCv1 vs XceptionIGCV是第一个空间域的$3 \times 3$Group Conv,之后接第二个$1 \times 1$逐点卷积来融合通道信息,乍一看和Xception很像,可以说Xception就是IGCV的一种极端情况,Group数和Channel数相等的情况。作者列举了$L=1$以及$M=1$的极端情况,也就对应着普通卷积以及Xception的极端组卷积结果当然是IGCv1最好(摊手),从图上可以看到当每组只有2个通道的时候,效果是最好的,也就是文中最后IGCv1的结构了。最后还补充了两次组卷积次序可以交换,并不影响结果;可以将$3 \times 3$卷积分解成$3 \times 1$和$1 \times 3$,可以更加提高效率。IGCv2 论文IGCv2的主要创新点也就是在IGCv1的基础上,对于block里第二次组卷积再进行一次IGC。即使用多个稀疏卷积来替换原本比较稠密的卷积。IGCv1 vs IGCv2 IGCv1的结构如上图,可以看出第二次组卷积的时候,每一组的通道数仍然很多,文中的话就是比较dense, 因此想到对于每一组再进行IGC,这样就能进一步提升计算的效率了,也就是IGCv2,将每个组的普通卷积替换成交错组卷积,具体可见下图,IGCv3 论文IGCV3的主要创新点对IGCV2的结构进行延伸,引入低秩分组卷积来代替原本的分组卷积。就是先用1x1Group Pointwise Covolution升维,再3x3的组卷积,再通过1x1Group Pointwise Covolution进行降维,主要结构如下图与MobileNetV2的对比,基本上就是1x1卷积改成了1x1的组卷积。IGCV3在IGCV2的基础上融合了MobileNetV2的主要结构,并且使用更狠的低秩稀疏分组卷积,在整体结构上和MobileNetV2十分接近,核心依然是在稀疏分组卷积以及排序操作,虽然性能比MobileNetV2有些许提升,但整体的创新性略显不足。2.1.4 Inception结构Inception结构作用原理:Inception的作用就是替代了人工确定卷积层中过滤器的类型或者是否创建卷积层和池化层,让网络自己学习它具体需要什么参数。Inception-v1 论文提高网络最简单粗暴的方法就是提高网络的深度和宽度,即增加隐层和以及各层神经元数目。但这种简单粗暴的方法存在一些问题:会导致更大的参数空间,更容易过拟合需要更多的计算资源网络越深,梯度容易消失,优化困难(这时还没有提出BN时,网络的优化极其困难)Inception-v1的目标就是,提高网络计算资源的利用率,在计算量不变的情况下,提高网络的宽度和深度。首先看第一个结构,有四个通道,有$1 \times 1、3 \times 3、5 \times 5$卷积核,该结构有几个特点:采用大小不同的卷积核,意味着感受野的大小不同,就可以得到不同尺度的特征。采用比较大的卷积核即$5 \times 5$,因为有些相关性可能隔的比较远,用大的卷积核才能学到此特征。但是这个结构有个缺点,$5 \times 5$的卷积核的计算量太大。那么作者想到了第二个结构,用$1 \times 1$的卷积核进行降维。这个1*1的卷积核,它的作用就是:降低维度,减少计算瓶颈增加网络层数,提高网络的表达能力那么在具体的卷积神经网络中,Inception应该放在哪里,作者的建议,在底层保持传统卷积不变,在高层使用Inception结构。Inception-v2 论文其主要思想在于提出了Batch Normalization,其次才是稍微改进了一下Inception,把Inception-v1中$5 \times 5$的卷积用2个$3 \times 3$的卷积替换。这样做法有两个优点:保持相同感受野的同时减少参数加强非线性的表达能力Inception-v3 论文主要从分解卷积核尺寸、使用辅助分类器、改变降低特征图尺寸的方式三个方面进行改进(使用辅助分类器与轻量化设计关系不大)。分解卷积核尺寸分解为对称的小的卷积核,(参考上面的Inception-v2)分解为不对称的卷积核分解为不对称的卷积核就是将$n \times n$的卷积核替换成 $1 \times n$ 和$n \times 1$ 的卷积核堆叠,计算量又会降低。但是该方法在大维度的特征图上表现不好,在特征图12-20维度上表现好。不对称分解方法有几个优点:节约了大量的参数增加一层非线性,提高模型的表达能力可以处理更丰富的空间特征,增加特征的多样性改变降低特征图尺寸的方式在传统的卷积神经网络的中,当有pooling时(pooling层会大量的损失信息),会在之前增加特征图的厚度(就是双倍增加滤波器的个数),通过这种方式来保持网络的表达能力,但是计算量会大大增加。作者对传统的池化方式进行了如下的改进:有两个通道,一个是卷积层,一个是pooling层,两个通道生成的特征图大小一样,concat在一起即可。Inception-v4 [论文]()本文的主要思想是将Inception与很火的ResNet结合,与轻量化设计的关系不大。2.1.5 Xception结构Xception 论文Xception 是 Google 继 Inception 后提出的对 Inception-v3 的另一种改进。作者认为,通道之间的相关性 与 空间相关性 最好要分开处理。采用 Separable Convolution(极致的 Inception 模块)来替换原来 Inception-v3中的卷积操作。结构的变形过程如下:在 Inception 中,特征可以通过$1 \times 1$卷积, $3 \times 3$卷积, $5 \times 5$ 卷积,pooling 等进行提取,Inception 结构将特征类型的选择留给网络自己训练,也就是将一个输入同时输给几种提取特征方式,然后做 concat 。Inception-v3的结构图如下:对 Inception-v3 进行简化,去除 Inception-v3 中的 avg pool 后,输入的下一步操作就都是 $1 \times 1$卷积:提取$1 \times 1$卷积的公共部分:Xception(极致的 Inception):先进行普通卷积操作,再对$1 \times 1$卷积后的每个channel分别进行$3 \times 3$卷积操作,最后将结果 concat:2.1.6 ResNeXt结构ResNeXt 论文ResNeXt是ResNet和Inception的结合体,不同于Inception v4的是,ResNext不需要人工设计复杂的Inception结构细节,而是每一个分支都采用相同的拓扑结构,ResNeXt的本质是分组卷积。所有Inception模型都具有一个重要的性质——都是遵循 拆分-变换-合并(split-transform-merge) 的设计策略。Inception模型中block的输入会先被拆分成若干低维编码(使用$1 \times 1$卷积实现),然后经过多个不同的滤波器(如$3 \times 3$、$5 \times 5$等)进行转换,最后通过沿通道维度串联的方式合并。这种设计策略希望在保持网络计算复杂度相当低的前提下获取与包含大量且密集的层的网络具有相同的表示能力。但是,Inception模型实现起来很麻烦,它包含一系列复杂的超参——每个变换的滤波器的尺寸和数量都需要指定,不同阶段的模块也需要定制。太多的超参数大多的影响因素,如何将Inception模型调整到适合不同的数据集/任务变得很不明晰。简化Inception基于此,作者的思想是每个结构使用相同的拓扑结构,那么这时候的Inception(这里简称简化Inception)表示为:$$ y = \sum^{C}_{i=1}T_i(x) $$其中 $C$ 是简Inception的基数(Cardinality),$T_i$是任意的变换,例如一系列的卷积操作等。Inception简化前后的对比如下图所示:ResNeXt再结合强大的残差网络,我们得到的便是完整的ResNeXt,也就是在简化Inception中添加一条short-cut,表示为:$$ y = \sum^{C}_{i=1}T_i(x)+x $$如下图所示:与Inception v4的区别ResNeXt的分支的拓扑结构是相同的,Inception V4需要人工设计;ResNeXt是先进行 $1 \times 1$卷积然后执行单位加,Inception V4是先拼接再执行 $1 \times 1$ 卷积。2.1.7 ShuffleNet结构ShuffleNet v1 [论文]()作者通过分析Xception和ResNeXt模型,发现这两种结构通过卷积核拆分虽然计算复杂度均较原始卷积运算有所下降,然而拆分所产生的逐点卷积计算量却相当可观,成为了新的瓶颈。例如对于ResNeXt模型逐点卷积占据了93.4%的运算复杂度。可见,为了进一步提升模型的速度,就必须寻求更为高效的结构来取代逐点卷积。受ResNeXt的启发,作者提出使用分组逐点卷积(group pointwise convolution)来代替原来的结构。通过将卷积运算的输入限制在每个组内,模型的计算量取得了显著的下降。然而这样做也带来了明显的问题:在多层逐点卷积堆叠时,模型的信息流被分割在各个组内,组与组之间没有信息交换(如图 1(a)所示)。这将可能影响到模型的表示能力和识别精度。因此,在使用分组逐点卷积的同时,需要引入组间信息交换的机制。也就是说,对于第二层卷积而言,每个卷积核需要同时接收各组的特征作为输入,如上图(b)所示。作者指出,通过引入“通道重排”(channel shuffle,见上图(c))可以很方便地实现这一机制;并且由于通道重排操作是可导的,因此可以嵌在网络结构中实现端到端的学习。作者还观察到对于较小的网络(如ShuffleNet 0.25x),较大的分组会得到更好结果,认为更宽的通道对于小网络尤其重要。ShuffleNet v2 论文论文中提出了FLOPs不能作为衡量目标检测模型运行速度的标准,因为MAC(Memory access cost)也是影响模型运行速度的一大因素。由此,作者通过实验得出4个设计小模型的准则:卷积操作时,输入输出采用相同通道数可以降低MAC过多的组,会导致MAC增加分支数量过少,模型速度越快element-wise操作导致速度的消耗,远比FLOPs上体现的多。根据以上四点准则,作者在shuffleNet V1的基础上提出了修改得到了shuffleNet V2的基本快。2.1.8 GhostNet结构GhostNet 论文作者通过对比分析ResNet-50网络第一个残差组(Residual group)输出的特征图可视化结果,发现一些特征图高度相似(如Ghost一般,下图中的三组box内的图像对)。如果按照传统的思考方式,可能认为这些相似的特征图存在冗余,是多余信息,想办法避免产生这些高度相似的特征图。但本文思路清奇,推测CNN的强大特征提取能力和这些相似的特征图(Ghost对)正相关,不去刻意的避免产生这种Ghost对,而是尝试利用简单的线性操作来获得更多的Ghost对。常规卷积输入为$Y = w \times h \times c$,输出为$Y_1 = w_1 \times h_1 \times c_1$,运算量约为$w \times h \times c \times w_1 \times h_1 \times c_1$(忽略偏置计算)。Ghost ModuleGhost Module分为常规卷积、Ghost生成和特征图拼接三步1.首先用常规卷积得到本征特征图$Y'$(intrinsic feature maps),2.然后将本征特征图$Y'$每一个通道的特征图$y_i'$,用$\Phi_{i,j}$操作来产生Ghost特征图$y_{i,j}$对线性操作 $\Phi_{i,j}$的理解:论文中表示,可以探索仿射变换和小波变换等其他低成本的线性运算来构建Ghost模块。但是,卷积是当前硬件已经很好支持的高效运算,它可以涵盖许多广泛使用的线性运算,例如平滑、模糊等。 此外,线性运算 $\Phi_{i,j}$的滤波器的大小不一致将降低计算单元(例如CPU和GPU)的效率,所以论文中实验中让Ghost模块中的滤波器size取固定值,并利用Depthwise卷积实现 $\Phi_{i,j}$ ,以构建高效的深度神经网络。所以说,论文中使用的线性操作并不是常见的旋转、平移、仿射变换、小波变换等,而是用的Depthwise卷积。一种猜测可能是传统的线性操作效果没有Depthwise效果好,毕竟CNN可以自动调整filter的权值。3.最后将第一步得到的本征特征图和第二步得到的Ghost特征图拼接(identity连接)得到最终结果OutPut。当然也可以认为(论文中的思路),第2步中的$\Phi_{i,j}$中包含一个恒等映射,即将本征特征图直接输出,则不需用第三部,例如:对于$Y’$中的每一个特征图,对其进行$s$次映射,$s$次中包含一次恒等映射,其余$s-1$次为cheap operate来得到Ghost特征图,最终得到输出结果,这二者理论上完全一致。因此可以看出,Ghost Module和深度分离卷积是很类似的,不同之处在于先进行PointwiseConv,后进行DepthwiseConv,另外增加了DepthwiseConv的数量,包括一个恒定映射。2.1.9 SENet结构该结构与轻量化的关系不大,但是与提升整体的网络性能有关系,因此也放在这里一起进行介绍。SENet 论文SENet网络的创新点在于关注channel之间的关系,希望模型可以自动学习到不同channel特征的重要程度。论文通过显式地建模通道之间的相互依赖关系,自适应地重新校准通道的特征响应,从而设计了SE block如下图所示。论文的核心就是Squeeze和Excitation(论文牛的地方)两个操作。Squeeze(挤压)由于卷积只是在一个局部空间内进行操作,$U$(特征图)很难获得足够的信息来提取channel之间的关系,对于网络中前面的层这更严重,因为感受野比较小。为了,SENet提出Squeeze操作,将一个channel上整个空间特征编码为一个全局特征,采用global average pooling来实现(原则上也可以采用更复杂的聚合策略):$$ z_c = F_{sq}(u_c)=\frac{1}{H \times W}\sum^{H}_{i=1}\sum^{W}_{j=1}u_c(i,j),z \in R^C $$Excitation(激励)Sequeeze操作得到了全局描述特征,然后使用Excitation来抓取channel之间的关系。这个操作需要满足两个准则:首先要灵活,它要可以学习到各个channel之间的非线性关系;第二点是学习的关系不是互斥的,因为这里允许多channel特征,而不是one-hot形式。基于此,这里采用sigmoid形式的gating机制:$$ s =F_{ex}(z,W)=\sigma(g(z,W))=\sigma(W_2ReLU(W_1z)) $$为了降低模型复杂度以及提升泛化能力,这里采用包含两个全连接层的bottleneck结构,其中第一个FC层起到降维的作用,降维系数为r是个超参数,然后采用ReLU激活。最后的FC层恢复原始的维度。最后将学习到的各个channel的激活值(sigmoid激活,值0~1)乘以U上的原始特征(具体见下图SE模块在Inception和ResNet上的应用):$$ \tilde{x}c=Fscale(u_c,s_c)=s_c \cdot u_c $$整个操作可以看成学习到了各个channel的权重系数,从而使得模型对各个channel的特征更有辨别能力,这应该也算一种attention机制。SE模块在Inception和ResNet上的应用2.2 模型量化模型:特指卷积神经网络(用于提取图像/视频视觉特征)量化:将信号的连续取值(或者大量可能的离散取值)近似为有限多个(或较少的)离散值的过程。深度学习模型参数通常是32bit浮点型,我们能否使用16bit,8bit,甚至2bit来存储呢?答案是肯定的。这就是模型量化需要解决的问题。是什么阻碍了量化模型的落地?精度挑战大线性量化对分布的描述不精确8bit to 1bit:比特数越低,精度损失越大分类to检测to识别:任务越难,精度损失越大大模型to小模型:通常认为模型越小,精度损失越大Depthwise对量化不友好Merge BN, Fully Quantized软硬件支持程度不同硬件高效支持的低比特指令不一样不同软件库提供的量化方案/量化细节不一样不同软件库对于不同Layer的量化位置不一样速度能否变快不是所有的硬件上都提供高效的量化计算指令针对体系结构精细的优化不同结构优化方案不同系统&算法协同设计2.2.1 伪量化所谓伪量化是指在存储时使用低精度进行量化,但推理时会还原为正常高精度。为什么推理时不仍然使用低精度呢?这是因为一方面框架层有些算子只支持浮点运算,需要专门实现算子定点化才行。另一方面,高精度推理准确率相对高一些。伪量化可以实现模型压缩,但对模型加速没有多大效果。普通伪量化常见的做法是保存模型每一层时,利用低精度来保存每一个网络参数,同时保存拉伸比例scale(标度)和零值对应的浮点数zero_point(零点位置),量化公式如下:$$ Q(input,scale,zero\_point) = round(\frac{input}{scale}+zero\_point) $$推理阶段,再利用公式:$$ input = zero\_point + Q(input,scale,zero\_point) \times scale $$将网络参数还原为32bit浮点。eg:假设量化为qint8,设量化后的数Q为00011101,scale = 0.025 , zero_point = 0;最高位为0(符号位),所以是正数;后7位转换为10进制是29,所以Q代表的数为 :zero_point + Q scale = 0 + 29 0.025 = 0.725聚类与伪量化利用k-means等聚类算法,步骤如下:将大小相近的参数聚在一起,分为一类。每一类计算参数的平均值,作为它们量化后对应的值。每一类参数存储时,只存储它们的聚类索引。索引和真实值(也就是类内平均值)保存在另外一张表中推理时,利用索引和映射表,恢复为真实值。过程如下图所示,从上可见,当只需要4个类时,我们仅需要2bit就可以实现每个参数的存储了,压缩量达到16倍。推理时通过查找表恢复为浮点值,精度损失可控。2.2.2 定点化与伪量化不同的是,定点化在推理时,不需要还原为浮点数。这需要框架实现算子的定点化运算支持。目前NCNN、MNN、XNN等移动端AI框架中,均加入了定点化支持。2.2.3 其他角度分类二值化$$ Q_B(x)=sgn(x)=\begin{cases} +1,& if \ x \ge 0 \\ -1,& otherwise \end{cases} $$线性量化(对称、非对称、Ristretto)Quantize$$ x_{int} = round(\frac{x}{\Delta})+z \\ x_Q = clamp(0,N_{levels}-1,x_{int}) $$De-quantize$$ x_{float}=(x_Q-z)\Delta $$对数量化可将乘法转变为加法,加法转变为索引2.3 剪枝TODO参考资料理解分组卷积和深度可分离卷积如何降低参数量深度可分离卷积inception-v1,v2,v3,v4----论文笔记深度学习模型轻量化(上) ShuffleNet:一种极高效的移动端卷积神经网络 shuffleNet v1 v2笔记XceptionResNeXt详解ResNet系] 003 ResNeXt最后一届ImageNet冠军模型:SENetGhostNet论文解析:Ghost ModuleMobileNet系列轻量级神经网络“巡礼”(二)—— MobileNet,从V1到V3轻量化网络:MobileNet v3解析mobilenet系列之又一新成员---mobilenet-v3深度学习: 激活函数 (Activation Functions)轻量级网络:IGCV系列V1、V2、V3IGC系列:全分组卷积网络,分组卷积极致使用 | 轻量级网络pytorch量化中torch.quantize_per_tensor()函数参数详解深度学习模型轻量化(下) Szegedy C, Liu W, Jia Y, et al. Going deeper with convolutions[C]//Proceedings of the IEEE conference on computer vision and pattern recognition. 2015: 1-9.Ioffe S, Szegedy C. Batch normalization: Accelerating deep network training by reducing internal covariate shift[C]//International conference on machine learning. PMLR, 2015: 448-456.Szegedy C, Vanhoucke V, Ioffe S, et al. Rethinking the inception architecture for computer vision[C]//Proceedings of the IEEE conference on computer vision and pattern recognition. 2016: 2818-2826.Szegedy C, Ioffe S, Vanhoucke V, et al. Inception-v4, inception-resnet and the impact of residual connections on learning[C]//Thirty-first AAAI conference on artificial intelligence. 2017.模型量化了解一下?【商汤泰坦公开课】模型量化的那些事
2021年12月30日
661 阅读
0 评论
0 点赞
2021-12-07
ViBe(Visual Background Extractor)背景模型介绍与实现
1.原理介绍1.1 模型工作原理背景物体就是指静止的或是非常缓慢的移动的物体,而前景物体就对应移动的物体。所以我们可以把物体检测看出一个分类问题,也就是来确定一个像素点是否属于背景点。在ViBe模型中,背景模型为每个像素点存储了一个样本集,然后将每一个新的像素值和样本集进行比较来判断是否属于背景点。可以知道如果一个新的观察值属于背景点那么它应该和样本集中的采样值比较接近。该模型主要包括三个方面:(1)算法模型初始化;(2)像素的分类过程;(3)模型的更新策略。1.2关于样本集的大小假定我们要处理的每一帧图像是$M \times N$ 个像素的,$x$表示某帧图像的一个像素点。模型要为$M \times N$中每个像素建立一个样本集,$x$像素的样本集可以表示为$$ M(x)=\{p_1 , p_2 , p_3 … p_n\} $$每个样本集的大小为n,n这个值如何确定的,暂时不用管,一般是实验得出的,论文中取$n=20$。所以样本集的总大小为$M \times N \times n$。1.3模型的初始化初始化就是建立背景模型的过程。通用的检测算法的初始化需要一定长度的视频序列来完成,通常要耗费数秒的时间,这极大的影戏的检测的实时性,对于手持相机实时拍照来讲并不合适。ViBe的初始化仅仅通过一帧图像即可完成。ViBe初始化就是填充像素的样本集的过程。由于在一帧图像中不可能包含像素点的时空分布信息,我们利用了相近像素点拥有相近的时空分布特性,具体来讲就是:对于一个像素点$x$,随机的选择它的邻居点$NG(x)$的像素值作为它的模型样本值$M_0(x)$。$$ M_0(x) = {p_0(y | y ∈NG(x))} $$这种初始化方法的优缺点:优点对于噪声的反应比较灵敏,计算量小速度快,不仅减少了背景模型建立的过程,还可以处理背景突然变化的情况,当检测到背景突然变化明显时,只需要舍弃原始的模型,重新利用变化后的首帧图像建立背景模型。缺点用于作平均的几帧初始图像中可能采用了运动物体的像素,这种条件下初始化样本集,容易引入拖影(Ghost)区域;1.4像素的分类过程(前景检测)如下图,假定当前帧为第$ t$ 帧,$p_t(x)$表示第$ t$ 帧图像 $x$ 像素的像素值,图中的$p1$到$p6$都是$x$像素的样本集中的值。那图中的横坐标C1和纵坐标C2是什么呢?我们假定我们处理的图像每个像素是RGB格式的,即一个像素值由R,G,B三个值表示,那么图中的坐标其实还隐藏了C3,即C1,C2,C3表示的正是三个通道值,如果是灰度图的话下面的图就要画成一维一条直线了。接下来我们根据预先设定的半径R统计以当前像素点为中心的圆形区域$S_R(p_t{x})$(实际对应到RGB空间中为球形区域)中包含的该像素点的样本集中的样本的数量,这里记为#。在距$p_t(x)$值半径R距离范围内的样本值有$p_2,p_4$,在半径R范围内的样本值总数计为#,那么下图#=2。让后将该值与预先设定的阈值#min(论文中给出的值是2)进行对比,当#<#min的值时,x这个像素就被标记为前景像素,否则就将其标记为背景像素,依次处理所有像素,就能得出前景图像了1.5模型的更新策略即使已经建立起了背景模型,也应该对背景模型进行不断的更新,这样才能使得背景模型能够适应背景的不断变化(如光照变化,背景物体变更等)。A. 普通更新策略对于其他的背景提取算法,背景模型有两种不同的更新策略:保守更新策略:前景点永远不会用来填充模型这样会引起死锁,产生Ghost区域。比如初始化的时候如果一块静止的区域被错误的检测为运动的,那么在这种策略下它永远会被当做运动的物体来对待;Blind策略:前景和背景都可以用来更新背景模型;对死锁不敏感,但这样的缺点在于,缓慢移动的物体会融入到背景中,无法检测出来;B. ViBe算法更新策略ViBe算法中,使用的更新策略是:保守更新策略 + 前景点计数法 + 随机子采样。保守更新策略:前景点永远不会用来填充模型前景点计数法:对像素点进行统计,如果某个像素点连续N次被检测为前景,则将其更新为背景点;随机子采样:在每一个新的视频帧中都去更新背景模型中的每一个像素点的样本值是没有必要的,当一个像素点被分类为背景点时,它有1/φ的概率去更新背景模型。这就决定了ViBe算法的更新策略的其他属性:无记忆更新策略:每次确定需要更新像素点的背景模型时,以新的像素值随机取代该像素点样本集的一个样本值;时间取样更新策略:并非每处理一帧数据,都需要更新处理,而是按一定的更新率更新背景模型;当一个像素点被判定为背景时,它有1/φ的概率更新背景模型;φ是时间采样因子,一般取值为16;空间邻域更新策略:针对需要更新像素点,在该像素点的邻域中随机选择一个像素点,以新选择的像素点更新被选中的背景模型;C. ViBe算法具体更新的方法:每个背景点都有1/φ的概率更新该像素点的模型样本值;有1/φ的概率去更新该像素点邻居点的模型样本值;前景点计数达到临界值时,将其变为背景,并有1/ φ的概率去更新自己的模型样本值。2.算法实现import numpy as np import cv2 class ViBe: ''' ViBe运动检测,分割背景和前景运动图像 ''' def __init__(self,num_sam=20,min_match=2,radiu=20,rand_sam=16): self.defaultNbSamples = num_sam #每个像素的样本集数量,默认20个 self.defaultReqMatches = min_match #前景像素匹配数量,如果超过此值,则认为是背景像素 self.defaultRadius = radiu #匹配半径,即在该半径内则认为是匹配像素 self.defaultSubsamplingFactor = rand_sam #随机数因子,如果检测为背景,每个像素有1/defaultSubsamplingFactor几率更新样本集和领域样本集 self.background = 0 self.foreground = 255 def __buildNeighborArray(self,img): ''' 构建一副图像中每个像素的邻域数组 参数:输入灰度图像 返回值:每个像素9邻域数组,保存到self.samples中 ''' height,width=img.shape self.samples=np.zeros((self.defaultNbSamples,height,width),dtype=np.uint8) #生成随机偏移数组,用于计算随机选择的邻域坐标 ramoff_xy=np.random.randint(-1,2,size=(2,self.defaultNbSamples,height,width)) #ramoff_x=np.random.randint(-1,2,size=(self.defaultNbSamples,2,height,width)) #xr_=np.zeros((height,width)) xr_=np.tile(np.arange(width),(height,1)) #yr_=np.zeros((height,width)) yr_=np.tile(np.arange(height),(width,1)).T xyr_=np.zeros((2,self.defaultNbSamples,height,width)) for i in range(self.defaultNbSamples): xyr_[1,i]=xr_ xyr_[0,i]=yr_ xyr_=xyr_+ramoff_xy xyr_[xyr_<0]=0 tpr_=xyr_[1,:,:,-1] tpr_[tpr_>=width]=width-1 tpb_=xyr_[0,:,-1,:] tpb_[tpb_>=height]=height-1 xyr_[0,:,-1,:]=tpb_ xyr_[1,:,:,-1]=tpr_ #xyr=np.transpose(xyr_,(2,3,1,0)) xyr=xyr_.astype(int) self.samples=img[xyr[0,:,:,:],xyr[1,:,:,:]] def ProcessFirstFrame(self,img): ''' 处理视频的第一帧 1、初始化每个像素的样本集矩阵 2、初始化前景矩阵的mask 3、初始化前景像素的检测次数矩阵 参数: img: 传入的numpy图像素组,要求灰度图像 返回值: 每个像素的样本集numpy数组 ''' self.__buildNeighborArray(img) self.fgCount=np.zeros(img.shape) #每个像素被检测为前景的次数 self.fgMask=np.zeros(img.shape) #保存前景像素 def Update(self,img): ''' 处理每帧视频,更新运动前景,并更新样本集。该函数是本类的主函数 输入:灰度图像 ''' height,width=img.shape #计算当前像素值与样本库中值之差小于阀值范围RADIUS的个数,采用numpy的广播方法 dist=np.abs((self.samples.astype(float)-img.astype(float)).astype(int)) dist[dist<self.defaultRadius]=1 dist[dist>=self.defaultRadius]=0 matches=np.sum(dist,axis=0) #如果大于匹配数量阀值,则是背景,matches值False,否则为前景,值True matches=matches<self.defaultReqMatches self.fgMask[matches]=self.foreground self.fgMask[~matches]=self.background #前景像素计数+1,背景像素的计数设置为0 self.fgCount[matches]=self.fgCount[matches]+1 self.fgCount[~matches]=0 #如果某个像素连续50次被检测为前景,则认为一块静止区域被误判为运动,将其更新为背景点 fakeFG=self.fgCount>50 matches[fakeFG]=False #此处是该更新函数的关键 #更新背景像素的样本集,分两个步骤 #1、每个背景像素有1/self.defaultSubsamplingFactor几率更新自己的样本集 ##更新样本集方式为随机选取该像素样本集中的一个元素,更新为当前像素的值 #2、每个背景像素有1/self.defaultSubsamplingFactor几率更新邻域的样本集 ##更新邻域样本集方式为随机选取一个邻域点,并在该邻域点的样本集中随机选择一个更新为当前像素值 #更新自己样本集 upfactor=np.random.randint(self.defaultSubsamplingFactor,size=img.shape) #生成每个像素的更新几率 upfactor[matches]=100 #前景像素设置为100,其实可以是任何非零值,表示前景像素不需要更新样本集 upSelfSamplesInd=np.where(upfactor==0) #满足更新自己样本集像素的索引 upSelfSamplesPosition=np.random.randint(self.defaultNbSamples,size=upSelfSamplesInd[0].shape) #生成随机更新自己样本集的的索引 samInd=(upSelfSamplesPosition,upSelfSamplesInd[0],upSelfSamplesInd[1]) self.samples[samInd]=img[upSelfSamplesInd] #更新自己样本集中的一个样本为本次图像中对应像素值 #更新邻域样本集 upfactor=np.random.randint(self.defaultSubsamplingFactor,size=img.shape) #生成每个像素的更新几率 upfactor[matches]=100 #前景像素设置为100,其实可以是任何非零值,表示前景像素不需要更新样本集 upNbSamplesInd=np.where(upfactor==0) #满足更新邻域样本集背景像素的索引 nbnums=upNbSamplesInd[0].shape[0] ramNbOffset=np.random.randint(-1,2,size=(2,nbnums)) #分别是X和Y坐标的偏移 nbXY=np.stack(upNbSamplesInd) nbXY+=ramNbOffset nbXY[nbXY<0]=0 nbXY[0,nbXY[0,:]>=height]=height-1 nbXY[1,nbXY[1,:]>=width]=width-1 nbSPos=np.random.randint(self.defaultNbSamples,size=nbnums) nbSamInd=(nbSPos,nbXY[0],nbXY[1]) self.samples[nbSamInd]=img[upNbSamplesInd] def getFGMask(self): ''' 返回前景mask ''' return self.fgMask调用测试import matplotlib.pyplot as plt # 使用opencv加载目标视频 video_path = "./test.mp4" capture = cv2.VideoCapture(video_path) # 实例化ViBe模型 vibe=ViBe() # 第一帧标志 flag_first_frame = True while True: # 逐一读取视频帧 ret, frame = capture.read() if not ret: break # 将视频帧转为灰度图 gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 如果是第一帧,则用于初始化背景模型 if flag_first_frame: vibe.ProcessFirstFrame(gray) flag_first_frame = False continue # 否则更新背景模型 vibe.Update(gray) # 获取前景并转为uint8类型 segMat=vibe.getFGMask() segMat = segMat.astype(np.uint8) # 拼接出显示图片 img_show = np.hstack((frame,cv2.cvtColor(segMat,cv2.COLOR_GRAY2BGR))) # 缩放到原来的二分之一并显示 x, y = img_show.shape[0:2] img_show = cv2.resize(img_show, (int(y / 2), int(x / 2))) cv2.imshow('vibe',img_show) if cv2.waitKey(50)&0xFF==ord("q"): break # 释放资源 capture.release() cv2.destroyAllWindows()参考资料《O. Barnich and M. Van Droogenbroeck. ViBe: a powerful random technique to estimate the background in video sequences.》ViBe背景建模算法ViBe:基于Python实现的加速版(2019.10)背景提取算法——帧间差分法、背景差分法、ViBe算法、ViBe+算法
2021年12月07日
945 阅读
0 评论
0 点赞
2021-12-06
暗通道先验去雾原理及实现
1.原理介绍1.1 大气散射模型为了表示雾对图像成像的影响,研究者提出了一个物理模型--大气散射模型,用其来表示雾霾等恶劣天气条件对图像造成的影响。该模型由McCartney首先提出,目前已经成为计算机视觉领域中朦胧图像生成过程的经典描述,该模型的数学表达式如下:$$ I(x)=J(x)t(x)+A(1-t(x)) $$其中:$I(x)$、$J(x)$分别表示有雾图像和对应的的无雾清晰图像,$A$表示全球大气光,$t(x)$是透射矩阵,描述的是光通过传输介质后没有被散射的部分,$t(x)$的数学表达式为:(其中,β指的是光的散射系数,d(x)指的是目标与摄象机之间的距离。)$$ t(x)=e^{-\beta d(x)} $$$J(x)t(x)$叫做直接衰减项,描述的是正常的清晰图像在透射媒介后发生衰减后保留下来的部分。可以看出,清晰图像会随着成像设备与物体的距离即$d(x)$的增加发生指数性衰减。$A(1-t(x))$部分称为大气遮罩层,表示的是全局大气光对成像的影响。因此,根据大气散射模型可知,要想恢复出清晰图像,需要解决两个问题:(1)准确的对全局大气光A进行估计求解,(2)准确的对透射矩阵t(x)进行求解。由上面的大气散射模型我们可以很容易得出除雾公式如下:$$ J(x)=\frac{I(x)-A(1-t(x))}{t(x)}=\frac{I(x)+t(x)}{t(x)}+A $$1.2 暗通道先验理论介绍暗通道先验理论为何凯明博士2009年在CVPR(IEEE Conference On Computer Version and Pattern Recogintion/IEEE计算视觉和模式识别会议)上所提出。该理论基于对大量无雾的户外图像的观察和统计,得出了大部分的户外无雾图像的每个局部区域都存在至少一个颜色通道的强度值很低,换言之,这个区域的各个像素点的颜色通道强度的最小值是个很小的数,其值约等于0,即在其暗通道图中表现为黑色。何凯明博士在其发表的暗通道先验除雾的论文中提出,对于任意的输入图像J,其暗通道Jdark可以用数学公式表达如下所示:$$ J^{dark}(x)=\min_{c\in{r,g,b}}[\min_{y\in{\Omega(x)}}(J^c(y))] $$式中$Ω(x)$则是一个正方形区域,该区域的中心是像素$x$,$J^c(x)$代表图像$J$的在某像素点的颜色强度。由公式可知,暗通道实际上图像的最小灰度图经过最小值滤波得到的。分别对无雾图像和有雾图像进行暗通道图的求解效果如图所示:1.3通过暗通道先验对大气光A和透射率t(x)进行估计由大气散射模型可知,若要对图像进行除雾,等价于现在的已经知道的$I(x)$,要求解出$J(x)$,显然这个方程有无数个解。因此就需要用到暗通道先验理论对透射率$t(x)$和大气光$A$进行估计了。首先完成的是对大气光$A$的估计,根据对何凯明博士发表的论文的理解,本文对大气光求解的具体步骤如下:将暗通道图和原图转化为[图片像素数量*通道数]的向量。求出暗通道图对于的向量中亮度前1/1000的向量索引。根据索引在原图转化得到的向量中寻找具有最高亮度的点的值,这就是A的估计值了。在完成大气$A$的求解后,便可以对$t(x)$的求解进行推导了。参考相关资料对$t(x)$的求解过程进行推导如下:首先将大气散射模型稍作变形得到如下式子:$$ \frac{I^c(x)}{A^c}=t(x)\frac{J^c(x)}{A^c}+1-t(x) $$为了对$t(x)$进行求解,先假设在每一个窗口内透射率$t(x)$为常数$\hat t(x)$,因为上面已经得到了$A$的估计值,所以在这里只需将$A$当做一个常量即可,对上式两边进行两次最小值运算,可以得到下式(i):$$ \min_{y \in \Omega(x)}(\min_{c \in \{r,g,b\}}(\frac{I^c(x)}{A^c})) =\hat t(x)\min_{y \in \Omega(x)}(\min_{c \in \{r,g,b\}}(\frac{J^c(x)}{A^c})) +1-\hat t(x) $$根据暗通道先验理论有:$$ J^{dark}(x)=min_{c\in{r,g,b}}[min_{y\in{\Omega(x)}}(J^c(y))] =0 $$因此,可以得出:$$ \min_{y \in \Omega(x)}(\min_{c \in \{r,g,b\}}(\frac{J^c(x)}{A^c})) =0 $$代入式(i)可得式(ii):$$ \hat t(x) = 1 - \min_{y \in \Omega(x)}(\min_{c \in \{r,g,b\}}(\frac{J^c(x)}{A^c})) $$到这就得到了透射图$t(x)$的估计结果。但是如果直接使用这一结果进行除雾效果有时产生的结果并不理想。因为空中总是会存在某些漂浮的小颗粒的,这使得我们看远处的东西会或多或少的受到雾的影响,此外,雾的存在还能让人更好的感受到物体的远近,因此,在去雾时需要考虑对一部分的雾进行保留。对雾的保留可以通过在式(ii)中引入一个在[0,1]之间的因子来实现,变化后得到的新的公式如下所示:$$ \hat t(x) = 1 - \omega\min_{y \in \Omega(x)}(min_{c \in \{r,g,b\}}(\frac{J^c(x)}{A^c})) $$根据何凯明博士的论文及进行了几次测试,最终本文选定的$\omega$的值为0.95。本文对复现实验中对$t(x)$进行求解过程及效果图如下图所示:1.4 根据大气光A和透射率t(x)的估计结果进行图像去雾到这里,我们就可以根据雾天图片退化模型进行清晰图像的恢复了。又因为当像素点x对应的透射图的$t(x)$很小时,会导致根据公式$$ J(x)=\frac{I(x)+t(x)}{t(x)}+A $$求解出的$J(x)$的值偏大,从而导致最终的得到的图片某些地方过曝,所以需要对其进行限制,本文选择限制的最大取值为$t_0=0.1$。从而的到最终使用的图像去雾公式为:$$ J(x)=\frac{I(x)+t(x)}{max(t(x),t_0)}+A $$2.代码实现2.0绘图辅助函数import matplotlib.pyplot as plt #显示单个图 def show_img_by_plt(img,title): plt.figure(figsize=(20,10)) #初始化画布 plt.axis('off') # 关掉坐标轴 plt.imshow(img) #显示图片 plt.title(title,y=-0.1) # 设置图像title plt.show() #show #通过subplot同时显示2个图 def show_double_img_by_subplot(img1,title1,img2,title2): plt.figure(figsize=(20,10)) #初始化画布 #第1个位置创建一个小图 plt.subplot(1,2,1)#表示将整个图像窗口分为1行2列, 当前位置为1 plt.axis('off') # 关掉坐标轴 plt.title(title1,y=-0.1) plt.imshow(img1) #第2个位置创建一个小图 plt.subplot(1,2,2)#表示将整个图像窗口分为1行2列, 当前位置为2 plt.axis('off') # 关掉坐标轴 plt.title(title2,y=-0.1) plt.imshow(img2) plt.show() #show #通过subplot同时显示2个图 #同时显示三个图 def show_three_img_by_subplot(img1,title1,img2,title2,img3,title3): plt.figure(figsize=(16,8)) #初始化画布 #第1个位置创建一个小图 plt.subplot(1,3,1)#表示将整个图像窗口分为1行2列, 当前位置为1 plt.axis('off') # 关掉坐标轴 plt.title(title1,y=-0.1) plt.imshow(img1) #第2个位置创建一个小图 plt.subplot(1,3,2)#表示将整个图像窗口分为1行2列, 当前位置为2 plt.axis('off') # 关掉坐标轴 plt.title(title2,y=-0.1) plt.imshow(img2) #第3个位置创建一个小图 plt.subplot(1,3,3)#表示将整个图像窗口分为1行2列, 当前位置为2 plt.axis('off') # 关掉坐标轴 plt.title(title3,y=-0.1) plt.imshow(img3) plt.show() #show2.1 获取图片暗通道图# 核心函数 #暗通道实际上是在rgb三个通道中取最小值组成灰度图,然后再进行一个最小值滤波得到的。 def get_dark_channel_img(original_img, r=15): #原图rgb三个通道中取最小值组成灰度图 temp_img = np.min(original_img,2) #再进行一个最小值滤波 #最小值滤波用腐蚀来替代了,其实腐蚀就是最小值滤波,最大值滤波是膨胀 kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (r,r)) """ cv2.getStructuringElement( ) 返回指定形状和尺寸的结构元素。 这个函数的第一个参数表示内核的形状,有三种形状可以选择。 矩形:MORPH_RECT; 交叉形:MORPH_CROSS; 椭圆形:MORPH_ELLIPSE; (r,r)表示kernel的size """ dst_img = cv2.erode(temp_img, kernel) """ dst = cv.dilate(temp_img, kerne) 对图片进行腐蚀 参数说明: dst_img 输出与输入相同大小和类型的图像. kernel 用于侵蚀的结构元素可以使用getStructuringElement来创建结构元素。 """ return dst_img #返回最终暗通道图#调用测试 import imageio import numpy as np import cv2 img_src="./image/get_dark_channel_img_clear.jpg" original_img = imageio.imread(img_src) dark_channel_img=get_dark_channel_img(original_img) show_double_img_by_subplot(original_img,"original_img",dark_channel_img,"dark_channel_img")2.2根据暗通道图和原图估计大气光#核心函数 """ 1.从暗通道图按照亮度的大小取前0.1%的像素。 2.在这些位置中,在原始有雾图像I中寻找对应的具有最高亮度的点的值,作为A值。 """ def estimate_A(original_img,dark_channel_img): #计算图片的像素总数 img_h,img_w,img_c_num = original_img.shape img_size = img_h*img_w #计算暗通道图的像素值较大的0.1%的像素的数量 num_px = int(max(math.floor(img_size/1000),1)) #将暗通道图变为列向量 dark_vec = dark_channel_img.reshape(img_size,1); #将图片变为列向量 img_vec = original_img.reshape(img_size,3); #将暗通道图对应的列向量进行排序并返回从小到大的数组索引--argsort函数返回的是数组值从小到大的索引值 indices = np.argsort(dark_vec, axis=0) #取暗通道图像素较大的0.1%的像素的索引 indices = indices[img_size-num_px::] #将原图对应的 暗通道图的像素值排前0.1% 的像素点的像素值进行求平均 A = np.max(img_vec[indices]) return A#调用测试 import imageio import numpy as np import cv2 import math # img_src=str(input("请输入原图的地址:")) img_src="./image/haze_image.jpg" original_img = imageio.imread(img_src)/255 dark_channel_img=get_dark_channel_img(original_img) A = estimate_A(original_img,dark_channel_img)2.3根据原图和大气光A进行投射图t(x)的估计#核心函数 #根据论文中透射图的估计公式进行书写的 def estimate_transmission(original_img,A, omega = 0.95): min_min_Ic_div_Ac = np.zeros(original_img.shape,"float64"); for index in range(0,3): min_min_Ic_div_Ac[:,:,index] = original_img[:,:,index]/A transmission = 1 - omega*get_dark_channel_img(min_min_Ic_div_Ac); return transmission#调用测试 import imageio import numpy as np import cv2 # img_src=str(input("请输入原图的地址:")) img_src="./image/haze_image.jpg" original_img = imageio.imread(img_src) dark_channel_img=get_dark_channel_img(original_img) A = estimate_A(original_img,dark_channel_img) transmission = estimate_transmission(original_img,A) show_double_img_by_subplot(original_img,"original_img",transmission,"transmission")2.4根据估计好的大气光和透射图进行图片恢复#核心函数 def recover_haze_by_dark_channel_prior(haze_img,t_max = 0.1): dark_channel_img=get_dark_channel_img(haze_img) A = estimate_A(haze_img,dark_channel_img) transmission = estimate_transmission(haze_img,A) clear_img = np.empty(haze_img.shape,haze_img.dtype); for index in range(0,3): clear_img[:,:,index] = (haze_img[:,:,index]-A[index])/cv2.max(transmission,t_max) + A[index] return clear_img#调用测试 import imageio import numpy as np # haze_img_src=str(input("请输入有雾图片的地址:")) haze_img_src="./image/haze_image1.jpg" haze_img = imageio.imread(haze_img_src) clear_img = recover_haze_by_dark_channel_prior(haze_img) show_double_img_by_subplot(haze_img,"haze_img",clear_img,"clear_img") transmission = estimate_transmission(haze_img,A) show_three_img_by_subplot(haze_img,"haze_img",clear_img,"clear_img",transmission,"transmission")3.暗通道先验图片去雾汇总import imageio import numpy as np import cv2 import math def get_dark_channel_img(original_img, r=6): #原图rgb三个通道中取最小值组成灰度图 temp_img = np.min(original_img,2) #再进行一个最小值滤波 #最小值滤波用腐蚀来替代了,其实腐蚀就是最小值滤波,最大值滤波是膨胀 kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (r,r)) dst_img = cv2.erode(temp_img, kernel) return dst_img #返回最终暗通道图 def estimate_A(original_img,dark_channel_img): #计算图片的像素总数 img_h,img_w,img_c_num = original_img.shape img_size = img_h*img_w #计算暗通道图的像素值较大的0.1%的像素的数量 num_px = int(max(math.floor(img_size/1000),1)) #将暗通道图变为列向量 dark_vec = dark_channel_img.reshape(img_size,1); #将图片变为列向量 img_vec = original_img.reshape(img_size,3); #将暗通道图对应的列向量进行排序并返回从小到大的数组索引--argsort函数返回的是数组值从小到大的索引值 indices = np.argsort(dark_vec, axis=0) #取暗通道图像素较大的0.1%的像素的索引 indices = indices[img_size-num_px::] #将原图对应的 暗通道图的像素值排前0.1% 的像素点的像素值进行求平均 atm_sum = np.zeros([1,3]) for index in range(1,num_px): atm_sum = atm_sum + img_vec[indices[index]] A = atm_sum / num_px; return A[0] def estimate_transmission(original_img,A, omega = 0.95): min_min_Ic_div_Ac = np.zeros(original_img.shape,"float64"); for index in range(0,3): min_min_Ic_div_Ac[:,:,index] = original_img[:,:,index]/A[index] transmission = 1 - omega*get_dark_channel_img(min_min_Ic_div_Ac); return transmission def recover_haze_by_dark_channel_prior(haze_img,t_max = 0.1): dark_channel_img=get_dark_channel_img(haze_img) A = estimate_A(haze_img,dark_channel_img) transmission = estimate_transmission(haze_img,A) clear_img = np.empty(haze_img.shape,haze_img.dtype); for index in range(0,3): clear_img[:,:,index] = (haze_img[:,:,index]-A[index])/cv2.max(transmission,t_max) + A[index] return clear_img haze_img_src="./image/haze_image.jpg" haze_img = imageio.imread(haze_img_src) clear_img = recover_haze_by_dark_channel_prior(haze_img) show_double_img_by_subplot(haze_img,"haze_img",clear_img,"clear_img")应用与视频from moviepy.editor import VideoFileClip def process_image(image): return deHaze(image/255.0)*255 output_file = './vedio/clear_dark_channel_short.mp4' test_clip = VideoFileClip("./vedio/haze_short.mp4") new_clip = test_clip.fl_image(process_image) new_clip.write_videofile(output_file, audio=False)参考资料暗通道先验原理——DCP去雾算法He K , Sun J , Fellow, et al. Single Image Haze Removal Using Dark Channel Prior[J]. IEEE Transactions on Pattern Analysis & Machine Intelligence, 2011, 33(12):2341-2353.
2021年12月06日
1,558 阅读
1 评论
0 点赞
2021-11-28
VOC数据集的Anchor聚类--Kmeans算法实现
1.K-means算法具体介绍参考:Kmeans算法简介k-means聚类的算法运行过程:(1)选择k个初始聚类中心 (2)计算每个对象与这k个中心各自的距离,按照最小距离原则分配到最邻近聚类 (3)使用每个聚类中的样本均值作为新的聚类中心 (4)重复步骤(2)和(3)直到聚类中心不再变化 (5)结束,得到k个聚类2.算法实现2.1数据加载函数封装# STEP1:加载数据集,获取所有box的width、height import os from progressbar import * import xmltodict import numpy as np def load_dataset(data_root): xml_dir= os.path.join(data_root,"Annotations") #xml文件路径(Annotations) width_height_list = [] #用于存储统计结果 #进度条功能 widgets = ['box width_height 统计: ',Percentage(), ' ', Bar('#'),' ', Timer(),' ', ETA()] pbar = ProgressBar(widgets=widgets, maxval=len(os.listdir(xml_dir))).start() count = 0 for xml_file in os.listdir(xml_dir): # 拼接xml文件的path xml_file_path = os.path.join(xml_dir,xml_file) # 读取xml文件到字符串 with open(xml_file_path) as f: xml_str = f.read() # xml字符串转为字典 xml_dic = xmltodict.parse(xml_str) # 获取图片的width、height img_width = float(xml_dic["annotation"]["size"]["width"]) img_height = float(xml_dic["annotation"]["size"]["height"]) # 获取xml文件中的所有objects obj_list = [] objects = xml_dic["annotation"]["object"] if isinstance(objects,list): # xml文件中包含多个object for obj in objects: obj_list.append(obj) else: # xml文件中包含1个object obj_list.append(objects) #print(obj_list) # width_height布统计 for obj in obj_list: #box 的height\width归一化 box_width = (float(obj['bndbox']["xmax"]) - float(obj['bndbox']["xmin"]))/img_width box_height = (float(obj['bndbox']["ymax"]) - float(obj['bndbox']["ymin"]))/img_height width_height_list.append([box_width,box_height]) #更新进度条 count += 1 pbar.update(count) #释放进度条 pbar.finish()调用效果#输出统计结果信息 data_root = "/data/jupiter/project/dataset/209_VOC_new" width_height_list = load_dataset(data_root) width_height_np = np.array(width_height_list) print("clustering feature data is ready. shape = (N object, width and height) = {}".format(width_height_np.shape))box width_height 统计: 100% |###############| Elapsed Time: 0:00:35 Time: 0:00:35 clustering feature data is ready. shape = (N object, width and height) = (10670, 2)2.2 将未聚类前的统计结果绘图表示# 将未聚类前的统计结果绘图表示 import matplotlib.pyplot as plt plt.figure(figsize=(10,10)) plt.scatter(width_height_np[:,0],width_height_np[:,1],alpha=0.3) plt.title("Clusters",fontsize=20) plt.xlabel("normalized width",fontsize=20) plt.ylabel("normalized height",fontsize=20) plt.show()调用效果2.3 实现距离评估函数(这里用的是iou)这里iou的计算公式为:$$ \begin{array}{rl} IoU &= \frac{\textrm{intersection} }{ \textrm{union} - \textrm{intersection} }\\ \textrm{intersection} &= Min(w_1,w_2) Min(h_1,h_2)\\ \textrm{union} & = w_1 h_1 + w_2 h_2 \end{array} $$图示代码实现import numpy as np # 数据间距离评估函数 def iou(box, clusters): """ 计算一个ground truth边界盒和k个先验框(Anchor)的交并比(IOU)值。 参数box: 元组或者数据,代表ground truth的长宽。 参数clusters: 形如(k,2)的numpy数组,其中k是聚类Anchor框的个数 返回:ground truth和每个Anchor框的交并比。 """ x = np.minimum(clusters[:, 0], box[0]) y = np.minimum(clusters[:, 1], box[1]) if np.count_nonzero(x == 0) > 0 or np.count_nonzero(y == 0) > 0: raise ValueError("Box has no area") intersection = x * y box_area = box[0] * box[1] cluster_area = clusters[:, 0] * clusters[:, 1] iou_ = intersection / (box_area + cluster_area - intersection) return iou_2.4实现kmeans聚类函数# 实现kmeans聚类函数 def kmeans(boxes, k): """ 利用IOU值进行K-means聚类 参数boxes: 形状为(r, 2)的ground truth框,其中r是ground truth的个数 参数k: Anchor的个数 返回值:形状为(k, 2)的k个Anchor框 """ # 即是上面提到的r rows = boxes.shape[0] # 距离数组,计算每个ground truth和k个Anchor的距离 distances = np.empty((rows, k)) # 上一次每个ground truth"距离"最近的Anchor索引 last_clusters = np.zeros((rows,)) # 设置随机数种子 np.random.seed() # 初始化聚类中心,k个簇,从r个ground truth随机选k个 clusters = boxes[np.random.choice(rows, k, replace=False)] # 开始聚类 while True: # 计算每个ground truth和k个Anchor的距离,用1-IOU(box,anchor)来计算 for row in range(rows): distances[row] = 1 - iou(boxes[row], clusters) # 对每个ground truth,选取距离最小的那个Anchor,并存下索引 nearest_clusters = np.argmin(distances, axis=1) # 如果当前每个ground truth"距离"最近的Anchor索引和上一次一样,聚类结束 if (last_clusters == nearest_clusters).all(): break # 更新簇中心为簇里面所有的ground truth框的均值 for cluster in range(k): clusters[cluster] = np.median(boxes[nearest_clusters == cluster], axis=0) # 更新每个ground truth"距离"最近的Anchor索引 last_clusters = nearest_clusters return clusters,nearest_clusters2.4 调用测试CLUSTERS = 9 #聚类数量,anchor数量 INPUTDIM = 416 #输入网络大小 clusters_center_list,nearest_clusters = kmeans(width_height_np, k=CLUSTERS) clusters_center_list_handle = np.array(clusters_center_list)*INPUTDIM # 得到最终填入YOLOv3 的cfg文件中的anchor print('Boxes:') print(clusters_center_list_handle.astype(np.int32)) Boxes: [[ 9 37] [235 239] [ 4 30] [ 24 33] [ 5 21] [ 52 63] [ 5 26] [ 7 28] [ 6 33]]2.5聚类结果绘制与效果评估(mean_iou)查看数据聚类结果import seaborn as sns current_palette = list(sns.xkcd_rgb.values()) def plot_cluster_result(plt,clusters,nearest_clusters,mean_iou,width_height_np,k): for icluster in np.unique(nearest_clusters): pick = nearest_clusters==icluster c = current_palette[icluster] plt.rc('font', size=8) plt.plot(width_height_np[pick,0],width_height_np[pick,1],"p", color=c, alpha=0.5,label="cluster = {}, N = {:6.0f}".format(icluster,np.sum(pick))) plt.text(clusters[icluster,0], clusters[icluster,1], "c{}".format(icluster), fontsize=20,color="red") plt.title("Clusters=%d" %k) plt.xlabel("width") plt.ylabel("height") plt.legend(title="Mean IoU = {:5.4f}".format(mean_iou)) # achor结果评估 def avg_iou(boxes, clusters): """ 计算一个ground truth和k个Anchor的交并比的均值。 """ return np.mean([np.max(iou(boxes[i], clusters)) for i in range(boxes.shape[0])]) figsize = (7,5) plt.figure(figsize=figsize) mean_iou = avg_iou(width_height_np, out) plot_cluster_result(plt,clusters_center_list,nearest_clusters,mean_iou,width_height_np,k=CLUSTERS)运行效果查看聚类中心分布w = width_height_np[:, 0].tolist() h = width_height_np[:, 1].tolist() centroid_w=clusters_center_list[:,0].tolist() centroid_h=clusters_center_list[:,1].tolist() plt.figure(dpi=200) plt.title("kmeans") plt.scatter(w, h, s=10, color='b') plt.scatter(centroid_w,centroid_h,s=10,color='r') plt.show()运行效果3.汇总简略版#coding=utf-8 import xml.etree.ElementTree as ET import numpy as np import glob def iou(box, clusters): """ 计算一个ground truth边界盒和k个先验框(Anchor)的交并比(IOU)值。 参数box: 元组或者数据,代表ground truth的长宽。 参数clusters: 形如(k,2)的numpy数组,其中k是聚类Anchor框的个数 返回:ground truth和每个Anchor框的交并比。 """ x = np.minimum(clusters[:, 0], box[0]) y = np.minimum(clusters[:, 1], box[1]) if np.count_nonzero(x == 0) > 0 or np.count_nonzero(y == 0) > 0: raise ValueError("Box has no area") intersection = x * y box_area = box[0] * box[1] cluster_area = clusters[:, 0] * clusters[:, 1] iou_ = intersection / (box_area + cluster_area - intersection) return iou_ def avg_iou(boxes, clusters): """ 计算一个ground truth和k个Anchor的交并比的均值。 """ return np.mean([np.max(iou(boxes[i], clusters)) for i in range(boxes.shape[0])]) def kmeans(boxes, k): """ 利用IOU值进行K-means聚类 参数boxes: 形状为(r, 2)的ground truth框,其中r是ground truth的个数 参数k: Anchor的个数 参数dist: 距离函数 返回值:形状为(k, 2)的k个Anchor框 """ # 即是上面提到的r rows = boxes.shape[0] # 距离数组,计算每个ground truth和k个Anchor的距离 distances = np.empty((rows, k)) # 上一次每个ground truth"距离"最近的Anchor索引 last_clusters = np.zeros((rows,)) # 设置随机数种子 np.random.seed() # 初始化聚类中心,k个簇,从r个ground truth随机选k个 clusters = boxes[np.random.choice(rows, k, replace=False)] # 开始聚类 while True: # 计算每个ground truth和k个Anchor的距离,用1-IOU(box,anchor)来计算 for row in range(rows): distances[row] = 1 - iou(boxes[row], clusters) # 对每个ground truth,选取距离最小的那个Anchor,并存下索引 nearest_clusters = np.argmin(distances, axis=1) # 如果当前每个ground truth"距离"最近的Anchor索引和上一次一样,聚类结束 if (last_clusters == nearest_clusters).all(): break # 更新簇中心为簇里面所有的ground truth框的均值 for cluster in range(k): clusters[cluster] = np.median(boxes[nearest_clusters == cluster], axis=0) # 更新每个ground truth"距离"最近的Anchor索引 last_clusters = nearest_clusters return clusters # 加载自己的数据集,只需要所有labelimg标注出来的xml文件即可 def load_dataset(path): dataset = [] for xml_file in glob.glob("{}/*xml".format(path)): tree = ET.parse(xml_file) # 图片高度 height = int(tree.findtext("./size/height")) # 图片宽度 width = int(tree.findtext("./size/width")) for obj in tree.iter("object"): # 偏移量 xmin = int(obj.findtext("bndbox/xmin")) / width ymin = int(obj.findtext("bndbox/ymin")) / height xmax = int(obj.findtext("bndbox/xmax")) / width ymax = int(obj.findtext("bndbox/ymax")) / height xmin = np.float64(xmin) ymin = np.float64(ymin) xmax = np.float64(xmax) ymax = np.float64(ymax) if xmax == xmin or ymax == ymin: print(xml_file) # 将Anchor的长宽放入dateset,运行kmeans获得Anchor dataset.append([xmax - xmin, ymax - ymin]) return np.array(dataset) ANNOTATIONS_PATH = "/data/jupiter/project/dataset/209_VOC_new/Annotations" #xml文件所在文件夹 CLUSTERS = 9 #聚类数量,anchor数量 INPUTDIM = 416 #输入网络大小 data = load_dataset(ANNOTATIONS_PATH) out = kmeans(data, k=CLUSTERS) print('Boxes:') out_handle = np.array(out)*INPUTDIM print(out_handle.astype(np.int32)) print("Accuracy: {:.2f}%".format(avg_iou(data, out) * 100)) final_anchors = np.around(out[:, 0] / out[:, 1], decimals=2).tolist() print("Before Sort Ratios:\n {}".format(final_anchors)) print("After Sort Ratios:\n {}".format(sorted(final_anchors)))Boxes: [[ 8 34] [234 256] [279 239] [ 52 63] [ 6 28] [ 5 26] [ 24 33] [ 10 37] [177 216]] Accuracy: 82.93% Before Sort Ratios: [0.24, 0.92, 1.17, 0.83, 0.23, 0.19, 0.74, 0.28, 0.82] After Sort Ratios: [0.19, 0.23, 0.24, 0.28, 0.74, 0.82, 0.83, 0.92, 1.17]参考资料YOLOv3使用自己数据集——Kmeans聚类计算anchor boxes目标检测算法之YOLO系列算法的Anchor聚类
2021年11月28日
1,055 阅读
0 评论
0 点赞
2021-11-27
ShuffleNet-v2简介与实现
1.原论文ECCV2018--ShuffleNet V2: Practical Guidelines for Efficient CNN Architecture Design2.设计理念目前衡量模型复杂度的一个通用指标是FLOPs,具体指的是multiply-add数量,但是这却是一个间接指标,因为它不完全等同于速度。如图1中的(c)和(d)可以看到相同FLOPs的两个模型,其速度却存在差异。这种不一致主要归结为两个原因,首先影响速度的不仅仅是FLOPs,如内存使用量(memory access cost, MAC),这不能忽略,对于GPUs来说可能会是瓶颈。另外模型的并行程度也影响速度,并行度高的模型速度相对更快。另外一个原因,模型在不同平台上的运行速度是有差异的,如GPU和ARM,而且采用不同的库也会有影响。3.根据设计理念及实验得出的4条基本设计准则3.1探索实验据此,作者在特定的平台下研究ShuffleNetv1和MobileNetv2的运行时间,并结合理论与实验得到了4条实用的指导原则:G1:同等通道大小最小化内存访问量对于轻量级CNN网络,常采用深度可分割卷积(depthwise separable convolutions),其中点卷积( pointwise convolution)即1x1卷积复杂度最大。这里假定输入和输出特征的通道数分别为 $c1$和$c2$,特征图的空间大小为$h \times w$,那么1x1卷积的FLOPs为$$ B=hwc_1c_2 $$对应的MAC(memory access cost)为$$ MAC = hw(c_1+c_2)+c_1c_2 $$根据均值不等式(这里假定内存足够),固定B时,MAC存在下限(令$c_2=\frac{B}{hwc_1}$),则$$ MAC \ge 2 \sqrt{hwB} + \frac{B}{hw} $$仅当$c_1=c_2$时,MAC取最小值,这个理论分析也通过实验得到证实,如下表所示,通道比为1:1时速度更快。G2:过量使用组卷积会增加MAC(memory access cost)组卷积(group convolution)是常用的设计组件,因为它可以减少复杂度却不损失模型容量。但是这里发现,分组过多会增加MAC(memory access cost)。对于组卷积,FLOPs为(其中g为组数):$$ B = hwc_1c_2/g $$对应的MAC(memory access cost)为$$ MAC = hwc_1+Bg/c_1+B/hw $$可以看到,在输入确定,B相同时,当g增加时,MAC(memory access cost)会同时增加,下表是对应的实验,所以明智之举是不要使用太大 ![[公式]](https://www.zhihu.com/equation?tex=g) 的组卷积。G3:网络碎片化会降低并行度一些网络如Inception,以及Auto ML自动产生的网络NASNET-A,它们倾向于采用“多路”结构,即存在一个lock中很多不同的小卷积或者pooling,这很容易造成网络碎片化,减低模型的并行度,相应速度会慢,这也可以通过实验得到证明。G4:不能忽略元素级操作(比如ReLU和Add)对于元素级(element-wise operators)比如ReLU和Add,虽然它们的FLOPs较小,但是却需要较大的MAC(memory access cost)。这里实验发现如果将ResNet中残差单元中的ReLU和shortcut移除的话,速度有20%的提升。4.ShuffleNet-v1 基本结构及存在的问题4.1ShuffleNet-v1基本结构其中:(a) the basic ShufflleNet unit;(b) the ShufflleNet unit for spatial down sampling (2×);4.2ShuffleNet-v1存在的问题在ShuffleNetv1的模块中,大量使用了1x1组卷积,这违背了G2原则,另外v1采用了类似ResNet中的瓶颈层(bottleneck layer),输入和输出通道数不同,这违背了G1原则。同时使用过多的组,也违背了G3原则。短路连接中存在大量的元素级Add运算,这违背了G4原则。5.ShuffleNet-v2基本块及改进分析5.1ShuffleNet-v2基本块根据前面的4条准则,作者分析了ShuffleNetv1设计的不足,并在此基础上改进得到了ShuffleNetv2,两者模块上的对比如下图所示:其中(c) the basic ShufflleNetv2 unit;(d) the ShufflleNetv2 unit for spatial down sampling (2×);DWConv: depthwise convolutionGConv:group convolution对于ShufflleNetv2基本块(the ShufflleNetv2 unit)还可以选择是否采用SE(Squeeze-and-Excitation)模块和残差(residual)结构5.2ShuffleNetv2的改进分析为了改善v1的缺陷,根据四条原则,作者提出了shufflenet v2。v2版本引入了一种新的运算:channel split(如上图网路结构中的图c)。channel split的做法:在开始时先将输入特征图在通道维度分成两个分支:通道数分别为 $c'$ 和 $c-c'$ ,实际实现时$c'=c/2$ 。一个是identity,一个经过三个conv,然后concat到一起,这个满足G4。右边的分支包含3个连续的卷积,并且输入和输出通道相同,这符合G1。而且两个1x1卷积不再是组卷积,这符合G2另外两个分支相当于已经分成两组。两个分支的输出不再是Add元素,而是concat在一起。取替了element-wise操作add。最后经过channel shuffle将两个分支的信息进行交流。channel split的作用:第一,划分一半到右分支,意味着右边计算量减少,从而可以提高channel数,提高网络capacity。第二,左分支相当于一种特征重用(feature reuse), 跟DenseNet和CondenseNet一样的思想。下图(a)为DenseNet的从source layer到target layer连接的权重的大小,可见target层前1-3层的信息对当前层帮助较大,而越远的连接比较多余。图(b)为ShuffleNet v2的情况,因为shuffle操作会导致每次会有一半的channel到下一层。因此,作者认为shufflenet跟densenet一样的利用到了feature reuse,所以有效。6.ShuffleNet-v2完整网络结构其中:1个stage=1个DSampling+Repeat个BasicUnit7.ShuffleNet-v2基本块实现(pytorch)7.1Channel Shuffle图示实现步骤假定将输入层分为 g 组,总通道数为 g × n 。首先你将通道那个维度拆分为 (g,n) 两个维度然后将这两个维度转置变成 (n,g)最后重新reshape成一个维度 g × n 。代码# Channel Shuffle def shuffle_chnls(x, groups=2): """Channel Shuffle""" bs, chnls, h, w = x.data.size() # 如果通道数不是分组的整数被,则无法进行Channel Shuffle操作,直接返回x if chnls % groups: return x # 计算用于Channel Shuffle的一个group的的通道数 chnls_per_group = chnls // groups # 执行channel shuffle操作 x = x.view(bs, groups, chnls_per_group, h, w) # 将通道那个维度拆分为 (g,n) 两个维度 x = torch.transpose(x, 1, 2).contiguous() # 将这两个维度转置变成 (n,g) x = x.view(bs, -1, h, w) # 最后重新reshape成一个维度 g × n g\times ng×n return x7.2ShufflleNetv2基本块( the basic ShufflleNetv2 unit)基本结构图示增加SE(Squeeze-and-Excitation)模块和残差(residual)结构后的基本块结构图示代码# 封装一个Conv+BN+RELU的基本块 class BN_Conv2d(nn.Module): def __init__(self, in_channels, out_channels, kernel_size, stride, padding, dilation=1, groups=1, bias=False, activation=True): # dilation=1-->卷积核膨胀 super(BN_Conv2d, self).__init__() layers = [nn.Conv2d(in_channels, out_channels, kernel_size=kernel_size, stride=stride, padding=padding, dilation=dilation, groups=groups, bias=bias), nn.BatchNorm2d(out_channels)] if activation: layers.append(nn.ReLU(inplace=True)) self.seq = nn.Sequential(*layers) def forward(self, x): return self.seq(x)# ShuffleNet-v2基本块 class BasicUnit(nn.Module): def __init__(self, in_chnls, out_chnls, is_se=False, is_residual=False, c_ratio=0.5, groups=2): super(BasicUnit, self).__init__() self.is_se, self.is_res = is_se, is_residual # 是否使用SE结构和残差结构 self.l_chnls = int(in_chnls * c_ratio) # 左侧输入通道数 self.r_chnls = in_chnls - self.l_chnls # 右侧输入通道数 self.ro_chnls = out_chnls - self.l_chnls # 右侧输出通道数 self.groups = groups # layers self.conv1 = BN_Conv2d(self.r_chnls, self.ro_chnls, 1, 1, 0) self.dwconv2 = BN_Conv2d(self.ro_chnls, self.ro_chnls, 3, 1, 1, # same padding, depthwise conv groups=self.ro_chnls, activation=None) act = False if self.is_res else True self.conv3 = BN_Conv2d(self.ro_chnls, self.ro_chnls, 1, 1, 0, activation=act) # 是否使用SE模块和residual结构 if self.is_se: self.se = SE(self.ro_chnls, 16) if self.is_res: self.shortcut = nn.Sequential() if self.r_chnls != self.ro_chnls: self.shortcut = BN_Conv2d(self.r_chnls, self.ro_chnls, 1, 1, 0, activation=False) def forward(self, x): # channel split 操作 x_l = x[:, :self.l_chnls, :, :] x_r = x[:, self.r_chnls:, :, :] # right path out_r = self.conv1(x_r) out_r = self.dwconv2(out_r) out_r = self.conv3(out_r) # 是否使用SE模块和residual结构 if self.is_se: coefficient = self.se(out_r) out_r *= coefficient if self.is_res: out_r += self.shortcut(x_r) # concatenate out = torch.cat((x_l, out_r), 1) return shuffle_chnls(out, self.groups)7.3ShufflleNetv2下采样基本块(the ShufflleNetv2 unit for spatial down sampling)图示代码# SuffleNet-v2下采样基本块 class DSampling(nn.Module): def __init__(self, in_chnls, groups=2): super(DSampling, self).__init__() self.groups = groups # down-sampling(通过stride=2实现), depth-wise conv(通过groups=in_chnls实现). self.dwconv_l1 = BN_Conv2d(in_chnls, in_chnls, 3, 2, 1, groups=in_chnls, activation=None) self.conv_l2 = BN_Conv2d(in_chnls, in_chnls, 1, 1, 0) self.conv_r1 = BN_Conv2d(in_chnls, in_chnls, 1, 1, 0) self.dwconv_r2 = BN_Conv2d(in_chnls, in_chnls, 3, 2, 1, groups=in_chnls, activation=False) self.conv_r3 = BN_Conv2d(in_chnls, in_chnls, 1, 1, 0) def forward(self, x): # left path out_l = self.dwconv_l1(x) out_l = self.conv_l2(out_l) # right path out_r = self.conv_r1(x) out_r = self.dwconv_r2(out_r) out_r = self.conv_r3(out_r) # concatenate out = torch.cat((out_l, out_r), 1) return shuffle_chnls(out, self.groups)8.ShuffleNet-v2网络结构实现# TODO参考资料ShuffleNet V2: Practical Guidelines for Efficient CNN Architecture DesignShuffleNetV2:轻量级CNN网络中的桂冠轻量级网络之ShuffleNet v2shufflenet中channel shuffle原理PyTorch实现ShuffleNet-v2亲身实践
2021年11月27日
1,208 阅读
0 评论
0 点赞
2021-11-27
VOC数据集中的图片宽度和高度取整
1.针对问题在数据标注时由于设置问题导致生成的最终的XML文件中的图片的width和height变成了小数,导致在运行某些代码时候会发生异常。样例文件如下所示<?xml version="1.0" ?> <annotation> <folder>/pool/label/209-20180708-08310907/JPEGImages</folder> <filename>/pool/label/209-20180708-08310907/JPEGImages/209-20180708-08310907_frame00731.jpg</filename> <source> <database>Unknown</database> </source> <size> <width>1920.0</width> # 问题所在 <height>1080.0</height> # 问题所在 <depth>3</depth> </size> <segmented>0</segmented> <object> <name>RefuelVehicle</name> <pose>Unspecified</pose> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>664</xmin> <ymin>368</ymin> <xmax>794</xmax> <ymax>464</ymax> </bndbox> </object> ······ </annotation>2.代码实现# 导包 import xmltodict import os from progressbar import * import numpy as np import time import matplotlib.pyplot as plt import xmltodict from xml.dom import minidom data_root = "/data/jupiter/project/dataset/VOC_zd" xml_dir= os.path.join(data_root,"Annotations") #xml文件路径(Annotations) width_height_list = [] #用于存储统计结果 #进度条功能 widgets = ['box width_height 校准: ',Percentage(), ' ', Bar('#'),' ', Timer(),' ', ETA()] pbar = ProgressBar(widgets=widgets, maxval=len(os.listdir(xml_dir))).start() count = 0 for xml_file in os.listdir(xml_dir): # 拼接xml文件的path xml_file_path = os.path.join(xml_dir,xml_file) # 读取xml文件到字符串 with open(xml_file_path) as f: xml_str = f.read() # xml字符串转为字典 xml_dic = xmltodict.parse(xml_str) # 获取图片的width、height img_width = xml_dic["annotation"]["size"]["width"] img_height = xml_dic["annotation"]["size"]["height"] if img_width.endswith(".0"): xml_dic["annotation"]["size"]["width"] = img_width[:-2] xml_dic["annotation"]["size"]["height"] = img_height[:-2] xmlstr = xmltodict.unparse(xml_dic) xml = minidom.parseString(xmlstr) xml_pretty_str = xml.toprettyxml() with open(xml_file_path,"w") as f: f.write(xml_pretty_str) # 更新进度条 count += 1 pbar.update(count) #释放进度条 pbar.finish()
2021年11月27日
693 阅读
0 评论
0 点赞
2021-11-27
快速使用Mobilenet SSD进行训练VOC格式的数据集
1.训练步骤STEP1:下载代码并配置环境下载代码git clone https://github.com/lufficc/SSD.git cd SSD修改requirements.txttorch==1.5 torchvision==0.5 yacs tqdm opencv-python vizer根据requirements.txt完成环境配置# Required packages: torch torchvision yacs tqdm opencv-python vizer conda create -n ssd-lufficc python=3.8 pip install -r requirements.txt额外补充安装pip install tensorboardX pip install sixSTEP2:在当前目录下建立数据集文件夹的软连接或者复制数据集到当前文件夹完成后文件夹结构├──VOC_data/VOC2007/ ├── Annotations #可以采用软连接的方式避免对大量数据进行复制 ├──放置xml文件 #TODO ├── JPEGImages ├──放置img文件 #TODO ├──ImageSets/Main #可以采用软连接的方式避免对大量数据进行复制 ├── split.py #数据分割脚本,用于生成训练索引文件split.py文件内容import os import random random.seed(0) xmlfilepath=r'./Annotations' saveBasePath=r"./ImageSets/Main/" trainval_percent=0.9 # 可以自己设置 train_percent=0.9 # 可以自己设置 temp_xml = os.listdir(xmlfilepath) total_xml = [] for xml in temp_xml: if xml.endswith(".xml"): total_xml.append(xml) num=len(total_xml) list=range(num) tv=int(num*trainval_percent) tr=int(tv*train_percent) trainval= random.sample(list,tv) train=random.sample(trainval,tr) print("train and val size",tv) print("traub suze",tr) ftrainval = open(os.path.join(saveBasePath,'trainval.txt'), 'w') ftest = open(os.path.join(saveBasePath,'test.txt'), 'w') ftrain = open(os.path.join(saveBasePath,'train.txt'), 'w') fval = open(os.path.join(saveBasePath,'val.txt'), 'w') for i in list: name=total_xml[i][:-4]+'\n' if i in trainval: ftrainval.write(name) if i in train: ftrain.write(name) else: fval.write(name) else: ftest.write(name) ftrainval.close() ftrain.close() fval.close() ftest .close()python split.py配置文件根目录的环境变量export VOC_ROOT="./VOC_data" #/path/to/voc_rootSTEP3:修改配置文件 vim configs/mobilenet_v2_ssd320_voc0712.yamlMODEL: NUM_CLASSES: 11 # 修改NUM_CLASSES BOX_HEAD: PREDICTOR: 'SSDLiteBoxPredictor' BACKBONE: NAME: 'mobilenet_v2' OUT_CHANNELS: (96, 1280, 512, 256, 256, 64) PRIORS: FEATURE_MAPS: [20, 10, 5, 3, 2, 1] STRIDES: [16, 32, 64, 107, 160, 320] MIN_SIZES: [60, 105, 150, 195, 240, 285] MAX_SIZES: [105, 150, 195, 240, 285, 330] ASPECT_RATIOS: [[2, 3], [2, 3], [2, 3], [2, 3], [2, 3], [m2, 3]] BOXES_PER_LOCATION: [6, 6, 6, 6, 6, 6] INPUT: IMAGE_SIZE: 320 DATASETS: TRAIN: ("voc_2007_trainval", "voc_2012_trainval") # TODO TEST: ("voc_2007_test", ) SOLVER: MAX_ITER: 120000 LR_STEPS: [80000, 100000] GAMMA: 0.1 BATCH_SIZE: 32 LR: 1e-3 OUTPUT_DIR: 'outputs/mobilenet_v2_ssd320_voc0712STEP4:修改类别信息vim ssd/data/datasets/voc.pyclass VOCDataset(torch.utils.data.Dataset): class_names = ('person','bridgevehicle','luggagevehicle','plane','refuelvehicle','foodvehicle','rubbishvehicle','watervehicle','platformvehicle','tractorvehicle','bridgeconnector') # 改成自己的class注意,类名必须小写 ······STEP5: 模型训练修改默认所用的device和batch_size vim ssd/config/defaults.py# 修改以下内容 _C.MODEL.DEVICE = "cuda:1" # cpu/cuda/cuda:1 _C.SOLVER.BATCH_SIZE = 128 _C.TEST.BATCH_SIZE = 32单GPU# for example, train SSD300: python train.py --config-file configs/mobilenet_v2_ssd320_voc0712.yaml多GPU# for example, train SSD300 with 2 GPUs: export NGPUS=2 python -m torch.distributed.launch --nproc_per_node=$NGPUS train.py --config-file configs/mobilenet_v2_ssd320_voc0712.yaml SOLVER.WARMUP_FACTOR 0.03333 SOLVER.WARMUP_ITERS 10002.模型评估单GPU# for example, evaluate SSD300: python test.py --config-file configs/mobilenet_v2_ssd320_voc0712.yaml多GPU# for example, evaluate SSD300 with 2 GPUs: export NGPUS=2 python -m torch.distributed.launch --nproc_per_node=$NGPUS test.py --config-file configs/mobilenet_v2_ssd320_voc0712.yaml3.模型调用(预测)#TODO参考资料https://github.com/qfgaohao/pytorch-ssd
2021年11月27日
848 阅读
0 评论
0 点赞
2021-11-25
Pytorch 实战:使用Resnet18实现对是否戴口罩进行图片分类
1.实验环境torch = 1.6.0torchvision = 0.7.0matplotlib = 3.3.3 # 绘图用progressbar = 2.5 # 绘制进度条用easydict # 超参数字典功能增强2.数据集数据集介绍包含2582张图片,3个类别(yes/unknow/no)下载地址:口罩检测数据集3.导入相关的包# 导包 import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader import torchvision from torchvision import datasets,transforms import matplotlib.pyplot as plt import random from progressbar import *4.设置超参数# 定义超参数 from easydict import EasyDict super_param={ 'train_data_root' : './data/train', 'val_data_root' : './data/train', 'device': torch.device('cuda:0' if torch.cuda.is_available() else cpu), 'lr': 0.001, 'epochs': 3, 'batch_size': 1, 'begain_epoch':0, 'model_load_flag':False, #是否加载以前的模型 'model_load_path':'./model/resnet18/epoch_1_0.8861347792408986.pkl', 'model_save_dir':'./model/resnet18', } super_param = EasyDict(super_param) if not os.path.exists(super_param.model_save_dir): os.mkdir(super_param.model_save_dir)5.模型搭建# 模型搭建,调用预训练模型Resnet18 class Modified_Resnet18(nn.Module): """docstring for ClassName""" def __init__(self, num_classs=3): super(Modified_Resnet18, self).__init__() model = torchvision.models.resnet18(pretrained=True) model.fc = nn.Linear(model.fc.in_features,num_classs) self.model = model def forward(self, x): x = self.model(x) return x model = Modified_Resnet18() print(model)Modified_Resnet18( (model): ResNet( (conv1): Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False) (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) (relu): ReLU(inplace=True) (maxpool): MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False) (layer1): Sequential( (0): BasicBlock( (conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False) (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) (relu): ReLU(inplace=True) (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False) (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) ) (1): BasicBlock( (conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False) (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) (relu): ReLU(inplace=True) (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False) (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) ) ) (layer2): Sequential( (0): BasicBlock( (conv1): Conv2d(64, 128, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False) (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) (relu): ReLU(inplace=True) (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False) (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) (downsample): Sequential( (0): Conv2d(64, 128, kernel_size=(1, 1), stride=(2, 2), bias=False) (1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) ) ) (1): BasicBlock( (conv1): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False) (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) (relu): ReLU(inplace=True) (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False) (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) ) ) (layer3): Sequential( (0): BasicBlock( (conv1): Conv2d(128, 256, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False) (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) (relu): ReLU(inplace=True) (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False) (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) (downsample): Sequential( (0): Conv2d(128, 256, kernel_size=(1, 1), stride=(2, 2), bias=False) (1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) ) ) (1): BasicBlock( (conv1): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False) (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) (relu): ReLU(inplace=True) (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False) (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) ) ) (layer4): Sequential( (0): BasicBlock( (conv1): Conv2d(256, 512, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False) (bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) (relu): ReLU(inplace=True) (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False) (bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) (downsample): Sequential( (0): Conv2d(256, 512, kernel_size=(1, 1), stride=(2, 2), bias=False) (1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) ) ) (1): BasicBlock( (conv1): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False) (bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) (relu): ReLU(inplace=True) (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False) (bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) ) ) (avgpool): AdaptiveAvgPool2d(output_size=(1, 1)) (fc): Linear(in_features=512, out_features=3, bias=True) ) )6.加载数据集# 训练数据封装成dataloader # 定义数据处理的transform transform = transforms.Compose([ transforms.Resize((224,224)), transforms.ToTensor(), # 0-255 to 0-1 # transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), ]) train_dataset =torchvision.datasets.ImageFolder(root=super_param.train_data_root,transform=transform) train_loader =DataLoader(train_dataset,batch_size=super_param.batch_size, shuffle=True) val_dataset =torchvision.datasets.ImageFolder(root=super_param.val_data_root,transform=transform) val_loader =DataLoader(val_dataset,batch_size=super_param.batch_size, shuffle=True) # 保存类别与类别索引的对应关系 class_to_idx = train_dataset.class_to_idx idx_to_class = dict([val,key] for key,val in class_to_idx.items()) print(len(train_dataset),len(val_dataset))查看一个数据样例#查看一个数据 import matplotlib.pyplot as plt index = random.randint(0,len(val_dataset)) img,idx = val_dataset[index] img = img.permute(1,2,0) label = idx_to_class[idx] print("label=",label) plt.figure(dpi=100) plt.xticks([]) plt.yticks([]) plt.imshow(img) plt.show()7.定义损失函数和优化器# 定义损失函数和优化器 criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(),lr=super_param.lr)8.定义单个epoch的训练函数# 定义单个epoch的训练函数 def train_epoch(model,train_loader,super_param,criterion,optimizer,epoch): model.train()#训练声明 for batch_index,(imgs,labels) in enumerate(train_loader): #data上device imgs,labels = imgs.to(super_param.device),labels.to(super_param.device) #梯度清零 optimizer.zero_grad() #前向传播 output = model(imgs) #损失计算 loss = criterion(output,labels) #梯度计算,反向传播 loss.backward() #参数优化 optimizer.step() #打印参考信息 if(batch_index % 10 == 0): print("\rEpoch:{} Batch Index(batch_size={}):{}/{} Loss:{}".format(epoch,super_param.batch_size,batch_index,len(train_loader),loss.item()),end="")9.定义验证函数# 定义验证函数 def val(model,val_loader,super_param,criterion): model.eval()#测试声明 correct = 0.0 #正确数量 val_loss = 0.0 #测试损失 #定义进度条 widgets = ['Val: ',Percentage(), ' ', Bar('#'),' ', Timer(),' ', ETA()] pbar = ProgressBar(widgets=widgets, maxval=100).start() with torch.no_grad(): # 不会计算梯度,也不会进行反向传播 for batch_index,(imgs,labels) in enumerate(val_loader): imgs,labels = imgs.to(super_param.device),labels.to(super_param.device) output = model(imgs)#模型预测 val_loss += criterion(output,labels).item() # 计算测试损失 #argmax返回 值,索引 dim=1表示要索引 pred = output.argmax(dim=1) # 找到概率最大的下标 correct += pred.eq(labels.view_as(pred)).sum().item()# 统计预测正确数量 # print("pred===========",pred) pbar.update(batch_index/len(val_loader)*100)#更新进度条进度 #释放进度条 pbar.finish() val_loss /= len(val_loader.dataset) val_accuracy = correct / len(val_loader.dataset) time.sleep(0.01) print("Val --- Avg Loss:{},Accuracy:{}".format(val_loss,val_accuracy)) return val_loss,val_accuracy10.模型训练model = model.to(super_param.device) if super_param.model_load_flag: #加载训练过的模型 model.load_state_dict(torch.load(super_param.model_load_path)) # 数据统计-用于绘图和模型保存 epoch_list = [] loss_list = [] accuracy_list =[] best_accuracy = 0.0 for epoch in range(super_param.begain_epoch,super_param.begain_epoch+super_param.epochs): train_epoch(model,train_loader,super_param,criterion,optimizer,epoch) val_loss,val_accuracy = val(model,val_loader,super_param,criterion) #数据统计 epoch_list.append(epoch) loss_list.append(val_loss) accuracy_list.append(accuracy_list) #保存准确率更高的模型 if(val_accuracy>best_accuracy): best_accuracy = val_accuracy torch.save(model.state_dict(),os.path.join(super_param.model_save_dir, 'epoch_' + str(epoch)+ '_' + str(best_accuracy) + '.pkl')) print('epoch_' + str(epoch) + '_' + str(best_accuracy) + '.pkl'+"保存成功")11.查看数据统计结果# 查看数据统计结果 fig = plt.figure(figsize=(12,12),dpi=70) #子图1 ax1 = plt.subplot(2,1,1) title = "bach_size={},lr={}".format(super_param.batch_size,super_param.lr) plt.title(title,fontsize=15) plt.xlabel('Epochs',fontsize=15) plt.ylabel('Loss',fontsize=15) plt.xticks(fontsize=13) plt.yticks(fontsize=13) plt.plot(epoch_list,loss_list) #子图2 ax2 = plt.subplot(2,1,2) plt.xlabel('Epochs',fontsize=15) plt.ylabel('Accuracy',fontsize=15) plt.xticks(fontsize=13) plt.yticks(fontsize=13) plt.plot(epoch_list,accuracy_list,'r') plt.show()
2021年11月25日
785 阅读
0 评论
0 点赞
2021-11-25
python实现音频读取与可视化+端点检测+音频切分
1.音频读取与可视化1.1 核心代码import wave import matplotlib.pyplot as plt import numpy as np import os filepath = "./audio/day0716_17.wav" f = wave.open(filepath,'rb') # 读取音频 params = f.getparams() # 查看音频的参数信息 print(params) # 可视化准备工作 strData = f.readframes(nframes)#读取音频,字符串格式 waveData = np.fromstring(strData,dtype=np.int16)#将字符串转化为int waveData = waveData*1.0/(max(abs(waveData)))#wave幅值归一化 # 可视化 time = np.arange(0,nframes)*(1.0 / framerate) plt.figure(figsize=(20,4)) plt.plot(time,waveData) plt.xlabel("Time(s)") plt.ylabel("Amplitude") plt.title("Single channel wavedata") plt.grid('on')#标尺,on:有,off:无。1.2 实现效果_wave_params(nchannels=1, sampwidth=2, framerate=16000, nframes=8744750, comptype='NONE', compname='not compressed')2.端点检测2.1 环境准备pip install speechbrain2.2 核心代码from speechbrain.pretrained import VAD VAD = VAD.from_hparams(source="speechbrain/vad-crdnn-libriparty", savedir="pretrained_models/vad-crdnn-libriparty") boundaries = VAD.get_speech_segments("./day0716_17.wav") print(boundaries)2.3 输出结果输出结果为包含语音数据的[开始时间,结束时间]区间序列tensor([[ 1.1100, 4.5700], [ 5.5600, 7.6100], [ 8.5800, 12.7800], ······ [508.7500, 519.0300], [526.0800, 537.1100], [538.0200, 546.5200]])3.pydub分割并保存音频3.1 核心代码from pydub import AudioSegment file_name = "denoise_0306.wav" sound = AudioSegment.from_mp3(file_name) # 单位:ms crop_audio = sound[1550:1900] save_name = "crop_"+file_name print(save_name) crop_audio.export(save_name, format="wav",tags={'artist': 'AppLeU0', 'album': save_name})4.汇总(仅供参考)汇总方式自行编写。以下案例为处理audio文件夹的的所有的wav结尾的文件从中提取出有声音的片段并进保存到相应的文件夹from pydub import AudioSegment import os from speechbrain.pretrained import VAD VAD = VAD.from_hparams(source="speechbrain/vad-crdnn-libriparty", savedir="pretrained_models/vad-crdnn-libriparty") audio_dir = "./audio/" audio_name_list = os.listdir(audio_dir) for audio_name in audio_name_list: if not audio_name.endswith(".wav"): continue print(audio_name,"开始处理") audio_path = os.path.join(audio_dir,audio_name) word_save_dir = os.path.join(audio_dir,audio_name[:-4]) if not os.path.exists(word_save_dir): os.mkdir(word_save_dir) else: print(audio_name,"已经完成,跳过") continue boundaries = VAD.get_speech_segments(audio_path) sound = AudioSegment.from_mp3(audio_path) for boundary in boundaries: start_time = boundary[0] * 1000 end_time = boundary[1] * 1000 word = sound[start_time:end_time] word_save_path = os.path.join(word_save_dir,str(int(boundary[0]))+"-"+ str(int(boundary[1])) +".wav") word.export(word_save_path, format="wav") print("\r"+word_save_path,"保存成功",end="") print(audio_name,"处理完成")参考资料https://huggingface.co/speechbrain/vad-crdnn-libripartypydub分割并保存音频
2021年11月25日
2,025 阅读
0 评论
0 点赞
2021-11-24
快速使用Yolov4-tiny训练VOC格式的数据集
1.训练步骤STEP1:下载代码并配置环境git clone https://github.com/bubbliiiing/yolov4-tiny-pytorch.git cd yolov4-tiny-pytorch pip install -r requirements.txtSTEP2:根据文件结构填充VOC格式的数据集数据放置格式(只需完成#TODO部分即可)├──VOCdevkit/VOC2007/ ├── Annotations #可以采用软连接的方式避免对大量数据进行复制 ├──放置xml文件 #TODO ├── JPEGImages ├──放置img文件 #TODO ├──ImageSets/Main #可以采用软连接的方式避免对大量数据进行复制 ├──放置训练索引文件 (无需手动完成,自动生成) ├── voc2yolo.py #数据分割脚本,用于生成训练索引文件编辑voc2yolo.py。设置tarin\val\test数据分割比例#----------------------------------------------------------------------# # 验证集的划分在train.py代码里面进行 # test.txt和val.txt里面没有内容是正常的。训练不会使用到。 #----------------------------------------------------------------------# ''' #--------------------------------注意----------------------------------# 如果在pycharm中运行时提示: FileNotFoundError: [WinError 3] 系统找不到指定的路径。: './VOCdevkit/VOC2007/Annotations' 这是pycharm运行目录的问题,最简单的方法是将该文件复制到根目录后运行。 可以查询一下相对目录和根目录的概念。在VSCODE中没有这个问题。 #--------------------------------注意----------------------------------# ''' import os import random random.seed(0) xmlfilepath=r'./Annotations' saveBasePath=r"./ImageSets/Main/" #----------------------------------------------------------------------# # 想要增加测试集修改trainval_percent # train_percent不需要修改 #----------------------------------------------------------------------# trainval_percent=0.9 train_percent=1 temp_xml = os.listdir(xmlfilepath) total_xml = [] for xml in temp_xml: if xml.endswith(".xml"): total_xml.append(xml) num=len(total_xml) list=range(num) tv=int(num*trainval_percent) tr=int(tv*train_percent) trainval= random.sample(list,tv) train=random.sample(trainval,tr) print("train and val size",tv) print("traub suze",tr) ftrainval = open(os.path.join(saveBasePath,'trainval.txt'), 'w') ftest = open(os.path.join(saveBasePath,'test.txt'), 'w') ftrain = open(os.path.join(saveBasePath,'train.txt'), 'w') fval = open(os.path.join(saveBasePath,'val.txt'), 'w') for i in list: name=total_xml[i][:-4]+'\n' if i in trainval: ftrainval.write(name) if i in train: ftrain.write(name) else: fval.write(name) else: ftest.write(name) ftrainval.close() ftrain.close() fval.close() ftest .close()生成训练索引文件python voc2yolo.pySTEP3:对VOC数据集将进行类别统计├──VOCdevkit/VOC2007/ ├── get_all_class.py #VOC类别统计脚本在其中写入以下内容import xmltodict import os # VOC xml文件所在文件夹 annotation_dir="./Annotations/" label_list = list() # 逐一处理xml文件 for file in os.listdir(annotation_dir): annotation_path = os.path.join(annotation_dir,file) # 读取xml文件 with open(annotation_path,'r') as f: xml_str = f.read() #转为字典 xml_dic = xmltodict.parse(xml_str) # 获取label并去重加入到label_list objects = xml_dic["annotation"]["object"] if isinstance(objects,list): # xml文件中包含多个object for obj in objects: label = obj['name'] if label not in label_list: label_list.append(label) else:# xml文件中只包含1个object obj = objects label = obj['name'] if label not in label_list: label_list.append(label) print(label_list)然后运行get_all_class.py获取所有的类别信息['Person', 'BridgeVehicle', 'LuggageVehicle', 'Plane', 'RefuelVehicle', 'FoodVehicle', 'RubbishVehicle', 'WaterVehicle', 'PlatformVehicle', 'TractorVehicle', 'BridgeConnector']STEP4:编辑model_data/voc_classes.txt 和下载权重文件将其中的类别数改为自己的,文件内容为Person BridgeVehicle LuggageVehicle Plane RefuelVehicle FoodVehicle RubbishVehicle WaterVehicle PlatformVehicle TractorVehicle BridgeConnector下载权重文件到model_data文件夹下wget https://github.com/bubbliiiing/yolov4-tiny-pytorch/releases/download/v1.0/yolov4_tiny_weights_coco.pthSTEP4:生成最终训练所需的txt文件然后运行voc_annotation.pypython voc_annotation.py此时会生成对应的2007_train.txt,每一行对应其图片位置及其真实框的位置。STEP5:修改train.py的NUM_CLASSSES将train.py的NUM_CLASSSES修改成所需要分的类的个数(不需要+1)STEP6:开始训练python train.py2.模型预测在yolo.py文件里面,在如下部分修改model_path和classes_path使其对应训练好的文件;model_path对应logs文件夹下面的权值文件,classes_path是model_path对应分的类。_defaults = { #--------------------------------------------------------------------------# # 使用自己训练好的模型进行预测一定要修改model_path和classes_path! # model_path指向logs文件夹下的权值文件,classes_path指向model_data下的txt # 如果出现shape不匹配,同时要注意训练时的model_path和classes_path参数的修改 #--------------------------------------------------------------------------# "model_path" : 'model_data/yolov4_tiny_weights_coco.pth', "classes_path" : 'model_data/coco_classes.txt', #---------------------------------------------------------------------# # anchors_path代表先验框对应的txt文件,一般不修改。 # anchors_mask用于帮助代码找到对应的先验框,一般不修改。 #---------------------------------------------------------------------# "anchors_path" : 'model_data/yolo_anchors.txt', "anchors_mask" : [[3,4,5], [1,2,3]], #-------------------------------# # 所使用的注意力机制的类型 # phi = 0为不使用注意力机制 # phi = 1为SE # phi = 2为CBAM # phi = 3为ECA #-------------------------------# "phi" : 0, #---------------------------------------------------------------------# # 输入图片的大小,必须为32的倍数。 #---------------------------------------------------------------------# "input_shape" : [416, 416], #---------------------------------------------------------------------# # 只有得分大于置信度的预测框会被保留下来 #---------------------------------------------------------------------# "confidence" : 0.5, #---------------------------------------------------------------------# # 非极大抑制所用到的nms_iou大小 #---------------------------------------------------------------------# "nms_iou" : 0.3, #---------------------------------------------------------------------# # 该变量用于控制是否使用letterbox_image对输入图像进行不失真的resize, # 在多次测试后,发现关闭letterbox_image直接resize的效果更好 #---------------------------------------------------------------------# "letterbox_image" : False, #-------------------------------# # 是否使用Cuda # 没有GPU可以设置成False #-------------------------------# "cuda" : True, }运行predict.py,输入待测试的图片路径img/street.jpg在predict.py里面进行设置可以进行fps测试和video视频检测。3.评估自己的数据集前往get_map.py文件修改classes_path,classes_path用于指向检测类别所对应的txt,这个txt和训练时的txt一样。评估自己的数据集必须要修改。在yolo.py里面修改model_path以及classes_path。model_path指向训练好的权值文件,在logs文件夹里。classes_path指向检测类别所对应的txt。运行get_map.py即可获得评估结果,评估结果会保存在map_out文件夹中。参考资料https://github.com/bubbliiiing/yolov4-tiny-pytorch
2021年11月24日
886 阅读
0 评论
0 点赞
2021-11-19
python结合opencv调用谷歌tesseract-ocr实现印刷体的表格识别识别
环境搭建安装依赖-leptonica库下载源码git clone https://github.com/DanBloomberg/leptonica.gitconfiguresudo apt install automake bash autogen.sh ./configure编译安装make sudo make install这样就安装好了leptonica库谷歌tesseract-ocr编译安装下载源码git clone https://github.com/tesseract-ocr/tesseract.git tesseract-ocr安装依赖sudo apt-get install g++ autoconf automake libtool autoconf-archive pkg-config libpng12-dev libjpeg8-dev libtiff5-dev zlib1g-dev libleptonica-dev -y安装训练所需要的库(只是调用可以不用安装)sudo apt-get install libicu-dev libpango1.0-dev libcairo2-devconfigurebash autogen.sh ./configure编译安装make sudo make install # 可选项,不训练可以选择不执行下面两条 make training sudo make training-install sudo ldconfig安装对应的字体库并添加对应的环境变量下载好的语言包 放在/usr/local/share/tessdata目录里面。语言包地址:https://github.com/tesseract-ocr/tessdata_best。里面有各种语言包,都是训练好的语言包。简体中文下载:chi_sim.traineddata , chi_sim_vert.traineddata英文包:eng.traineddata。设置环境变量vim ~/.bashrc # 在.bashrc的文件末尾加入以下内容 export TESSDATA_PREFIX=/usr/local/share/tessdata source ~/.bashrc查看字体库tesseract --list-langs使用tesseract-ocr测试# 识别/home/app/1.png这张图片,内容输出到output.txt 里面,用chi_sim 中文来识别(不用加.traineddata,会默认加) tesseract /home/app/1.png output -l chi_sim # 查看识别结果 cat output.txt安装python调用依赖包pip install pytesseract配合opencv调用进行印刷体表格识别代码import cv2 import numpy as np import pytesseract import matplotlib.pyplot as plt import re #原图 raw = cv2.imread("table3.2.PNG") # 灰度图片 gray = cv2.cvtColor(raw, cv2.COLOR_BGR2GRAY) # 二值化 binary = cv2.adaptiveThreshold(~gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 35, -5) rows, cols = binary.shape # 识别横线 scale = 50 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (cols // scale, 1)) eroded = cv2.erode(binary, kernel, iterations=1) dilated_col = cv2.dilate(eroded, kernel, iterations=1) # 识别竖线 scale = 10 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (1, rows // scale)) eroded = cv2.erode(binary, kernel, iterations=1) dilated_row = cv2.dilate(eroded, kernel, iterations=1) # 标识交点 bitwise_and = cv2.bitwise_and(dilated_col, dilated_row) plt.figure(dpi=300) # plt.imshow(bitwise_and,cmap="gray") # 标识表格 merge = cv2.add(dilated_col, dilated_row) plt.figure(dpi=300) # plt.imshow(merge,cmap="gray") # 两张图片进行减法运算,去掉表格框线 merge2 = cv2.subtract(binary, merge) plt.figure(dpi=300) # plt.imshow(merge2,cmap="gray") # 识别黑白图中的白色交叉点,将横纵坐标取出 ys, xs = np.where(bitwise_and > 0) # 提取单元格切分点 # 横坐标 x_point_arr = [] # 通过排序,获取跳变的x和y的值,说明是交点,否则交点会有好多像素值值相近,我只取相近值的最后一点 # 这个10的跳变不是固定的,根据不同的图片会有微调,基本上为单元格表格的高度(y坐标跳变)和长度(x坐标跳变) sort_x_point = np.sort(xs) for i in range(len(sort_x_point) - 1): if sort_x_point[i + 1] - sort_x_point[i] > 10: x_point_arr.append(sort_x_point[i]) i = i + 1 x_point_arr.append(sort_x_point[i]) # 要将最后一个点加入 # 纵坐标 y_point_arr = [] sort_y_point = np.sort(ys) for i in range(len(sort_y_point) - 1): if (sort_y_point[i + 1] - sort_y_point[i] > 10): y_point_arr.append(sort_y_point[i]) i = i + 1 # 要将最后一个点加入 y_point_arr.append(sort_y_point[i]) # 循环y坐标,x坐标分割表格 plt.figure(dpi=300) data = [[] for i in range(len(y_point_arr))] for i in range(len(y_point_arr) - 1): for j in range(len(x_point_arr) - 1): # 在分割时,第一个参数为y坐标,第二个参数为x坐标 cell = raw[y_point_arr[i]:y_point_arr[i + 1], x_point_arr[j]:x_point_arr[j + 1]] #读取文字,此为默认英文 text1 = pytesseract.image_to_string(cell, lang="chi_sim+eng") #去除特殊字符 text1 = re.findall(r'[^\*"/:?\\|<>″′‖ 〈\n]', text1, re.S) text1 = "".join(text1) print('单元格图片信息:' + text1) data[i].append(text1) plt.subplot(len(y_point_arr),len(x_point_arr),len(x_point_arr)*i+j+1) plt.imshow(cell) plt.axis('off') plt.tight_layout() plt.show()实现效果表格图片识别结果单元格图片信息:停机位Stands 单元格图片信息:空器避展限制Wingspanlimitsforaircraft(m) 单元格图片信息:Nr61.62.414-416.886.887.898,899.ZZ14 单元格图片信息:80 单元格图片信息:Nr101,102.105,212.219-222,228.417-419,874-876.895-897.910-912 单元格图片信息:65 单元格图片信息:NrZZ11 单元格图片信息:61 单元格图片信息:Nr103,104.106.107.109,117,890-894.ZL1,ZL2(deicingstands) 单元格图片信息:52 单元格图片信息:Nr878.879 单元格图片信息:51 单元格图片信息:Nr229.230 单元格图片信息:48 单元格图片信息:Nr877 单元格图片信息:42 单元格图片信息:Nr61R.62L 单元格图片信息:38.1 单元格图片信息:Nr61L.62R.108.110-116,118.201-211.213-218,223-227.409-413,414L.414R.415L.415R.416L,416R.417R.418R.419R.501-504.601-610,886L,886R.887L.887R.888.889.898L.898R.8991L.899R_901-909,910R.911R.912R.913-916.921-925.ZZ1IL.ZZ12.ZZ13 单元格图片信息:36 单元格图片信息:NrZZ11IR 单元格图片信息:32 单元格图片信息:Nr884.885 单元格图片信息:29(fuselagelength32.8) 单元格图片信息:Nr417L.418L.419L 单元格图片信息:24.9 单元格图片信息:Nr910L.911L.912L.931-946 单元格图片信息:24
2021年11月19日
1,060 阅读
0 评论
0 点赞
1
2
3
...
5