本专栏主要是深度学习/自动驾驶相关的源码实现,获取全套代码请参考
 
这里写目录标题
- 准备
- 逐步源码实现
- 数据集读取
- VIt模型搭建
- hand
- 类别和位置编码
- 类别编码
- 位置编码
 
 
- blocks
- head
- VIT整体
 
- Runner(参考mmlab)
- 可视化
 
- 总结
准备

 本博客完成Vision Transfomer(VIT)模型的搭建和flowers数据集的训练测试.整个源码包括如下几个任务:
 1.读取flowers数据集的dataset类,对应文件dataset.py
 2.VIT模型搭建,主要依赖于上几篇博客,对应model.py
1.transfomer中Multi-Head Attention的源码实现的MultiheadAttention类,用于搭建BaseTransformerLayer类,实现encoder和decoder功能
2.transfomer中Decoder和Encoder的base_layer的源码实现的BaseTransformerLayer类,帮助我们丝滑地搭建各类transformer网络
3.transfomer中正余弦位置编码的源码实现[可选]
3.设置优化器学习率和训练/验证模型,对应runner.py和train.py
 4.可视化测试单个图片的预测结果,对应demo.py
逐步源码实现
源码结构如下
 
数据集读取
主要原理:根据dataset的路径,存储各个图片对应的路径,label隐藏在路径中.
 在getitem函数中完成指定index图片和label的读取和数据增强功能
class Flowers(Dataset):
    # 用于读取flower数据集
    def __init__(self, dataset_path: str, transforms=None):
        '''
        存储所有数据 data路径和label
        :param dataset_path:
        '''
        super(Dataset, self).__init__()
        flowers = os.listdir(dataset_path)
        flowers = sorted(flowers) # 必须排序,否在每一次顺序不一样训练测试类别就会乱
        self.flower_paths = []
        self.class2label = {}  # 类别str 转 label
        label = 0
        for _, flower in enumerate(flowers):
            flowers_path = os.path.join(dataset_path, flower)
            if os.path.isdir(flowers_path):
                self.class2label[flower] = label
                label +=1
                sub_flowers = os.listdir(flowers_path)
                for sub_flower in sub_flowers:
                    self.flower_paths.append(os.path.join(flowers_path, sub_flower))
        self.label2class = label2class(self.class2label)  # label 转 类别str
        self.transforms = transforms
    ''''''
    def __getitem__(self, item):
        # 读取数据和label
        img = Image.open(self.flower_paths[item])
        label = self.class2label[self.flower_paths[item].split('/')[-2]]
        if self.transforms is not None:
            img = self.transforms(img)  # 数据增强
        return img, label
VIt模型搭建
将整个深度学习模型按照人体分为hand+backbone+neck+head 4个部分,Vit模型不同CNN模型,它的backbone+neck为多个MultiHeadAttention堆叠组成,称之为blocks.
hand
hand主用完成预处理,将数据用"手"揉捏成想要的类型.本处主要完成图片的patch操作,将图片分割成一个个小块,使用大核的卷积完成.然后把w和h拉平后shape就和NLP(b,n,d)一样了.
class PatchLayer(nn.Module):
    def __init__(self, img_size, patch_size=20, embeding_dim=64):
        super(PatchLayer, self).__init__()
        self.grid_size = (img_size[0] // patch_size, img_size[1] // patch_size)
        self.num_patches = self.grid_size[0] * self.grid_size[1]
        self.proj = nn.Conv2d(in_channels=3,
                              out_channels=embeding_dim,
                              kernel_size=(patch_size, patch_size),
                              stride=patch_size,
                              padding=0)
        self.norm = nn.LayerNorm(normalized_shape=embeding_dim)
    def forward(self, img):
        img = self.proj(img)  # 图片分割
        img = img.flatten(start_dim=2)  # wh拉平
        img = img.permute(0, 2, 1)  # [b wh c]
        img = self.norm(img)
        return img
类别和位置编码
类别编码
直接cat到input上面,那么最后也取出对应的那一列作为类别输出.这是transformer类型网络的常用手段.
 个人解释:训练出类别的访问者,这个访问者可以从特征信息(原input)中提取类别信息.训练访问者方法就是类别loss回归,训练时候先果推出因,推理时因推出果
位置编码
add到input上,可以使用可学习式的位置编码也可以使用正余弦位置编码.这是transformer类型网络的常用手段,还要特征层编码等
 个人解释:训练出位置的标记者
        # 类别编码
        self.cls_token = nn.Parameter(torch.zeros(size=[1, 1, embed_dim]))
        # 固定位置编码和可学习位置编码
        # self.pos_embed = posemb_sincos_1d(len=num_patches + 1, dim=embed_dim,temperature=1000).unsqueeze(0)
        self.pos_embed = nn.Parameter(torch.zeros(1, num_patches + 1, embed_dim))
blocks
blocks使用注意力机制完成特征提取,
 个人解释:
 input线性映射为[query,key,value],需求侧(query)从供给侧(value)中取值,取值的根据是qurey@key转置生成的注意力矩阵(需求侧和供给侧每个像素之间的相似度),最后输出与输入shape相同.所以我们重复depth次,多次特征提取.
 源码直接调用:transfomer中Decoder和Encoder的base_layer的源码实现的BaseTransformerLayer类
head
主要对transfomer输出的类别特征进行映射,embed维度映射为num_class维度
self.head = nn.Linear(embed_dim, num_classes)
VIT整体
主要是上述几个模块的集合及其正向传播过程:
 完成二维图片变一维特征,一维特征transfomer特征提取,分类头输出.
class Vit(nn.Module):
    def __init__(self, img_size=[224, 224], patch_size=16, num_classes=1000,
                 embed_dim=768, depth=12, num_heads=12):
        super(Vit, self).__init__()
        self.patch_embed = PatchLayer(img_size, patch_size, embed_dim)
        num_patches = self.patch_embed.num_patches
        self.blocks = nn.Sequential(*[
            BaseTransformerLayer(attn_cfgs=[dict(embed_dim=embed_dim, num_heads=num_heads)],
                                 fnn_cfg=dict(embed_dim=embed_dim, feedforward_channels=4 * embed_dim, act_cfg='ReLU',
                                              ffn_drop=0.),
                                 operation_order=('self_attn', 'norm', 'ffn', 'norm'))
            for _ in range(depth)
        ])
        # 类别编码
        self.cls_token = nn.Parameter(torch.zeros(size=[1, 1, embed_dim]))
        # 固定位置编码和可学习位置编码
        # self.pos_embed = posemb_sincos_1d(len=num_patches + 1, dim=embed_dim,temperature=1000).unsqueeze(0)
        self.pos_embed = nn.Parameter(torch.zeros(1, num_patches + 1, embed_dim))
        # 分类头
        self.head = nn.Linear(embed_dim, num_classes)
        self.loss_class = nn.CrossEntropyLoss()  # 内置softmax
        self.init_weights()
   ''''''
   def forward(self, img):
        query = self.hand(img)
        query = self.extract_feature(query)
        cls_fea = query[:, -1, :]  # 刚刚class_token被cat到了dim1的最后一个数
        x = self.head(cls_fea)
        return x
Runner(参考mmlab)
建立优化前,设置学习率,根据指定的work_flow顺序进行训练的测试,并保留最优权重
class Runner:
    def __init__(self, arg, model, device):
        self.arg = arg
        # 建立优化器
        params = [p for p in model.parameters() if p.requires_grad]
        self.optimizer = torch.optim.SGD(params=params, lr=arg.lr, momentum=0.9, weight_decay=5E-5)
        lf = lambda x: ((1 + math.cos(x * math.pi / arg.epochs)) / 2) * (1 - arg.lrf) + arg.lrf  # cosine
        self.scheduler = torch.optim.lr_scheduler.LambdaLR(self.optimizer, lr_lambda=lf)
        self.model = model.to(device)
        self.device = device
        if arg.load_from is not None and arg.load_from != '':
            weight_dict = torch.load(arg.load_from, map_location=device)
            model.load_state_dict(weight_dict)
    def run(self, dataloaders: dict):
        # 开始训练和验证
        assert 'train' in self.arg.work_flow.keys(), '必须要用训练任务'
        epoch_start = 0
        best_accuracy = 0.0
        while epoch_start < self.arg.epochs:
            for task, times in self.arg.work_flow.items():
                if task == 'train':  # 开始训练
                    for _ in range(times):
                        epoch_start += 1  # epoch只记录训练轮
                        self.model.train()
                        loss_sum = 0.0
                        data_loader = tqdm(dataloaders['train'], file=sys.stdout)
                        for step, data_dict in enumerate(data_loader):
                            img, label = data_dict
                            instance = {
                                'data': img.to(self.device),
                                'label': label.to(self.device)
                            }
                            loss = self.model.loss(**instance)
                            loss_sum += loss.detach()  # 要十分注意 避免往计算图中引入新的东西
                            loss.backward()
                            self.optimizer.step()
                            self.optimizer.zero_grad()
                            data_loader.desc = "[train epoch {}] loss: {:.3f}".\
                                format(epoch_start,loss_sum.item() / (step + 1))
                        self.scheduler.step()
                        print('train: epoch={}, loss={}'.format(epoch_start, loss_sum / (step + 1.0)))
                elif task == 'val':  # 开始验证
                    ''''''
                else:
                    raise ValueError('task must be in [train, val, test]')
可视化
读取单张图片,转换格式输入模型,输出的label,转化为class名和置信度,显示图像,class名和置信度.
if __name__ == '__main__':
    # 建立数据集
    data_transform = transforms.Compose([transforms.Resize(256),
                                         transforms.CenterCrop(224),
                                         transforms.ToTensor(),
                                         transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5])])
    img = Image.open('*****daisy/21652746_cc379e0eea_m.jpg')
    input = data_transform(img).unsqueeze(0)
    label2class = Flowers(dataset_path='../datasets/flower_photos-mini').label2class
    device = torch.device('cuda:0')
    # 建立模型
    model = Vit(img_size=[224, 224],
                patch_size=16,
                embed_dim=768,
                depth=12,
                num_heads=12,
                num_classes=5).to(device)
    weight_dict = torch.load('weights/vit.pth', map_location=device)
    model.load_state_dict(weight_dict)
    model.eval()
    with torch.no_grad():
        output = model(input.to(device))
        output = output.detach().cpu()
        label = output[0].numpy().argmax()
        cnf = torch.softmax(output[0],dim=0).numpy().max()*100.0
        cnf = np.around(cnf, decimals=2) #保留2位小数
    plt.imshow(img)
    plt.title('{} : {}%'.format(label2class[label],cnf))
    plt.show()
总结
vit是视觉transfomer最经典的模型,复现一次代码十分有必要,中间会产生很多思考和问题.
 后面章节将会更有价值,我将会:
1.利用本次的代码进行很多思考和trick的验证
2.总结本次代码的BUG们,及其产生的原理和解决方法
如需获取全套代码请参考















![[leetcode] 22. 括号生成](https://img-blog.csdnimg.cn/direct/a36c1b59f2eb48eebd27dc7ba26116ca.png)


