https://github.com/lllyasviel/ControlNet-v1-1-nightly#controlnet-11-tiletile modelcontrol_v11f1e_sd15_tile, from https://huggingface.co/lllyasviel/ControlNet-v1-1/tree/maintile model 能够生成新的图像细节，并忽略原图里的已有图像细节，因此，该模型可以用于移除不好的细节，添加改善后的细节. 例如，移除由于图像缩放所引起的模糊.参考：超分但，并不是超分模型.其主要有两个功能：忽略图片的细节，生成新的细节若 local tile semantics 和 prompts 不匹配，则忽略 global prompts，并采用 local context 来引导 diffusion 生成.如果原图像本身已经有很好的细节，仍然可以采用 tile model 替换图像细节. Stable Diffusion's I2I 可以实现类似作用，但 tile model 更易于保持图像的整体结构，只改变细节，即使 denosing strength =1.0.ControlNet tile model，对于给定 tile，其首先识别 tile 里所包含的东西，并增强其识别到的语义的影响；且，当 内容与 prompts 不匹配时，降低 global prompts 的影响.sd-webui-controlnet tile modelhttps://github.com/Mikubill/sd-webui-controlnet/issues/1033使用方式：it can do 2x, 4x, or 8x super resolutionit can add, or change, or re-generate image details in an imageit can fix, refine, and improve bad image details obtained by any other super resolution methods like bad details or blurring from RealESRGANit can guide SD to diffuse in tiles, "one beautiful girl" will not generate 16 girls if you use 16 tiles and denoising strength 1.0.it can finish unfinished artwork drafts if those drafts are drawn by color blocksPreprocessor:The preprocessor can be none or tile_resample (tile_resample is just a resizing to make your image smaller if you want more variations)Hint:do not forget to try different control modes - it can produce interesting effectsthis model supports resolution 768 or 1024 well. You can try it if you find SD's 512 is too small and blur.
https://huggingface.co/docs/diffusers/using-diffusers/using_safetensors.ckpt 和 .safetensors[1] - .ckpt 文件是用 pickle 序列化的，可能包含恶意代码，不信任的模型来源，加载 .ckpt 可能存在安全问题.https://docs.python.org/zh-cn/3/library/pickle.html构建恶意的 pickle 数据来 在解封时执行任意代码 是可能的。 绝对不要对不信任来源的数据和可能被篡改过的数据进行解封。处理不信任数据时，更安全的序列化格式如 json 可能更为适合。[2] - .safetensors 文件是用 numpy 保存的，只包含张量数据，没有任何代码，加载 .safetensors 更安全和快速.为了将 .ckpt 文件转换为 .safetensors 文件，需要先加载 .ckpt 中的数据，然后用 numpy 保存为 .safetensors.safetensorsdoc: https://huggingface.co/docs/safetensors/index模型加载import torch import safetensors.torch import os def load_state_dict(ckpt_path, location='cpu'): _, extension = os.path.splitext(ckpt_path) if extension.lower() == ".safetensors": state_dict = safetensors.torch.load_file(ckpt_path, device=location) else: state_dict = get_state_dict(torch.load( ckpt_path, map_location=torch.device(location))) state_dict = get_state_dict(state_dict) print(f'Loaded state_dict from [{ckpt_path}]') return state_dict def get_state_dict(d): return d.get('state_dict', d)
原文：Getting Started with PyTorch Image Models (timm): A Practitioner’s Guide - 2022.02.02Github： rwightman/pytorch-image-modelsPyTorch Image Models（timm） 是一个优秀的图像分类 Python 库，其包含了大量的图像模型（Image Models）、Optimizers、Schedulers、Augmentations 等等.timm 提供了参考的 training 和 validation 脚本，用于复现在 ImageNet 上的训练结果；以及更多的 官方文档 和 timmdocs project.https://rwightman.github.io/pytorch-image-models/https://fastai.github.io/timmdocs/但，由于 timm 的功能之多，所以在定制使用时很难知道如何入手. 这里主要进行概述.pip install timm==0.5.41. Modelstimm 提供了大量的模型结构集合，而且很多模型都包含了预训练权重，或 PyTorch 训练、或从Jax和TensorFlow中移植，很方便下载使用.模型列表：https://paperswithcode.com/lib/timm查看模型列表：#打印 timm 提供的模型列表 print(timm.list_models()) print(len(timm.list_models())) #739 #带有预训练权重的模型列表 print(timm.list_models(pretrained=True)) print(len(timm.list_models(pretrained=True))) #592其中，timm.list_models() 函数：list_models(filter='', module='', pretrained=False, exclude_filters='', name_matches_cfg=False)查看特定族模型，如：print(timm.list_models('gluon_resnet*')) print(timm.list_models('*resnext*', 'resnet') ) print(timm.list_models('resnet*', pretrained=True))1.1. create_model 一般用法timm 创建模型最简单的方式是采用 create_model.以 Resnet-D 模型为例（Bag of Tricks for Image Classification For Convolutional Neural Networks paper），其是Resnet 的一种变形，其采用 average pool 进行下采样.model = timm.create_model('resnet50d', pretrained=True) print(model) #查看模型配置参数 print(model.default_cfg) ''' {'url': 'https://github.com/rwightman/pytorch-image-models/releases/download/v0.1-weights/resnet50d_ra2-464e36ba.pth', 'num_classes': 1000, 'input_size': (3, 224, 224), 'pool_size': (7, 7), 'crop_pct': 0.875, 'interpolation': 'bicubic', 'mean': (0.485, 0.456, 0.406), 'std': (0.229, 0.224, 0.225), 'first_conv': 'conv1.0', 'classifier': 'fc', 'architecture': 'resnet50d'} '''1.2. create_model 修改输入通道timm models 有个非常有用的特点，其可以处理任意通道数量的输入图像. 这是很多其他库所不具备的. 其实现原理可参考：https://fastai.github.io/timmdocs/models#So-how-is-timm-able-to-load-these-weights?model = timm.create_model('resnet50d', pretrained=True, in_chans=1) print(model) #test, single channel image x = troch.randn(1, 1, 224, 224) out = model(x) print(out.shape) #torch.Size([1, 1000])1.3. create_model 定制模型timm create_model 函数提供了很多参数，用于模型定制，函数定义如：create_model(model_name, pretrained=False, checkpoint_path='', scriptable=None, exportable=None, no_jit=None, **kwargs)**kwargs 示例参数如，global_pool - 定义最终分类层所采用的 global pooling 类型. 取决于网络结构是否用到了全局池化层.drop_rate - 设定训练时的 dropout 比例，默认是 0.num_classes - 输出类别数1.3.1. 修改类别数查看当前模型输出层：#如果输出层是 fc，则如 print(model.fc) #Linear(in_features=2048, out_features=1000, bias=True) #通用方式，查看输出层， print(model.get_classifier())修改输出层类别数：model = timm.create_model('resnet50d', pretrained=True, num_classes=10) print(model) print(model.get_classifier()) #Linear(in_features=2048, out_features=10, bias=True)如果完全不需要创建最后一层，可以将 num_classes 设为 0，模型将用恒等函数作为最后一层，其对于查看倒数第二层的输出有用.model = timm.create_model('resnet50d', pretrained=True, num_classes=0) print(model) print(model.get_classifier()) #Identity()1.3.2. Global pooling在 model.default_cfg 中出现的 pool_size 设置，说明了在分类器前用到了一个全局池化层，如：print(model.global_pool) #SelectAdaptivePool2d (pool_type=avg, flatten=Flatten(start_dim=1, end_dim=-1))其中，pool_type 支持：avg - 平均池化max - 最大池化avgmax - 平均池化和最大池化的求和，加权 0.5catevgmax - 沿着特征维度的平均池化和最大池化的输出的拼接，特征维度会翻倍'' - 不采用 pooling，其被替换为恒等操作（Identity）pool_types = ['avg', 'max', 'avgmax', 'catavgmax', ''] x = torch.randn(1, 3, 224, 224) for pool_type in pool_types: model = timm.create_model('resnet50d', pretrained=True, num_classes=0, global_pool=pool_type) model.eval() out = model(x) print(out.shape)1.3.3. 修改已有模型如，model = timm.create_model('resnet50d', pretrained=True) print(f'[INFO]Original Pooling: {model.global_pool}') print(f'[INFO]Original Classifier: {model.get_classifier}') model = model.reset_classifier(10, 'max') print(f'[INFO]Modified Pooling: {model.global_pool}') print(f'[INFO]Modified Classifier: {model.get_classifier}')1.3.4. 创建新的分类 head虽然单个线性层已经足够得到比较好的结果，但有些时候需要更大的分类 head 来提升性能.model = timm.create_model('resnet50d', pretrained=True, num_classes=10, global_pool='catavgmax') print(model) num_in_features = model.get_classifier().in_features print(num_in_features) model.fc = nn.Sequential( nn.BatchNorm1d(num_in_features), nn.Linear(in_features=num_in_features, out_features=512, bias=False), nn.ReLU(), nn.BatchNorm1d(512), nn.Dropout(0.4), nn.Linear(in_features=512, out_features=10, bias=False)) model.eval() x = troch.randn(1, 3, 224, 224) out = model(x) print(out.shape)1.4. 特征提取timm 提供了很多不同类型网络中间层的机制，其有助于作为特征提取以应用于下游任务.1.4.1. 最终特征图from PIL import Image import matplotlib.pyplot as plt import numpy as np import torch image = Image.open('test.jpg') image = torch.as_tensor(np.array(image, dtype=np.float32)).transpose(2, 0)[None] model = timm.create_model("resnet50d", pretrained=True) print(model.default_cfg) #如，只查看最终特征图，这里是池化层前的最后一个卷积层的输出 feature_output = model.forward_features(image) def vis_feature_output(feature_output): plt.imshow(feature_output[0]).transpose(0, 2).sum(-1).detach().numpy()) plt.show() # vis_feature_output(feature_output)1.4.2. 多种特征输出model = timm.create_model("resnet50d", pretrained=True, features_only=True) print(model.feature_info.module_name()) #['act1', 'layer1', 'layer2', 'layer3', 'layer4'] print(model.feature_info.reduction()) #[2, 4, 8, 16, 32] print(model.feature_info.channels()) #[64, 256, 512, 1024, 2048] out = model(image) print(len(out)) # 5 for o in out: print(o.shape) plt.imshow(o[0].transpose(0, 2).sum(-1).detach().numpy()) plt.show()1.4.3. 采用 Torch FXTorchVision 新增了一个 FX 模块，其更便于获得输入在前向计算过程中的中间变换. 通过符号性的追踪前向方法，以生成一个图，途中的每个节点表示一个操作. 由于节点是易读的，其可以很方便的准确指定到具体节点.https://pytorch.org/docs/stable/fx.html#module-torch.fxhttps://pytorch.org/blog/FX-feature-extraction-torchvision/#torchvision >= 0.11.0 from torchvision.models.feature_extraction import get_graph_node_names, create_feature_extractor model = timm.create_model("resnet50d", pretrained=True, exportable=True) nodes, _ = get_graph_node_names(model) print(nodes) features = {'layer1.0.act2': 'out'} feature_extractor = create_feature_extractor(model, return_nodes=features) print(feature_extractor) out = feature_extractor(image) plt.imshow(out['out'][0].transpose(0, 2).sum(-1).detach().numpy()) plt.show()1.5. 模型导出不同格式模型训练后，一般推荐将模型导出为优化的格式，以进行推断.1.5.1. 导出 TorchScripthttps://pytorch.org/docs/stable/jit.htmlhttps://pytorch.org/tutorials/beginner/Intro_to_TorchScript_tutorial.htmlmodel = timm.create_model("resnet50d", pretrained=True, scriptable=True) model.eval() #重要 scripted_model = torch.jit.script(model) print(scripted_model) print(scripted_model(torch.rand(8, 3, 224, 224)).shape)1.5.2. 导出 ONNXOpen Neural Network eXchange (ONNX)https://pytorch.org/docs/master/onnx.htmlmodel = timm.create_model("resnet50d", pretrained=True, exportable=True) model.eval() #重要 x = torch.randn(2, 3, 224, 224, requires_grad=True) torch_out = model(x) #Export the model torch.onnx.export(model, #模型 x, #输入 'resnet50d.onnx', #模型导出路径 export_params=True, #模型文件存储训练参数权重 opset_version=10, #ONNX 版本 do_constant_folding=True,#是否执行不断折叠优化 input_names=['input'], #输入名 output_names=['output'], #输出名 dynamic_axes={'input': {0: 'batch_size'}, 'output': {0: 'batch_size'}} ) #验证导出模型 import onnx onnx_model = onnx.load('resnet50d.onnx') onnx.checker.check_model(onnx_model) traced_model = torch.jit.trace(model, torch.rand(8, 3, 224, 224)) type(traced_model) print(traced_model(torch.rand(8, 3, 224, 224)).shape)2. Augmentationstimm 的数据格式与 TorchVision 类似，PIL 图像作为输入.from timm.data.transforms_factory import create_transform print(create_transform(224, )) ''' Compose( Resize(size=256, interpolation=bilinear, max_size=None, antialias=None) CenterCrop(size=(224, 224)) ToTensor() Normalize(mean=tensor([0.4850, 0.4560, 0.4060]), std=tensor([0.2290, 0.2240, 0.2250])) ) ''' print(create_transform(224, is_training=True)) ''' Compose( RandomResizedCropAndInterpolation(size=(224, 224), scale=(0.08, 1.0), ratio=(0.75, 1.3333), interpolation=bilinear) RandomHorizontalFlip(p=0.5) ColorJitter(brightness=[0.6, 1.4], contrast=[0.6, 1.4], saturation=[0.6, 1.4], hue=None) ToTensor() Normalize(mean=tensor([0.4850, 0.4560, 0.4060]), std=tensor([0.2290, 0.2240, 0.2250])) ) '''2.1. RandAugment对于新任务场景，很难确定要用到哪些数据增强. 且，鉴于如此多的数据增强策略，其组合数量更是庞大.一种好的起点是，采用在其他任务上被验证有效的数据增强pipeline. 如，RandAugmentRandAugment，是一种自动数据增强方法，其从增强方法集合中均匀采样，如， equalization, rotation, solarization, color jittering, posterizing, changing contrast, changing brightness, changing sharpness, shearing, and translations，并按序应用其中的一些.RandAugment: Practical automated data augmentation with a reduced search spaceRandAugment 参数：N - 随机变换的数量（ number of distortions uniformly sampled and applied per-image）M - 变换的幅度（distortion magnitude）timm 中 RandAugment 是通过配置字符串来指定的，以 - 分割符.m - 随机增强的幅度n - 每张图像进行的随机变换数，默认为 2.mstd - 标准偏差的噪声幅度mmax - 设置幅度的上界，默认 10w - 加权索引的概率（index of a set of weights to influence choice of operation）inc - 采用随幅度增加的数据增强，默认为 0如，rand-m9-n3-mstd0.5 - 幅度为9，每张图像 3 种数据增强，mstd 为 0.5rand-mstd1-w0 - mstd 为 1.0，weights 为 0，默认幅度m为10，每张图像 2 种数据增强print(create_transform(224, is_training=True, auto_augment='rand-m9-mstd0.5')) ''' Compose( RandomResizedCropAndInterpolation(size=(224, 224), scale=(0.08, 1.0), ratio=(0.75, 1.3333), interpolation=bilinear) RandomHorizontalFlip(p=0.5) RandAugment(n=2, ops= AugmentOp(name=AutoContrast, p=0.5, m=9, mstd=0.5) AugmentOp(name=Equalize, p=0.5, m=9, mstd=0.5) AugmentOp(name=Invert, p=0.5, m=9, mstd=0.5) AugmentOp(name=Rotate, p=0.5, m=9, mstd=0.5) AugmentOp(name=Posterize, p=0.5, m=9, mstd=0.5) AugmentOp(name=Solarize, p=0.5, m=9, mstd=0.5) AugmentOp(name=SolarizeAdd, p=0.5, m=9, mstd=0.5) AugmentOp(name=Color, p=0.5, m=9, mstd=0.5) AugmentOp(name=Contrast, p=0.5, m=9, mstd=0.5) AugmentOp(name=Brightness, p=0.5, m=9, mstd=0.5) AugmentOp(name=Sharpness, p=0.5, m=9, mstd=0.5) AugmentOp(name=ShearX, p=0.5, m=9, mstd=0.5) AugmentOp(name=ShearY, p=0.5, m=9, mstd=0.5) AugmentOp(name=TranslateXRel, p=0.5, m=9, mstd=0.5) AugmentOp(name=TranslateYRel, p=0.5, m=9, mstd=0.5)) ToTensor() Normalize(mean=tensor([0.4850, 0.4560, 0.4060]), std=tensor([0.2290, 0.2240, 0.2250])) ) '''也可以通过 rand_augment_transform 函数来实现：from timm.data.auto_augment import rand_augment_transform tfm = rand_augment_transform(config_str='rand-m9-mstd0.5', hparams={'img_mean': (124, 116, 104)}) print(tfm) ''' RandAugment(n=2, ops= AugmentOp(name=AutoContrast, p=0.5, m=9, mstd=0.5) AugmentOp(name=Equalize, p=0.5, m=9, mstd=0.5) AugmentOp(name=Invert, p=0.5, m=9, mstd=0.5) AugmentOp(name=Rotate, p=0.5, m=9, mstd=0.5) AugmentOp(name=Posterize, p=0.5, m=9, mstd=0.5) AugmentOp(name=Solarize, p=0.5, m=9, mstd=0.5) AugmentOp(name=SolarizeAdd, p=0.5, m=9, mstd=0.5) AugmentOp(name=Color, p=0.5, m=9, mstd=0.5) AugmentOp(name=Contrast, p=0.5, m=9, mstd=0.5) AugmentOp(name=Brightness, p=0.5, m=9, mstd=0.5) AugmentOp(name=Sharpness, p=0.5, m=9, mstd=0.5) AugmentOp(name=ShearX, p=0.5, m=9, mstd=0.5) AugmentOp(name=ShearY, p=0.5, m=9, mstd=0.5) AugmentOp(name=TranslateXRel, p=0.5, m=9, mstd=0.5) AugmentOp(name=TranslateYRel, p=0.5, m=9, mstd=0.5)) '''2.2. CutMix 和 MixupCutMix Mixuptimm 的 Mixup 类，支持的不同混合策略有：batch - CutMix vs Mixup selection, lambda, and CutMix region sampling are performed per batchpair - mixing, lambda, and region sampling are performed on sampled pairs within a batchelem - mixing, lambda, and region sampling are performed per image within batchhalf - the same as elementwise but one of each mixing pair is discarded so that each sample is seen once per epochMixup 支持的数据增强有：mixup_alpha (float): mixup alpha value, mixup is active if > 0., (default: 1)cutmix_alpha (float): cutmix alpha value, cutmix is active if > 0. (default: 0)cutmix_minmax (List[float]): cutmix min/max image ratio, cutmix is active and uses this vs alpha if not None.prob (float): the probability of applying mixup or cutmix per batch or element (default: 1)switch_prob (float): the probability of switching to cutmix instead of mixup when both are active (default: 0.5)mode (str): how to apply mixup/cutmix params (default: batch)label_smoothing (float): the amount of label smoothing to apply to the mixed target tensor (default: 0.1)num_classes (int): the number of classes for the target variablefrom timm.data import ImageDataset from torch.utils.data import DataLoader def create_dataloader_iterator(): dataset = ImageDataset('pets/images', transform=create_transform(224, )) dl = iter(DataLoader(dataset, batch_size=2)) return dl dataloader = create_dataloader_iterator() inputs, classes = next(dataloader) # out = torchvision.utils.make_grid(inputs) imshow(out, title=[x.item() for x in classes]) # from timm.data.mixup import Mixup mixup_args = {'mixup_alpha': 1., 'cutmix_alpha': 1., 'prob': 1, 'switch_prob': 0.5, 'mode': 'batch', 'label_smoothing': 0.1, 'num_classes': 2} mixup_fn = Mixup(**mixup_args) mixed_inputs, mixed_classes = mixup_fn(inputs.to(torch.device('cuda:0')), classes.to(torch.device('cuda:0'))) out = torchvision.utils.make_grid(mixed_inputs) imshow(out, title=mixed_classes)3. Datasetstimm 中 create_dataset 函数期望有两个输入参数：name - 指定待加载数据集的名字root - 数据集存放根目录其支持不同的数据存储：TorchVisionTensorFlow datasets本地文件夹#TorchVision ds = create_dataset('torch/cifar10', 'cifar10', download=True, split='train') print(ds, type(ds)) print(ds[0]) #TensorFlow ds = create_dataset('tfds/beans', 'beans', download=True, split='train[:10%]', batch_size=2, is_training=True) print(ds) ds_iter = iter(ds) image, label = next(ds_iter) #本地文件夹 ds = create_dataset(name='', root='imagenette/imagenette2-320.tar', transfor=create_transform(224)) image, label = ds[0] print(image.shape)3.1. ImageDataset 类除了 create_dataset，timm 还提供了两个 ImageDataset 和 IterableImageDataset 以适应更多的场景.from timm.data import ImageDataset imagenette_ds = ImageDataset('imagenette/imagenette2-320/train') print(len(imagenette_ds)) print(imagenette_ds.parser) print(imagenette_ds.parser.class_to_idx) from timm.data.parser.parser_image_in_tar import ParserImageTar data_path = 'imagenette' ds = ImageDataset(data_path, parser=ParserImageInTar(data_path))3.1.1. 定制 Parser参考 ParserImageFolder:""" A dataset parser that reads images from folders Folders are scannerd recursively to find image files. Labels are based on the folder hierarchy, just leaf folders by default. Hacked together by / Copyright 2020 Ross Wightman """ import os from timm.utils.misc import natural_key from .parser import Parser from .class_map import load_class_map from .constants import IMG_EXTENSIONS def find_images_and_targets(folder, types=IMG_EXTENSIONS, class_to_idx=None, leaf_name_only=True, sort=True): labels = [] filenames = [] for root, subdirs, files in os.walk(folder, topdown=False, followlinks=True): rel_path = os.path.relpath(root, folder) if (root != folder) else '' label = os.path.basename(rel_path) if leaf_name_only else rel_path.replace(os.path.sep, '_') for f in files: base, ext = os.path.splitext(f) if ext.lower() in types: filenames.append(os.path.join(root, f)) labels.append(label) if class_to_idx is None: # building class index unique_labels = set(labels) sorted_labels = list(sorted(unique_labels, key=natural_key)) class_to_idx = {c: idx for idx, c in enumerate(sorted_labels)} images_and_targets = [(f, class_to_idx[l]) for f, l in zip(filenames, labels) if l in class_to_idx] if sort: images_and_targets = sorted(images_and_targets, key=lambda k: natural_key(k[0])) return images_and_targets, class_to_idx class ParserImageFolder(Parser): def __init__( self, root, class_map=''): super().__init__() self.root = root class_to_idx = None if class_map: class_to_idx = load_class_map(class_map, root) self.samples, self.class_to_idx = find_images_and_targets(root, class_to_idx=class_to_idx) if len(self.samples) == 0: raise RuntimeError( f'Found 0 images in subfolders of {root}. Supported image extensions are {", ".join(IMG_EXTENSIONS)}') def __getitem__(self, index): path, target = self.samples[index] return open(path, 'rb'), target def __len__(self): return len(self.samples) def _filename(self, index, basename=False, absolute=False): filename = self.samples[index][0] if basename: filename = os.path.basename(filename) elif not absolute: filename = os.path.relpath(filename, self.root) return filename如：from pathlib import Path from timm.data.parsers.parser import Parser class ParserImageName(Parser): def __init__(self, root, class_to_idx=None): super().__init__() self.root = Path(root) self.samples = list(self.root.glob("*.jpg")) if class_to_idx: self.class_to_idx = class_to_idx else: classes = sorted( set([self.__extract_label_from_path(p) for p in self.samples]), key=lambda s: s.lower(), ) self.class_to_idx = {c: idx for idx, c in enumerate(classes)} def __extract_label_from_path(self, path): return "_".join(path.parts[-1].split("_")[0:-1]) def __getitem__(self, index): path = self.samples[index] target = self.class_to_idx[self.__extract_label_from_path(path)] return open(path, "rb"), target def __len__(self): return len(self.samples) def _filename(self, index, basename=False, absolute=False): filename = self.samples[index][0] if basename: filename = filename.parts[-1] elif not absolute: filename = filename.absolute() return filename # data_path = 'test' ds = ImageDataset(data_path, parser=ParserImageName(data_path)) print(ds[0]) print(ds.parser.class_to_idx)4. Optimizerstimm 支持的优化器有：SGDAdam AdamWAdamPRMSPropTFLAMB - FusedLAMB optimizer from Apex 的 PyTorch 版AdaBeliefMADGRADAdaHessianimport inspect import timm.optim optims_list = [cls_name for cls_name, cls_obj in inspect.getmembers(timm.optim) if inspect.isclass(cls_obj) if cls_name != 'Lookhead'] print(optims_list)timm 中 create_optimizer_v2 函数.import torch model = torch.nn.Sequential(torch.nn.Linear(2, 1), torch.nn.Flatten(0, 1)) optimizer = timm.optim.create_optimizer_v2(model, opt='sgd', lr=0.01, momentum=0.8) print(optimizer, type(optimizer)) ''' SGD ( Parameter Group 0 dampening: 0 lr: 0.01 momentum: 0.8 nesterov: True weight_decay: 0.0 ) <class 'torch.optim.sgd.SGD'> ''' optimizer = timm.optim.create_optimizer_v2(model, opt='lamb', lr=0.01, weight_decay=0.01) print(optimizer, type(optimizer)) ''' Lamb ( Parameter Group 0 always_adapt: False betas: (0.9, 0.999) bias_correction: True eps: 1e-06 grad_averaging: True lr: 0.01 max_grad_norm: 1.0 trust_clip: False weight_decay: 0.0 Parameter Group 1 always_adapt: False betas: (0.9, 0.999) bias_correction: True eps: 1e-06 grad_averaging: True lr: 0.01 max_grad_norm: 1.0 trust_clip: False weight_decay: 0.01 ) <class 'timm.optim.lamb.Lamb'> '''手工创建优化器，如：optimizer = timm.optim.RMSpropTF(model.parameters(), lr=0.01)4.1. 使用示例# replace # optimizer = torch.optim.Adam(model.parameters(), lr=0.01) # with optimizer = timm.optim.AdamP(model.parameters(), lr=0.01) for epoch in num_epochs: for batch in training_dataloader: inputs, targets = batch outputs = model(inputs) loss = loss_function(outputs, targets) loss.backward() optimizer.step() optimizer.zero_grad() # optimizer = timm.optim.Adahessian(model.parameters(), lr=0.01) is_second_order = ( hasattr(optimizer, "is_second_order") and optimizer.is_second_order ) # True for epoch in num_epochs: for batch in training_dataloader: inputs, targets = batch outputs = model(inputs) loss = loss_function(outputs, targets) loss.backward(create_graph=second_order) optimizer.step() optimizer.zero_grad()4.2. LookaheadLookahead Optimizer: k steps forward, 1 step backoptimizer = timm.optim.create_optimizer_v2(model.parameters(), opt='lookahead_adam', lr=0.01) #或 timm.optim.Lookahead(optimizer, alpha=0.5, k=6) optimizer.sync_lookahead() 示例如，optimizer = timm.optim.AdamP(model.parameters(), lr=0.01) optimizer = timm.optim.Lookahead(optimizer) for epoch in num_epochs: for batch in training_dataloader: inputs, targets = batch outputs = model(inputs) loss = loss_function(outputs, targets) loss.backward() optimizer.step() optimizer.zero_grad() optimizer.sync_lookahead()5. Schedulerstimm 支持的 Schedulers 有：StepLRScheduler: 每 n 次迭代衰减一次学习率，类似于 torch.optim.lr_scheduler.StepLRMultiStepLRScheduler: 设置特定迭代次数，衰减学习率，类似于 torch.optim.lr_scheduler.MultiStepLRPlateauLRScheduler: reduces the learning rate by a specified factor each time a specified metric plateaus; 类似于 torch.optim.lr_scheduler.ReduceLROnPlateauCosineLRScheduler: cosine decay schedule with restarts, 类似于 torch.optim.lr_scheduler.CosineAnnealingWarmRestartsTanhLRScheduler: hyberbolic-tangent decay schedule with restartsPolyLRScheduler: polynomial decay schedule5.1. 使用示例与PyTorch shceduler 不同的是，timm scheduler 每个 epoch 更新两次：.step_update - 每次 optimizer 更新后调用..step - 每个 epoch 结束后调用training_epochs = 300 cooldown_epochs = 10 num_epochs = training_epochs + cooldown_epochs optimizer = timm.optim.AdamP(my_model.parameters(), lr=0.01) scheduler = timm.scheduler.CosineLRScheduler(optimizer, t_initial=training_epochs) for epoch in range(num_epochs): num_steps_per_epoch = len(train_dataloader) num_updates = epoch * num_steps_per_epoch for batch in training_dataloader: inputs, targets = batch outputs = model(inputs) loss = loss_function(outputs, targets) loss.backward() optimizer.step() scheduler.step_update(num_updates=num_updates) optimizer.zero_grad() scheduler.step(epoch + 1)5.2. CosineLRScheduler为了深入阐述 timm 所提供的参数选项，这里以 timm 默认训练脚本中所采用的 sheduler - CosineLRScheduler 为例. timm 的 cosine scheduler 与 PyTorch 中的实现是不同的.5.2.1. PyTorch CosineAnnealingWarmRestartsCosineAnnealingWarmRestarts 需要设定如下参数：T_0 (int): Number of iterations for the first restart.T_mult (int): A factor that increases T_{i} after a restart. (Default: 1)eta_min (float): Minimum learning rate. (Default: 0.)last_epoch (int) — The index of last epoch. (Default: -1)#args num_epochs=300 num_epoch_repeat=num_epochs//2 num_steps_per_epoch=10 def create_model_and_optimizer(): model = torch.nn.Linear(2, 1) optimizer = torch.optim.SGD(model.parameters(), lr=0.05) return model, optimizer #create learning rate scheduler model, optimizer = create_model_and_optimizer() scheduler = torch.optim.lr_scheduler.CosineAnnealingWarmRestarts( optimizer, T_0=num_epoch_repeat*num_steps_per_epoch, T_mult=1, eta_min=1e-6, last_epoch=-1) #vis import matplotlib.pyplot as plt lrs = [] for epoch in range(num_epochs): for i in range(num_steps_per_epoch): scheduler.step() lrs.append(optimizer.param_groups[0]['lr']) plt.plot(lrs) plt.show()可以看出，lr 在 150 epoch 前保持衰减，而在第 150 epoch 时重启为初始值，并开始再次衰减.5.2.2. timm CosineLRSchedulertimm CosineLRScheduler 需要设定如下参数：t_initial (int): Number of iterations for the first restart, this is equivalent to T_0 in torch’s implementationlr_min (float): Minimum learning rate, this is equivalent to eta_min in torch’s implementation (Default: 0.)cycle_mul (float): A factor that increases T_{i} after a restart, this is equivalent to T_mult in torch’s implementation (Default: 1)cycle_limit (int): Limit the number of restarts in a cycle (Default: 1)t_in_epochs (bool): Whether the number iterations is given in terms of epochs rather than the number of batch updates (Default: True)#args num_epochs=300 num_epoch_repeat=num_epochs//2 num_steps_per_epoch=10 def create_model_and_optimizer(): model = torch.nn.Linear(2, 1) optimizer = torch.optim.SGD(model.parameters(), lr=0.05) return model, optimizer #create learning rate scheduler model, optimizer = create_model_and_optimizer() scheduler = timm.scheduler.CosineLRScheduler( optimizer, t_initial=num_epoch_repeat*num_steps_per_epoch, lr_min=1e-6, cycle_limit=num_epoch_repeat+1, t_in_epochs=False) #or scheduler = timm.scheduler.CosineLRScheduler( optimizer, t_initial=num_epoch_repeat, lr_min=1e-6, cycle_limit=num_epoch_repeat+1, t_in_epochs=True) #vis import matplotlib.pyplot as plt lrs = [] for epoch in range(num_epochs): num_updates = epoch * num_steps_per_epoch for i in range(num_steps_per_epoch): num_updates += 1 scheduler.step_update(num_updates=num_updates) scheduler.step(epoch+1) lrs.append(optimizer.param_groups[0]['lr']) plt.plot(lrs) plt.show()示例策略：scheduler = timm.scheduler.CosineLRScheduler( optimizer, t_initial=num_epoch_repeat*num_steps_per_epoch, cycle_mul=2., cycle_limit=num_epoch_repeat+1, t_in_epochs=False) scheduler = timm.scheduler.CosineLRScheduler( optimizer, t_initial=num_epoch_repeat*num_steps_per_epoch, lr_min=1e-5, cycle_limit=1) scheduler = timm.scheduler.CosineLRScheduler( optimizer, t_initial=50, lr_min=1e-5, cycle_decay=0.8, cycle_limit=num_epoch_repeat+1) scheduler = timm.scheduler.CosineLRScheduler( optimizer, t_initial=num_epoch_repeat*num_steps_per_epoch, lr_min=1e-5, k_decay=0.5, cycle_limit=num_epoch_repeat+1) scheduler = timm.scheduler.CosineLRScheduler( optimizer, t_initial=num_epoch_repeat*num_steps_per_epoch, lr_min=1e-5, k_decay=2, cycle_limit=num_epoch_repeat+1)5.2.3. 添加 warm up如，设置 20 个 warm up epochs，#args num_epochs=300 num_epoch_repeat=num_epochs//2 num_steps_per_epoch=10 def create_model_and_optimizer(): model = torch.nn.Linear(2, 1) optimizer = torch.optim.SGD(model.parameters(), lr=0.05) return model, optimizer #create learning rate scheduler scheduler = timm.scheduler.CosineLRScheduler( optimizer, t_initial=num_epoch_repeat, lr_min=1e-5, cycle_limit=num_epoch_repeat+1, warmup_lr_init=0.01, warmup_t=20) #vis import matplotlib.pyplot as plt lrs = [] for epoch in range(num_epochs): num_updates = epoch * num_steps_per_epoch for i in range(num_steps_per_epoch): num_updates += 1 scheduler.step_update(num_updates=num_updates) scheduler.step(epoch+1) lrs.append(optimizer.param_groups[0]['lr']) plt.plot(lrs) plt.show()5.2.4. 添加 noise#args num_epochs=300 num_epoch_repeat=num_epochs//2 num_steps_per_epoch=10 def create_model_and_optimizer(): model = torch.nn.Linear(2, 1) optimizer = torch.optim.SGD(model.parameters(), lr=0.05) return model, optimizer #create learning rate scheduler scheduler = timm.scheduler.CosineLRScheduler( optimizer, t_initial=num_epoch_repeat, lr_min=1e-5, cycle_limit=num_epoch_repeat+1, noise_range_t=(0, 150), #noise_range_t：噪声范围 noise_pct=0.1) #noise_pct：噪声程度 #vis import matplotlib.pyplot as plt lrs = [] for epoch in range(num_epochs): num_updates = epoch * num_steps_per_epoch for i in range(num_steps_per_epoch): num_updates += 1 scheduler.step_update(num_updates=num_updates) scheduler.step(epoch+1) lrs.append(optimizer.param_groups[0]['lr']) plt.plot(lrs) plt.show()5.3. timm 默认设置def create_model_and_optimizer(): model = torch.nn.Linear(2, 1) optimizer = torch.optim.SGD(model.parameters(), lr=0.05) return model, optimizer #create learning rate scheduler model, optimizer = create_model_and_optimizer() #args training_epochs=300 cooldown_epochs=10 num_epochs=training_epochs + cooldown_epochs num_steps_per_epoch=10 scheduler = timm.scheduler.CosineLRScheduler( optimizer, t_initial=training_epochs, lr_min=1e-6, t_in_epochs=True, warmup_t=3, warmup_lr_init=1e-4, cycle_limit=1) # no restart #vis import matplotlib.pyplot as plt lrs = [] for epoch in range(num_epochs): num_updates = epoch * num_steps_per_epoch for i in range(num_steps_per_epoch): num_updates += 1 scheduler.step_update(num_updates=num_updates) scheduler.step(epoch+1) lrs.append(optimizer.param_groups[0]['lr']) plt.plot(lrs) plt.show()5.4. 其他 Scheduler#TanhLRScheduler scheduler = timm.scheduler.TanhLRScheduler( optimizer, t_initial=num_epoch_repeat, lr_min=1e-6, cycle_limit=num_epoch_repeat+1) #PolyLRScheduler scheduler = timm.scheduler.PolyLRScheduler( optimizer, t_initial=num_epoch_repeat, lr_min=1e-6, cycle_limit=num_epoch_repeat+1) scheduler = timm.scheduler.PolyLRScheduler( optimizer, t_initial=num_epoch_repeat, lr_min=1e-6, cycle_limit=num_epoch_repeat+1, k_decay=0.5) scheduler = timm.scheduler.PolyLRScheduler( optimizer, t_initial=num_epoch_repeat, lr_min=1e-6, cycle_limit=num_epoch_repeat+1, k_decay=2)6. EMA 模型指数移动平均EMA，Exponential Moving Average Model模型训练时，一种好的方式是，将模型权重值设置为整个训练过程中所有参数的移动平均，而不是仅仅只采用最后一次增量更新的.实际上，这往往是通过保持 EMA 来实现的，其是训练的模型副本.不过，相比于每次更新 step 更新全量的模型参数，一般将这些参数设置为当前参数值和更新参数值的线性组合，公式如下：$$ updated\_EMA\_model\_weights = decay * EMA\_model\_weights + (1. - decay) * updated\_model\_weights $$如，$$ updated\_EMA\_model\_weights = 0.99 * EMA\_model\_weights + 0.01 * updated\_model\_weights $$timm 中 ModelEmaV2 示例，model = create_model().to(gpu_device) ema_model = timm.utils.ModelEmaV2(model, decay=0.9998) for epoch in num_epochs: for batch in training_dataloader: inputs, targets = batch outputs = model(inputs) loss = loss_function(outputs, targets) loss.backward() optimizer.step() optimizer.zero_grad() ema_model.update(model) for batch in validation_dataloader: inputs, targets = batch outputs = model(inputs) validation_loss = loss_function(outputs, targets) ema_model_outputs = ema_model.module(inputs) ema_model_validation_loss = loss_function(ema_model_outputs, targets)
多模态模型，这里主要是视觉和文本2个模态交互的模型。一般的思路是，分别对文本和视觉进行特征提取，得到embedding，然后进行early或者late fusion，再根据预训练任务进行参数更新。[1] - 视觉特征的提取直接提取视觉向量；目标检测，大多用的是Faster R-CNN，输出bounding box和对应位置；提取类别特征潜在问题：提取的信息有冗余和噪音，对下游任务有益的视觉信息没有被有效提取，和对应的文本信息有语义鸿沟，不好做视觉-文本对齐等。[2] - 文本特征的提取文本一般会用BERT等做初始化，大规模训练集会从头开始训练。Early fusion：特征提取后，一般是直接concat，进入transformer；Late fusion：特征提取后，模态内部再进行深度学习，再做模态交互。计算机视觉：计算机具有看到并理解其所看到的东西的能力，以类似于人类；自然语言处理：计算机具有理解语言的能力，以类似于人类的方式。CLIP 架起了计算机视觉与自然语言处理之间的桥梁.CLIPCLIP，Contrastive Language–Image Pre-training[1] - 双流模型，文本和视觉分别进入transformer encoder，经过线性投影计算不同图文对的相似度；[2] - 使用对比学习，将图片分类转换成图文匹配任务。用4亿对来自网络的图文数据集，将文本作为图像标签，进行训练。进行下游任务时，只需要提供和图上的concepts对应的文本描述，就可以进行zero-shot transfer。[3] - 输入图片到 CLIP 模型，可以返回与图片标题（描述）或摘要.数据：4亿个网络公开的图文对。为覆盖到更多的视觉concepts， 用了50w个query在搜索引擎搜索图片，一个query差不多有2w张图片。输入：一个batch有N个图像文本对；模型：对比学习，预测对图文数据，将图片分类任务转换成图文匹配任务：[1] - 双流，2个encoder分别处理文本和图片数据，text encoder使用Transformer，image encoder用了2种模型，ResNet和Vision Transformer(ViT)；5种ResNet：ResNet-50, ResNet-101, EfficientNet-style的ResNet，包括RN50x4, RN50x16, RN50x64；3种ViT：ViT-B/32, ViT-B/16, ViT-L/14；[2] - encoder representation直接线性投影到multi-modal embedding space；[3] - 计算2模态之间的cosine similarity，让N个匹配的图文对相似度最大，不匹配的图文对相似度最小；[4] - 对称的cross-entropy loss；[5] - 数据增强：对resized图片进行random square crop。[6] - 将分类模型转换成图文匹配任务，用文本来弱监督图片分类。实验Zero-shot Transfer图片分类的zero-shot指的是对未知类别进行推理。本文的zero-shot指的是对未知任务进行推理，通过zero-shot transfer衡量任务学习的能力。Visual N-Grams (Li et al., 2017) 是第一个将zero-shot transfer应用到图片分类任务上的模型。模型用于学习长度为1~5grams的共142806个visual n-grams，对输入的图片，最大化对应的n-grams的probability。同样的，CLIP在进行zero-shot transfer时，将数据集中的类别标签转换为文字描述，主要步骤如下：输入：一张图片 + 所有类别转换的文本（100个类别就是100个文本描述）；转换向量：经过2个encoder，分别输出image和text的feature embedding；计算cosine similarity；预测类别：multinomial logistic regression classifier。zero-shot prediction 代码zero-shot prediction基于输入的图片，在类别描述中检索，找到最合适的类别。"Ref：https://github.com/openai/CLIP" import os import clip import torch from torchvision.datasets import CIFAR100 # Load the model device = "cuda" if torch.cuda.is_available() else "cpu" model, preprocess = clip.load('ViT-B/32', device) # Download the dataset cifar100 = CIFAR100(root=os.path.expanduser("~/.cache"), download=True, train=False) # Prepare the inputs image, class_id = cifar100[3637] image_input = preprocess(image).unsqueeze(0).to(device) text_inputs = torch.cat([clip.tokenize(f"a photo of a {c}") for c in cifar100.classes]).to(device) #cifar每个类别，输入图片，检索匹配的类别 # Calculate features with torch.no_grad(): image_features = model.encode_image(image_input) text_features = model.encode_text(text_inputs) # Pick the top 5 most similar labels for the image image_features /= image_features.norm(dim=-1, keepdim=True) text_features /= text_features.norm(dim=-1, keepdim=True) similarity = (100.0 * image_features @ text_features.T).softmax(dim=-1) values, indices = similarity[0].topk(5) # Print the result print("\nTop predictions:\n") for value, index in zip(values, indices): print(f"{cifar100.classes[index]:>16s}: {100 * value.item():.2f}%") """ Top predictions: snake: 65.31% turtle: 12.29% sweet_pepper: 3.83% lizard: 1.88% crocodile: 1.75% """Linear-probe evaluation将 pretraining 的模型视作特征提取器，增加一个分类头，只训练这个分类头。第二种方式的直觉在于“一个好的特征应该能够区分不同的类”，除此之外，fine-tune 效果好有可能是因为架构很适合下游任务，但是 linear-probe 只取决于特征质量。通过CLIP的image_encoder得到视觉向量，结合标签做Logistic Regression。"Ref：https://github.com/openai/CLIP" import os import clip import torch import numpy as np from sklearn.linear_model import LogisticRegression from torch.utils.data import DataLoader from torchvision.datasets import CIFAR100 from tqdm import tqdm # Load the model device = "cuda" if torch.cuda.is_available() else "cpu" model, preprocess = clip.load('ViT-B/32', device) # Load the dataset root = os.path.expanduser("~/.cache") train = CIFAR100(root, download=True, train=True, transform=preprocess) test = CIFAR100(root, download=True, train=False, transform=preprocess) def get_features(dataset): all_features = [] all_labels = [] with torch.no_grad(): for images, labels in tqdm(DataLoader(dataset, batch_size=100)): features = model.encode_image(images.to(device)) all_features.append(features) all_labels.append(labels) return torch.cat(all_features).cpu().numpy(), torch.cat(all_labels).cpu().numpy() # Calculate the image features train_features, train_labels = get_features(train) test_features, test_labels = get_features(test) # Perform logistic regression classifier = LogisticRegression(random_state=0, C=0.316, max_iter=1000, verbose=1) # c自定义 classifier.fit(train_features, train_labels) # Evaluate using the logistic regression classifier predictions = classifier.predict(test_features) accuracy = np.mean((test_labels == predictions).astype(np.float)) * 100. print(f"Accuracy = {accuracy:.3f}")CLIP 局限[1] - 不是和SOTA的比较：以上的数据分析，都是和a linear classifier on top of ResNet-50 features进行比较，大部分的数据集，都有对应的SOTA模型。为了达到SOTA，zero-shot CLIP估计要提高1000x的算力，当前情况不支持；[2] - 在部分fine-grained分类上表现不佳：前面实验分析发现，模型不能很好的区分cars，species of flowers, 以及variants of aircraft；abstract和systematic任务表现不好，比如统计图上object的数量；在训练集中基本不会出现的比较novel的任务，表现欠佳，比如classifyingdistance to the nearest car in a photo；[3] - 训练集中没有出现的图片类型（out-of-distribution），表现不好，比如OCR识别数字效果可以，但是MNIST的准确率只有88%；其余Align before Fuse: Vision and Language Representation Learning with Momentum Distillationhttps://arxiv.org/pdf/2107.07651.pdf https://github.com/salesforce/ALBEF提取的视觉token和文本token不对齐，做图文特征交叉是个难点。 之前的模型，使用bounding box提取region feature，本文提出ALBEF，通过cross-modal attention，实现图文表征学习。且为了从noisy的web数据上提升模型效果，提出momentum distillation，这是一种self-training method，学习pseudo-targets。实验结果显示，模型在多个下游任务上，达到SOTA。应用1. CLIP-ArtCLIP-Art: Contrastive Pre-training for Fine-Grained Art Classification-CVPR2021https://github.com/KeremTurgutlu/clip_art解决两个问题：实例检索(instance retrieval)细粒度艺术属性识别(fine-grained artwork attribute recognition)将给定图片的噪声细粒度类别标注转为自然语言文本；iMet 数据集，每张图片生成了超过 15 的文本描述微调 ViT-B/32 CLIP 模型，InfoNCE loss（使正样本对之间的互信息最大，使负样本对之间的互信息最小）域适应$CLIP_{art}$ 以进一步微调到细粒度艺术识别任务2. CLIP + StyleGANhttps://github.com/vipermu/StyleCLIPused CLIP and StyleGAN to generate portraits in the style of "My Little Pony."https://twitter.com/metasemantic/status/1368713208429764616A pony that looks like Elvis PresleyGenerating Images from Prompts using CLIP and StyleGAN3. 语义搜索https://twitter.com/metasemantic/status/13494465859529891864. 跨模态搜索5. 图像分类CLIP vs. ResNext6. StyleGAN-NADAStyleGAN-NADA: CLIP-Guided Domain Adaptation of Image Generatorshttps://github.com/rinongal/StyleGAN-nadahttps://stylegan-nada.github.io/7. StyleCLIPStyleCLIP: Text-Driven Manipulation of StyleGAN Imageryhttps://github.com/orpatashnik/StyleCLIP材料[1] - A Beginner’s Guide to the CLIP Model[2] - CLIP from OpenAI: what is it and how you can try it out yourself[3] - Simple Implementation of OpenAI CLIP model: A Tutorial[4] - Generating Images from Prompts using CLIP and StyleGAN
原文：从此明白了卷积神经网络（CNN）- 2021.11.05出处：SimpleAI - 微信公众号图示很赞，好文，记录，学习备忘.1. 引子-边缘检测看一个最简单的例子：“边界检测（edge detection）”，假设有这样的一张图片，大小8×8：图片中的数字代表该位置的像素值，像素值越大，颜色越亮，所以为了示意，把右边小像素的地方画成深色。图的中间两个颜色的分界线就是要检测的边界。怎么检测这个边界呢？可以设计这样的一个 滤波器（filter，也称为kernel），大小3×3：然后，用这个filter，往图片上“盖”，覆盖一块跟filter一样大的区域之后，对应元素相乘，然后求和。计算一个区域之后，就向其他区域挪动，接着计算，直到把原图片的每一个角落都覆盖到了为止。这个过程就是 “卷积”。（不用管卷积在数学上到底是指什么运算，只用知道在CNN中是怎么计算的。）这里的“挪动”，就涉及到一个步长了，假如步长是1，那么覆盖了一个地方之后，就挪一格，容易知道，总共可以覆盖6×6个不同的区域。那么，将这6×6个区域的卷积结果，拼成一个矩阵：可以发现这个图片，中间颜色浅，两边颜色深，这说明原图片中间的边界，在这里被反映出来了!从上面这个例子中发现，可以通过设计特定的filter，让它去跟图片做卷积，就可以识别出图片中的某些特征，比如边界。上面的例子是检测竖直边界，也可以设计出检测水平边界的，只用把刚刚的filter旋转90°即可。对于其他的特征，理论上只要经过精细的设计，总是可以设计出合适的filter的。CNN（convolutional neural network），主要就是通过一个个的filter，不断地提取特征，从局部的特征到总体的特征，从而进行图像识别等等功能。那么问题来了，怎么可能去设计这么多各种各样的filter呀？首先，都不一定清楚对于一大推图片，需要识别哪些特征；其次，就算知道了有哪些特征，想真的去设计出对应的filter，恐怕也并非易事，要知道，特征的数量可能是成千上万的。其实学过神经网络之后，就知道，这些filter，根本就不用去设计，每个filter中的各个数字，不就是参数吗，可以通过大量的数据，来 让机器自己去“学习”这些参数嘛。这，就是CNN的原理。2. CNN的基本概念2.1. Padding从上面的引子中可知，原图像在经过filter卷积之后，变小了，从(8,8)变成了(6,6)。假设再进行一次卷积，那大小就变成了(4,4)了。这样有啥问题呢？ 主要有两个问题：[1] - 每次卷积，图像都缩小，这样卷不了几次就没了；[2] - 相比于图片中间的点，图片边缘的点在卷积中被计算的次数很少。这样的话，边缘的信息就易于丢失。为了解决这个问题，可以采用padding的方法。每次卷积前，先给图片周围都补一圈空白，让卷积之后图片跟原来一样大，同时，原来的边缘也被计算了更多次。比如，把(8,8)的图片给补成(10,10)，那么经过(3,3)的filter之后，就是(8,8)，没有变。把上面这种“让卷积之后的大小不变”的padding方式，称为 “Same”方式，把不经过任何填白的，称为 “Valid”方式。这个是在使用一些框架的时候，需要设置的超参数。2.2. Stride前面所介绍的卷积，都是默认步长是1，但实际上，还可以设置步长为其他的值。比如，对于(8,8)的输入，用(3,3)的filter，如果stride=1，则输出为(6,6);如果stride=2，则输出为(3,3);（这里例子举得不大好，除不断就向下取整）2.3. PoolingPooling，是为了提取一定区域的主要特征，并减少参数数量，防止模型过拟合。比如下面的MaxPooling，采用了一个2×2的窗口，并取stride=2：除了MaxPooling,还有AveragePooling，顾名思义就是取那个区域的平均值。2.4. 对多通道（channels）图片的卷积这个需要单独提一下。彩色图像，一般都是RGB三个通道（channel）的，因此输入数据的维度一般有三个：（长，宽，通道）。 比如一个28×28的RGB图片，维度就是(28,28,3)。前面的引子中，输入图片是2维的(8,8)，filter是(3,3)，输出也是2维的(6,6)。如果输入图片是三维的呢（即增多了一个channels），比如是(8,8,3)，这个时候，filter的维度就要变成(3,3,3)了，它的 最后一维要跟输入的channel维度一致。这个时候的卷积，是三个channel的所有元素对应相乘后求和，也就是之前是9个乘积的和，现在是27个乘积的和。因此，输出的维度并不会变化。还是(6,6)。但是，一般情况下，会使用多了filters同时卷积，比如，如果同时使用4个filter的话，那么输出的维度则会变为(6,6,4)。如下图，来展示上面的过程：图中的输入图像是(8,8,3)，filter有4个，大小均为(3,3,3)，得到的输出为(6,6,4)。这个图画的真清晰！而且给出了3和4这个两个关键数字是怎么来的。其实，如果套用前面学过的神经网络的符号来看待CNN的话，输入图片就是X，shape=(8,8,3);4个filters其实就是第一层神经网络的参数W1,，shape=(3,3,3,4),这个4是指有4个filters;输出是Z1，shape=(6,6,4);后面其实还应该有一个激活函数，比如relu，经过激活后，Z1变为A1，shape=(6,6,4);所以，在前面的图中，加一个激活函数，给对应的部分标上符号，就是这样的：3. CNN 的结构组成已经知道了卷积（convolution）、池化（pooling）以及填白（padding）是怎么进行的，接下来就来看看CNN的整体结构，它包含了3种层（layer）：1. Convolutional layer（卷积层—CONV）由滤波器filters和激活函数构成。一般要设置的超参数包括filters的数量、大小、步长，以及padding是“valid”还是“same”。当然，还包括选择什么激活函数。2. Pooling layer （池化层—POOL）这里里面没有参数需要学习，因为里面的参数都是设置好了，要么是Maxpooling，要么是Averagepooling。需要指定的超参数，包括是Max还是average，窗口大小以及步长。通常使用的比较多的是Maxpooling, 而且一般取大小为(2,2)步长为2的filter，这样，经过pooling之后，输入的长宽都会缩小2倍，channels不变。3. Fully Connected layer（全连接层—FC）神经网络中的那种最普通的层，就是一排神经元。因为这一层是每一个单元都和前一层的每一个单元相连接，所以称之为“全连接”。这里要指定的超参数，无非就是神经元的数量，以及激活函数。接下来，随便看一个CNN的模样，来获取对CNN的一些感性认识：这个CNN的结构可以用：X→CONV(relu)→MAXPOOL→CONV(relu)→FC(relu)→FC(softmax)→Y来表示。这里需要说明的是，在经过数次卷积和池化之后，最后会先将多维的数据进行“扁平化”，也就是把 (height,width,channel)的数据压缩成长度为 height × width × channel 的一维数组，然后再与 FC层连接，这之后就跟普通的神经网络无异了。可以从图中看到，随着网络的深入，图像（严格来说中间的那些不能叫图像了，但是为了方便，还是这样说吧）越来越小，但是channels却越来越大了。在图中的表示就是长方体面对径向面积越来越小，但是长度却越来越长了。4. 卷积神经网络 VS. 传统神经网络其实现在回过头来看，CNN跟之前学习的神经网络，也没有很大的差别。传统的神经网络，其实就是多个FC层叠加起来。CNN，无非就是把FC改成了CONV和POOL，就是把传统的由一个个神经元组成的layer，变成了由filters组成的layer。那么，为什么要这样变？有什么好处？具体说来有两点：4.1.参数共享机制（parameters sharing）对比一下传统神经网络的层和由filters构成的CONV层：假设图像是8×8大小，也就是64个像素，假设用一个有9个单元的全连接层：那这一层需要多少个参数呢？需要 64×9 = 576个参数（先不考虑偏置项b）。因为每一个链接都需要一个权重w。再看看 同样有9个单元的filter是怎么样的：其实不用看就知道，有几个单元就几个参数，所以总共就9个参数！因为，对于不同的区域，都共享同一个filter，因此就共享这同一组参数。这也是有道理的，通过前面的讲解可知，filter是用来检测特征的，那一个特征一般情况下很可能在不止一个地方出现，比如“竖直边界”，就可能在一幅图中多出出现，那么 共享同一个filter不仅是合理的，而且是应该这么做的。由此可见，参数共享机制，让网络的参数数量大大地减少。这样，可以用较少的参数，训练出更加好的模型，典型的事半功倍，而且可以有效地 避免过拟合。同样，由于filter的参数共享，即使图片进行了一定的平移操作，仍然可以识别出特征，这叫做 “平移不变性”。因此，模型就更加稳健了。4.2. 连接的稀疏性（sparsity of connections）由卷积的操作可知，输出图像中的任何一个单元，只跟输入图像的一部分有关系：而传统神经网络中，由于都是全连接，所以输出的任何一个单元，都要受输入的所有的单元的影响。这样无形中会对图像的识别效果大打折扣。比较，每一个区域都有自己的专属特征，不希望它受到其他区域的影响。正是由于上面这两大优势，使得CNN超越了传统的NN，开启了神经网络的新时代。
出处：Paddle文档平台 - TSM1. TSM 简介视频流的爆炸性增长为以高精度和低成本执行视频理解任务带来了挑战。传统的2D CNN计算成本低，但无法捕捉视频特有的时间信息；3D CNN可以得到良好的性能，但计算量庞大，部署成本高。作者提出了一种通用且有效的时间偏移模块（Temporal Shift Module，TSM），它通过沿时间维度移动部分通道来促进相邻帧间的信息交换，同时它可以插入到2D CNN中实现零计算和零参数的时间建模，以此兼具2D卷积的高效与3D卷积的高性能。2. 模型介绍2.1. Intuition首先考虑一个正常的卷积操作，以核大小为3的一维卷积为例。假设卷积的权重为 $W = (w_1, w_2, w_3)$，输入 $X$ 是一个1D无限长的向量，则卷积操作 $Y = Conv(W,X)$ 可被表示为：$$ Y_i = w_1X_{i-1} + w_2X_i + w_3X_{i+1} $$将卷积操作解耦为两步，位移和乘法累加。对输入 $X$ 进行 $-1, 0, +1$ 的位移，具体表示为：$$ X^{-1}_{i} = X_{i - 1}, \quad X^{0}_{i} = X_i, \quad X^{+1}_{i} = X_{i+1} $$乘法累加可表示为：$$ Y = w_1X^{-1} + w_2X^0 + w_3X^{+1} $$第一步位移是不需要时间成本的，第二步乘法累加需要更大的计算消耗，但是TSM将乘法累加部分合并在了2D卷积中，因此它和基本的2D CNN网络相比不存在额外开销。图1 Temporal Shift moduleTemporal Shift Module(TSM) 如 图1 所示，在 图1 a 中，作者描述了一个具有C通道和T帧的张量，图片中每一行不同颜色代表在不同时间点的特征，沿时间维度，我们将一部分通道移动-1，另一部分通道移动+1，其余部分不动（如 图1 b 所示）。对于在线视频识别，作者也提供了在线版本的TSM（如 图1c 所示），由于在在线识别模式中，我们不能获得未来帧，因此只进行单一方向的移动。2.2. 简易的空间移动会带来什么问题虽然时间位移的原理很简单，但作者发现直接将空间位移策略应用于时间维度并不能提供高性能和效率。具体来说，如果简单的转移所有通道，则会带来两个问题：[1] - 由于大量数据移动而导致的效率下降问题。位移操作不需要计算但是会涉及数据移动，数据移动增加了硬件上的内存占用和推理延迟，作者观察到在视频理解网络中，当使用naive shift策略时，CPU延迟增加13.7%，GPU延迟增加12.4%，使整体推理变慢。[2] - 空间建模能力变差导致性能下降，由于部分通道被转移到相邻帧，当前帧不能再访问通道中包含的信息，这可能会损失2D CNN主干的空间建模能力。与TSN基线相比，使用naive shift会降低2.6%的准确率。2.3. TSM模块为了解决naive shift的两个问题，TSM给出了相应的解决方法。[1] - 减少数据移动。 为了研究数据移动的影响，作者测量了TSM模型在不同硬件设备上的推理延迟，作者移动了不同比例的通道数并测量了延迟，位移方式分为无位移、部分位移（位移1/8、1/4、1/2的通道）和全部位移，使用ResNet-50主干和8帧输入测量模型。作者观察到，如果移动所有的通道，那么延迟开销将占CPU推理时间的13.7%（如 图2 a 所示），如果只移动一小部分通道，如1/8，则可将开销限制在3%左右。图2 不同比例的通道位移下延迟与准确率对比[2] - 保持空间特征学习能力。 一种简单的TSM使用方法是将其直接插入到每个卷基层或残差模块前，如 图3 a 所示，这种方法被称为 in-place shift，但是它会损失主干模型的空间特征学习能力，尤其当我们移动大量通道时，存储在通道中的当前帧信息会随着通道移动而丢失。为解决这个问题，作者提出了另一种方法，即将TSM放在残差模块的残差分支中，这种方法被称为 residual TSM，如 图3 b 所示，它可以解决退化的空间特征学习问题，因为原始的激活信息在时间转移后仍可通过identity映射访问。 图3 In-place TSM 和 Residual TSM为检验上述假设，作者在 Kinetics 数据集上比较了 In-place TSM 和 Residual TSM 的性能。在 图2 b 中我们可以看到，对于所有比例的位移，Residual TSM 都具有更好的性能。同时，作者发现，性能与位移通道的比例有关：如果比例太小，则时间推理的能力可能不足以处理复杂的时间关系；如果太大，则会损害空间特征学习能力，选择1/4的通道偏移时，性能会达到峰值。2.4. TSM 视频网络Offline Models with Bi-directional TSM 作者使用双向TSM来构建离线视频识别模型。给定视频 V，首先从视频中采样T帧 $F_1, ..., F_T$。帧采样后，2D CNN单独处理每个帧，并对输出logits求平均值以给出最终预测。我们为每个残差模块插入了TSM，无需计算即可实现时间信息融合。在论文中，作者使用ResNet50作为网络主干。Online Models with Uni-directional TSM在线视频理解是现实生活中很重要的任务，单向TSM将特征从前一帧转移到当前帧。用于在线识别的单向TSM 推理图如 图4 所示，在推理过程中，对于每一帧，我们保存每个残差块的前 1/8 特征图并将其缓存在内存中，对于下一帧，我们用缓存的特征图来替换当前特征图的前 1/8。我们使用 7/8 当前特征图和 1/8 前一帧的特征图组合来生成下一层，并重复。图4 单向TSM3. 实验结果表1 与TSN在不同数据集上的精度对比如 表1 所示，作者在不同的数据集上分别测试了TSN的精度和TSM的精度。该表格可分为两部分，上部分涉及的数据集在时间关系上没有那么重要，TSM的计算结果小幅度优于2D TSN基线。下部分数据集，Something-Something V1和V2 以及 Jester，它们很大程度上取决于时间关系，TSM在这些数据集上性能有大幅度的明显提升。表2 与SOTA模型对比作者在Something-Something V1数据集上将TSM模型的性能与最先进的方法进行了对比。首先，由于TSN缺少时间建模，因此无法获得良好的性能。对于 TRN，虽然在特征提取后添加了后期时间融合，但其性能仍显著低于最先进的方法，跨所有层的时间融合的重要性。在第二部分中，TSM与高效视频理解框架ECO进行对比。ECO使用早期2D + 晚期3D的结构，可实现中级时间融合。与ECO相比，TSM在较小的FLOP上获得了更好的性能。第三部分包含当前的最新方法： Non-local I3D + GCN，可实现所有级别的时间融合。但由于GCN需要使用一个在MSCOCO对象检测数据集上训练的地区提议网络来生成边界框，这引入了额外的数据和训练成本，因此不能公平的进行比较。只将TSM与它的CNN部分（Non-local I3D）比较的话，TSM在验证集上的FLOP减小了10倍，精度提升1.2%。
出处：[Paddle文档平台 - 图像分类1. LeNet1.1. 模型介绍LeNet是最早的卷积神经网络之一[1]，其被提出用于识别手写数字和机器印刷字符。1998年，Yann LeCun第一次将LeNet卷积神经网络应用到图像分类上，在手写数字识别任务中取得了巨大成功。算法中阐述了图像中像素特征之间的相关性能够由参数共享的卷积操作所提取，同时使用卷积、下采样（池化）和非线性映射这样的组合结构，是当前流行的大多数深度图像识别网络的基础。1.2. 模型结构LeNet通过连续使用卷积和池化层的组合提取图像特征，其架构如 图1 所示，这里展示的是用于MNIST手写体数字识别任务中的LeNet-5模型：图1 LeNet模型网络结构示意图[1] - 第一模块：包含5×5的6通道卷积和2×2的池化。卷积提取图像中包含的特征模式（激活函数使用Sigmoid），图像尺寸从28减小到24。经过池化层可以降低输出特征图对空间位置的敏感性，图像尺寸减到12。[2] - 第二模块：和第一模块尺寸相同，通道数由6增加为16。卷积操作使图像尺寸减小到8，经过池化后变成4。[3] - 第三模块：包含4×4的120通道卷积。卷积之后的图像尺寸减小到1，但是通道数增加为120。将经过第3次卷积提取到的特征图输入到全连接层。第一个全连接层的输出神经元的个数是64，第二个全连接层的输出神经元个数是分类标签的类别数，对于手写数字识别的类别数是10。然后使用Softmax激活函数即可计算出每个类别的预测概率。提示：卷积层的输出特征图如何当作全连接层的输入使用呢？卷积层的输出数据格式是$[N, C, H, W]$，在输入全连接层的时候，会自动将数据拉平，也就是对每个样本，自动将其转化为长度为$K$的向量，其中$K = C \times H \times W$，一个mini-batch的数据维度变成了$N\times K$的二维向量。1.3. 模型实现LeNet网络的实现代码如下：#! -*- coding:utf-8 -*- import paddle import numpy as np from paddle.nn import Conv2D, MaxPool2D, Linear import paddle.nn.functional as F # 定义 LeNet 网络结构 class LeNet(paddle.nn.Layer): def __init__(self, num_classes=1): super(LeNet, self).__init__() # 创建卷积和池化层 # 创建第1个卷积层 self.conv1 = Conv2D(in_channels=1, out_channels=6, kernel_size=5) self.max_pool1 = MaxPool2D(kernel_size=2, stride=2) # 尺寸的逻辑：池化层未改变通道数；当前通道数为6 # 创建第2个卷积层 self.conv2 = Conv2D(in_channels=6, out_channels=16, kernel_size=5) self.max_pool2 = MaxPool2D(kernel_size=2, stride=2) # 创建第3个卷积层 self.conv3 = Conv2D(in_channels=16, out_channels=120, kernel_size=4) # 尺寸的逻辑：输入层将数据拉平[B,C,H,W] -> [B,C*H*W] # 输入size是[28,28]，经过三次卷积和两次池化之后，C*H*W等于120 self.fc1 = Linear(in_features=120, out_features=64) # 创建全连接层，第一个全连接层的输出神经元个数为64， 第二个全连接层输出神经元个数为分类标签的类别数 self.fc2 = Linear(in_features=64, out_features=num_classes) # 网络的前向计算过程 def forward(self, x): x = self.conv1(x) # 每个卷积层使用Sigmoid激活函数，后面跟着一个2x2的池化 x = F.sigmoid(x) x = self.max_pool1(x) x = F.sigmoid(x) x = self.conv2(x) x = self.max_pool2(x) x = self.conv3(x) # 尺寸的逻辑：输入层将数据拉平[B,C,H,W] -> [B,C*H*W] x = paddle.reshape(x, [x.shape[0], -1]) x = self.fc1(x) x = F.sigmoid(x) x = self.fc2(x) return x1.4. 模型特点卷积网络使用一个3层的序列组合：卷积、下采样（池化）、非线性映射（LeNet-5最重要的特性，奠定了目前深层卷积网络的基础）；使用卷积提取空间特征；使用映射的空间均值进行下采样；使用$tanh$或$sigmoid$进行非线性映射；多层神经网络（MLP）作为最终的分类器；层间的稀疏连接矩阵以避免巨大的计算开销。1.5. 模型指标LeNet-5在MNIST手写数字识别任务上进行了模型训练与测试，论文中提供的模型指标如 图2 所示。使用 distortions 方法处理后，error rate能够达到0.8%。图2 LeNet模型指标1.6. 参考文献[1] - Gradient-based learn- ing applied to document recognition.2. AlexNet2.1. 模型介绍AlexNet[1]是2012年ImageNet竞赛的冠军模型，其作者是神经网络领域三巨头之一的Hinton和他的学生Alex Krizhevsky。AlexNet以极大的优势领先2012年ImageNet竞赛的第二名，也因此给当时的学术界和工业界带来了很大的冲击。此后，更多更深的神经网络相继被提出，比如优秀的VGG，GoogLeNet，ResNet等。2.2. 模型结构AlexNet与此前的LeNet相比，具有更深的网络结构，包含5层卷积和3层全连接，具体结构如 图1 所示。图1 AlexNet模型网络结构示意图[1] - 第一模块：对于$224\times 224$的彩色图像，先用96个$11\times 11\times 3$的卷积核对其进行卷积，提取图像中包含的特征模式（步长为4，填充为2，得到96个$54\times 54$的卷积结果（特征图）；然后以$2\times 2$大小进行池化，得到了96个$27\times 27$大小的特征图；[2] - 第二模块：包含256个$5\times 5$的卷积和$2\times 2$池化，卷积操作后图像尺寸不变，经过池化后，图像尺寸变成$13\times 13$；[3] - 第三模块：包含384个$3\times 3$的卷积，卷积操作后图像尺寸不变；[4] - 第四模块：包含384个$3\times 3$的卷积，卷积操作后图像尺寸不变；[5] - 第五模块：包含256个$3\times 3$的卷积和$2\times 2$的池化，卷积操作后图像尺寸不变，经过池化后变成256个$6\times 6$大小的特征图。将经过第5次卷积提取到的特征图输入到全连接层，得到原始图像的向量表达。前两个全连接层的输出神经元的个数是4096，第三个全连接层的输出神经元个数是分类标签的类别数（ImageNet比赛的分类类别数是1000），然后使用Softmax激活函数即可计算出每个类别的预测概率。2.3. 模型实现基于Paddle框架，AlexNet的具体实现的代码如下所示：# -*- coding:utf-8 -*- import paddle import numpy as np from paddle.nn import Conv2D, MaxPool2D, Linear, Dropout import paddle.nn.functional as F # 定义 AlexNet 网络结构 class AlexNet(paddle.nn.Layer): def __init__(self, num_classes=1): super(AlexNet, self).__init__() # AlexNet与LeNet一样也会同时使用卷积和池化层提取图像特征 # 与LeNet不同的是激活函数换成了‘relu’ self.conv1 = Conv2D(in_channels=3, out_channels=96, kernel_size=11, stride=4, padding=5) self.max_pool1 = MaxPool2D(kernel_size=2, stride=2) self.conv2 = Conv2D(in_channels=96, out_channels=256, kernel_size=5, stride=1, padding=2) self.max_pool2 = MaxPool2D(kernel_size=2, stride=2) self.conv3 = Conv2D(in_channels=256, out_channels=384, kernel_size=3, stride=1, padding=1) self.conv4 = Conv2D(in_channels=384, out_channels=384, kernel_size=3, stride=1, padding=1) self.conv5 = Conv2D(in_channels=384, out_channels=256, kernel_size=3, stride=1, padding=1) self.max_pool5 = MaxPool2D(kernel_size=2, stride=2) self.fc1 = Linear(in_features=12544, out_features=4096) self.drop_ratio1 = 0.5 self.drop1 = Dropout(self.drop_ratio1) self.fc2 = Linear(in_features=4096, out_features=4096) self.drop_ratio2 = 0.5 self.drop2 = Dropout(self.drop_ratio2) self.fc3 = Linear(in_features=4096, out_features=num_classes) def forward(self, x): x = self.conv1(x) x = F.relu(x) x = self.max_pool1(x) x = self.conv2(x) x = F.relu(x) x = self.max_pool2(x) x = self.conv3(x) x = F.relu(x) x = self.conv4(x) x = F.relu(x) x = self.conv5(x) x = F.relu(x) x = self.max_pool5(x) x = paddle.reshape(x, [x.shape[0], -1]) x = self.fc1(x) x = F.relu(x) # 在全连接之后使用dropout抑制过拟合 x = self.drop1(x) x = self.fc2(x) x = F.relu(x) # 在全连接之后使用dropout抑制过拟合 x = self.drop2(x) x = self.fc3(x) return x2.4. 模型特点AlexNet中包含了几个比较新的技术点，也首次在CNN中成功应用了ReLU、Dropout和LRN等Trick。同时AlexNet也使用了GPU进行运算加速。AlexNet将LeNet的思想发扬光大，把CNN的基本原理应用到了很深很宽的网络中。AlexNet主要使用到的新技术点如下：成功使用ReLU作为CNN的激活函数，并验证其效果在较深的网络超过了Sigmoid，成功解决了Sigmoid在网络较深时的梯度弥散问题。虽然ReLU激活函数在很久之前就被提出了，但是直到AlexNet的出现才将其发扬光大。训练时使用Dropout随机忽略一部分神经元，以避免模型过拟合。Dropout虽有单独的论文论述，但是AlexNet将其实用化，通过实践证实了它的效果。在AlexNet中主要是最后几个全连接层使用了Dropout。在CNN中使用重叠的最大池化。此前CNN中普遍使用平均池化，AlexNet全部使用最大池化，避免平均池化的模糊化效果。并且AlexNet中提出让步长比池化核的尺寸小的观点，这样池化层的输出之间会有重叠和覆盖，提升了特征的丰富性。提出了LRN局部响应归一化层，对局部神经元的活动创建竞争机制，使得其中响应比较大的值变得相对更大，并抑制其他反馈较小的神经元，增强了模型的泛化能力。使用CUDA加速深度卷积网络的训练，利用GPU强大的并行计算能力，处理神经网络训练时大量的矩阵运算。AlexNet使用了两块GTX 580 GPU进行训练，单个GTX 580只有3GB显存，这限制了可训练的网络的最大规模。因此作者将AlexNet分布在两个GPU上，在每个GPU的显存中储存一半的神经元的参数。因为GPU之间通信方便，可以互相访问显存，而不需要通过主机内存，所以同时使用多块GPU也是非常高效的。同时，AlexNet的设计让GPU之间的通信只在网络的某些层进行，控制了通信的性能损耗。 使用数据增强，随机地从$256\times 256$ 大小的原始图像中截取$224\times 224$大小的区域（以及水平翻转的镜像），相当于增加了$2\times (256-224)^2=2048$倍的数据量。如果没有数据增强，仅靠原始的数据量，参数众多的CNN会陷入过拟合中，使用了数据增强后可以大大减轻过拟合，提升泛化能力。进行预测时，则是取图片的四个角加中间共5个位置，并进行左右翻转，一共获得10张图片，对他们进行预测并对10次结果求均值。同时，AlexNet论文中提到了会对图像的RGB数据进行PCA处理，并对主成分做一个标准差为0.1的高斯扰动，增加一些噪声，这个Trick可以让错误率再下降1%。2.5. 模型指标AlexNet 作为 ImageNet 2012比赛的冠军算法，在 ImageNet 测试集上达到了 15.3% 的 top-5 error rate，远远超过第二名（SIFT+FVs）的 26.2% 。如 图2 所示。图2 AlexNet模型指标2.6. 参考文献[1] - Imagenet classification with deep convolutional neural networks. 3. VGG3.1. 模型介绍随着AlexNet在2012年的ImageNet大赛上大放异彩后，卷积神经网络进入了飞速发展的阶段。2014年，由Simonyan和Zisserman提出的VGG[1]网络在ImageNet上取得了亚军的成绩。VGG的命名来源于论文作者所在的实验室Visual Geometry Group，其对卷积神经网络进行了改良，探索了网络深度与性能的关系，用更小的卷积核和更深的网络结构，取得了较好的效果，成为了CNN发展史上较为重要的一个网络。VGG中使用了一系列大小为3x3的小尺寸卷积核和池化层构造深度卷积神经网络，因为其结构简单、应用性极强而广受研究者欢迎，尤其是它的网络结构设计方法，为构建深度神经网络提供了方向。3.2. 模型结构图1 是VGG-16的网络结构示意图，有13层卷积和3层全连接层。VGG网络的设计严格使用$3\times 3$的卷积层和池化层来提取特征，并在网络的最后面使用三层全连接层，将最后一层全连接层的输出作为分类的预测。VGG中还有一个显著特点：每次经过池化层（maxpooling）后特征图的尺寸减小一倍，而通道数增加一倍（最后一个池化层除外）。在VGG中每层卷积将使用ReLU作为激活函数，在全连接层之后添加dropout来抑制过拟合。使用小的卷积核能够有效地减少参数的个数，使得训练和测试变得更加有效。比如使用两层$3\times 3$ 卷积层，可以得到感受野为5的特征图，而比使用$5 \times 5$的卷积层需要更少的参数。由于卷积核比较小，可以堆叠更多的卷积层，加深网络的深度，这对于图像分类任务来说是有利的。VGG模型的成功证明了增加网络的深度，可以更好的学习图像中的特征模式。图1 VGG模型网络结构示意图3.3. 模型实现基于Paddle框架，VGG的具体实现如下代码所示：# -*- coding:utf-8 -*- import numpy as np import paddle from paddle.nn import Conv2D, MaxPool2D, BatchNorm2D, Linear # 定义vgg网络 class VGG(paddle.nn.Layer): def __init__(self): super(VGG, self).__init__() in_channels = [3, 64, 128, 256, 512, 512] # 定义第一个卷积块，包含两个卷积 self.conv1_1 = Conv2D(in_channels=in_channels[0], out_channels=in_channels[1], kernel_size=3, padding=1, stride=1) self.conv1_2 = Conv2D(in_channels=in_channels[1], out_channels=in_channels[1], kernel_size=3, padding=1, stride=1) # 定义第二个卷积块，包含两个卷积 self.conv2_1 = Conv2D(in_channels=in_channels[1], out_channels=in_channels[2], kernel_size=3, padding=1, stride=1) self.conv2_2 = Conv2D(in_channels=in_channels[2], out_channels=in_channels[2], kernel_size=3, padding=1, stride=1) # 定义第三个卷积块，包含三个卷积 self.conv3_1 = Conv2D(in_channels=in_channels[2], out_channels=in_channels[3], kernel_size=3, padding=1, stride=1) self.conv3_2 = Conv2D(in_channels=in_channels[3], out_channels=in_channels[3], kernel_size=3, padding=1, stride=1) self.conv3_3 = Conv2D(in_channels=in_channels[3], out_channels=in_channels[3], kernel_size=3, padding=1, stride=1) # 定义第四个卷积块，包含三个卷积 self.conv4_1 = Conv2D(in_channels=in_channels[3], out_channels=in_channels[4], kernel_size=3, padding=1, stride=1) self.conv4_2 = Conv2D(in_channels=in_channels[4], out_channels=in_channels[4], kernel_size=3, padding=1, stride=1) self.conv4_3 = Conv2D(in_channels=in_channels[4], out_channels=in_channels[4], kernel_size=3, padding=1, stride=1) # 定义第五个卷积块，包含三个卷积 self.conv5_1 = Conv2D(in_channels=in_channels[4], out_channels=in_channels[5], kernel_size=3, padding=1, stride=1) self.conv5_2 = Conv2D(in_channels=in_channels[5], out_channels=in_channels[5], kernel_size=3, padding=1, stride=1) self.conv5_3 = Conv2D(in_channels=in_channels[5], out_channels=in_channels[5], kernel_size=3, padding=1, stride=1) # 使用Sequential 将全连接层和relu组成一个线性结构（fc + relu） # 当输入为224x224时，经过五个卷积块和池化层后，特征维度变为[512x7x7] self.fc1 = paddle.nn.Sequential(paddle.nn.Linear(512 * 7 * 7, 4096), paddle.nn.ReLU()) self.drop1_ratio = 0.5 self.dropout1 = paddle.nn.Dropout(self.drop1_ratio, mode='upscale_in_train') # 使用Sequential 将全连接层和relu组成一个线性结构（fc + relu） self.fc2 = paddle.nn.Sequential(paddle.nn.Linear(4096, 4096), paddle.nn.ReLU()) self.drop2_ratio = 0.5 self.dropout2 = paddle.nn.Dropout(self.drop2_ratio, mode='upscale_in_train') self.fc3 = paddle.nn.Linear(4096, 1) self.relu = paddle.nn.ReLU() self.pool = MaxPool2D(stride=2, kernel_size=2) def forward(self, x): x = self.relu(self.conv1_1(x)) x = self.relu(self.conv1_2(x)) x = self.pool(x) x = self.relu(self.conv2_1(x)) x = self.relu(self.conv2_2(x)) x = self.pool(x) x = self.relu(self.conv3_1(x)) x = self.relu(self.conv3_2(x)) x = self.relu(self.conv3_3(x)) x = self.pool(x) x = self.relu(self.conv4_1(x)) x = self.relu(self.conv4_2(x)) x = self.relu(self.conv4_3(x)) x = self.pool(x) x = self.relu(self.conv5_1(x)) x = self.relu(self.conv5_2(x)) x = self.relu(self.conv5_3(x)) x = self.pool(x) x = paddle.flatten(x, 1, -1) x = self.dropout1(self.relu(self.fc1(x))) x = self.dropout2(self.relu(self.fc2(x))) x = self.fc3(x) return x3.4. 模型特点整个网络都使用了同样大小的卷积核尺寸$3\times3$和最大池化尺寸$2\times2$。$1\times1$卷积的意义主要在于线性变换，而输入通道数和输出通道数不变，没有发生降维。两个$3\times3$的卷积层串联相当于1个$5\times5$的卷积层，感受野大小为$5\times5$。同样地，3个$3\times3$的卷积层串联的效果则相当于1个$7\times7$的卷积层。这样的连接方式使得网络参数量更小，而且多层的激活函数令网络对特征的学习能力更强。VGGNet在训练时有一个小技巧，先训练浅层的的简单网络VGG11，再复用VGG11的权重来初始化VGG13，如此反复训练并初始化VGG19，能够使训练时收敛的速度更快。在训练过程中使用多尺度的变换对原始数据做数据增强，使得模型不易过拟合。3.5. 模型指标VGG 在 2014 年的 ImageNet 比赛上取得了亚军的好成绩，具体指标如 图2 所示。图2 第一行为在 ImageNet 比赛中的指标，测试集的Error rate达到了7.3%，在论文中，作者对算法又进行了一定的优化，最终可以达到 6.8% 的Error rate。图2 VGG模型指标3.6. 参考文献[1] - Very deep convolutional networks for large-scale image recognition.4. GoogLeNet4.1. 模型介绍GoogLeNet[1]是2014年ImageNet比赛的冠军，它的主要特点是网络不仅有深度，还在横向上具有“宽度”。从名字GoogLeNet可以知道这是来自谷歌工程师所设计的网络结构，而名字中GoogLeNet更是致敬了LeNet。GoogLeNet中最核心的部分是其内部子网络结构Inception，该结构灵感来源于NIN(Network In Network)。4.2. 模型结构由于图像信息在空间尺寸上的巨大差异，如何选择合适的卷积核来提取特征就显得比较困难了。空间分布范围更广的图像信息适合用较大的卷积核来提取其特征；而空间分布范围较小的图像信息则适合用较小的卷积核来提取其特征。为了解决这个问题，GoogLeNet提出了一种被称为Inception模块的方案。如 图1 所示：说明：Google的研究人员为了向LeNet致敬，特地将模型命名为GoogLeNet。Inception一词来源于电影《盗梦空间》（Inception）。图1 Inception模块结构示意图图1(a)是Inception模块的设计思想，使用3个不同大小的卷积核对输入图片进行卷积操作，并附加最大池化，将这4个操作的输出沿着通道这一维度进行拼接，构成的输出特征图将会包含经过不同大小的卷积核提取出来的特征，从而达到捕捉不同尺度信息的效果。Inception模块采用多通路(multi-path)的设计形式，每个支路使用不同大小的卷积核，最终输出特征图的通道数是每个支路输出通道数的总和，这将会导致输出通道数变得很大，尤其是使用多个Inception模块串联操作的时候，模型参数量会变得非常大。为了减小参数量，Inception模块使用了图(b)中的设计方式，在每个3x3和5x5的卷积层之前，增加1x1的卷积层来控制输出通道数；在最大池化层后面增加1x1卷积层减小输出通道数。基于这一设计思想，形成了上图(b)中所示的结构。下面这段程序是Inception块的具体实现方式，可以对照图(b)和代码一起阅读。提示：可能会问，经过3x3的最大池化之后图像尺寸不会减小吗，为什么还能跟另外3个卷积输出的特征图进行拼接？这是因为池化操作可以指定窗口大小$k_h = k_w = 3$，stride=1和padding=1，输出特征图尺寸可以保持不变。Inception模块的具体实现如下代码所示：#!--*-- coding: utf-8 --*-- import numpy as np import paddle from paddle.nn import Conv2D, MaxPool2D, AdaptiveAvgPool2D, Linear import paddle.nn.functional as F # 定义Inception块 class Inception(paddle.nn.Layer): def __init__(self, c0, c1, c2, c3, c4, **kwargs): ''' Inception模块的实现代码， c1,图(b)中第一条支路1x1卷积的输出通道数，数据类型是整数 c2,图(b)中第二条支路卷积的输出通道数，数据类型是tuple或list, 其中c2[0]是1x1卷积的输出通道数，c2[1]是3x3 c3,图(b)中第三条支路卷积的输出通道数，数据类型是tuple或list, 其中c3[0]是1x1卷积的输出通道数，c3[1]是3x3 c4,图(b)中第一条支路1x1卷积的输出通道数，数据类型是整数 ''' super(Inception, self).__init__() # 依次创建Inception块每条支路上使用到的操作 self.p1_1 = Conv2D(in_channels=c0,out_channels=c1, kernel_size=1) self.p2_1 = Conv2D(in_channels=c0,out_channels=c2[0], kernel_size=1) self.p2_2 = Conv2D(in_channels=c2[0],out_channels=c2[1], kernel_size=3, padding=1) self.p3_1 = Conv2D(in_channels=c0,out_channels=c3[0], kernel_size=1) self.p3_2 = Conv2D(in_channels=c3[0],out_channels=c3[1], kernel_size=5, padding=2) self.p4_1 = MaxPool2D(kernel_size=3, stride=1, padding=1) self.p4_2 = Conv2D(in_channels=c0,out_channels=c4, kernel_size=1) def forward(self, x): # 支路1只包含一个1x1卷积 p1 = F.relu(self.p1_1(x)) # 支路2包含 1x1卷积 + 3x3卷积 p2 = F.relu(self.p2_2(F.relu(self.p2_1(x)))) # 支路3包含 1x1卷积 + 5x5卷积 p3 = F.relu(self.p3_2(F.relu(self.p3_1(x)))) # 支路4包含 最大池化和1x1卷积 p4 = F.relu(self.p4_2(self.p4_1(x))) # 将每个支路的输出特征图拼接在一起作为最终的输出结果 return paddle.concat([p1, p2, p3, p4], axis=1)GoogLeNet的架构如 图2 所示，在主体卷积部分中使用5个模块（block），每个模块之间使用步幅为2的3 ×3最大池化层来减小输出高宽。第一模块使用一个64通道的7 × 7卷积层。第二模块使用2个卷积层:首先是64通道的1 × 1卷积层，然后是将通道增大3倍的3 × 3卷积层。第三模块串联2个完整的Inception块。第四模块串联了5个Inception块。第五模块串联了2 个Inception块。第五模块的后面紧跟输出层，使用全局平均池化层来将每个通道的高和宽变成1，最后接上一个输出个数为标签类别数的全连接层。说明：在原作者的论文中添加了图中所示的softmax1和softmax2两个辅助分类器，如下图所示，训练时将三个分类器的损失函数进行加权求和，以缓解梯度消失现象。图2 GoogLeNet模型网络结构示意图4.3.模型实现GoogLeNet的具体实现如下代码所示：#!--*-- coding: utf-8 --*-- import paddle from paddle import ParamAttr import paddle.nn as nn import paddle.nn.functional as F from paddle.nn import Conv2D, BatchNorm, Linear, Dropout from paddle.nn import AdaptiveAvgPool2D, MaxPool2D, AvgPool2D from paddle.nn.initializer import Uniform import math # 全连接层参数初始化 def xavier(channels, filter_size, name): stdv = (3.0 / (filter_size**2 * channels))**0.5 param_attr = ParamAttr(initializer=Uniform(-stdv, stdv), name=name + "_weights") return param_attr class ConvLayer(nn.Layer): def __init__(self, num_channels, num_filters, filter_size, stride=1, groups=1, act=None, name=None): super(ConvLayer, self).__init__() self._conv = Conv2D( in_channels=num_channels, out_channels=num_filters, kernel_size=filter_size, stride=stride, padding=(filter_size - 1) // 2, groups=groups, weight_attr=ParamAttr(name=name + "_weights"), bias_attr=False) def forward(self, inputs): y = self._conv(inputs) return y # 定义Inception块 class Inception(nn.Layer): def __init__(self, input_channels, output_channels, filter1, filter3R, filter3, filter5R, filter5, proj, name=None): ''' Inception模块的实现代码， c1,图(b)中第一条支路1x1卷积的输出通道数，数据类型是整数 c2,图(b)中第二条支路卷积的输出通道数，数据类型是tuple或list, 其中c2[0]是1x1卷积的输出通道数，c2[1]是3x3 c3,图(b)中第三条支路卷积的输出通道数，数据类型是tuple或list, 其中c3[0]是1x1卷积的输出通道数，c3[1]是3x3 c4,图(b)中第一条支路1x1卷积的输出通道数，数据类型是整数 ''' super(Inception, self).__init__() # 依次创建Inception块每条支路上使用到的操作 self._conv1 = ConvLayer(input_channels, filter1, 1, name="inception_" + name + "_1x1") self._conv3r = ConvLayer(input_channels, filter3R, 1, name="inception_" + name + "_3x3_reduce") self._conv3 = ConvLayer(filter3R, filter3, 3, name="inception_" + name + "_3x3") self._conv5r = ConvLayer(input_channels, filter5R, 1, name="inception_" + name + "_5x5_reduce") self._conv5 = ConvLayer(filter5R, filter5, 5, name="inception_" + name + "_5x5") self._pool = MaxPool2D(kernel_size=3, stride=1, padding=1) self._convprj = ConvLayer(input_channels, proj, 1, name="inception_" + name + "_3x3_proj") def forward(self, inputs): # 支路1只包含一个1x1卷积 conv1 = self._conv1(inputs) # 支路2包含 1x1卷积 + 3x3卷积 conv3r = self._conv3r(inputs) conv3 = self._conv3(conv3r) # 支路3包含 1x1卷积 + 5x5卷积 conv5r = self._conv5r(inputs) conv5 = self._conv5(conv5r) # 支路4包含 最大池化和1x1卷积 pool = self._pool(inputs) convprj = self._convprj(pool) # 将每个支路的输出特征图拼接在一起作为最终的输出结果 cat = paddle.concat([conv1, conv3, conv5, convprj], axis=1) cat = F.relu(cat) return cat class GoogLeNet(nn.Layer): def __init__(self, class_dim=1000): super(GoogLeNetDY, self).__init__() # GoogLeNet包含五个模块，每个模块后面紧跟一个池化层 # 第一个模块包含1个卷积层 self._conv = ConvLayer(3, 64, 7, 2, name="conv1") # 3x3最大池化 self._pool = MaxPool2D(kernel_size=3, stride=2) # 第二个模块包含2个卷积层 self._conv_1 = ConvLayer(64, 64, 1, name="conv2_1x1") self._conv_2 = ConvLayer(64, 192, 3, name="conv2_3x3") # 第三个模块包含2个Inception块 self._ince3a = Inception(192, 192, 64, 96, 128, 16, 32, 32, name="ince3a") self._ince3b = Inception(256, 256, 128, 128, 192, 32, 96, 64, name="ince3b") # 第四个模块包含5个Inception块 self._ince4a = Inception(480, 480, 192, 96, 208, 16, 48, 64, name="ince4a") self._ince4b = Inception(512, 512, 160, 112, 224, 24, 64, 64, name="ince4b") self._ince4c = Inception(512, 512, 128, 128, 256, 24, 64, 64, name="ince4c") self._ince4d = Inception(512, 512, 112, 144, 288, 32, 64, 64, name="ince4d") self._ince4e = Inception(528, 528, 256, 160, 320, 32, 128, 128, name="ince4e") # 第五个模块包含2个Inception块 self._ince5a = Inception(832, 832, 256, 160, 320, 32, 128, 128, name="ince5a") self._ince5b = Inception(832, 832, 384, 192, 384, 48, 128, 128, name="ince5b") # 全局池化 self._pool_5 = AvgPool2D(kernel_size=7, stride=7) self._drop = Dropout(p=0.4, mode="downscale_in_infer") self._fc_out = Linear( 1024, class_dim, weight_attr=xavier(1024, 1, "out"), bias_attr=ParamAttr(name="out_offset")) self._pool_o1 = AvgPool2D(kernel_size=5, stride=3) self._conv_o1 = ConvLayer(512, 128, 1, name="conv_o1") self._fc_o1 = Linear( 1152, 1024, weight_attr=xavier(2048, 1, "fc_o1"), bias_attr=ParamAttr(name="fc_o1_offset")) self._drop_o1 = Dropout(p=0.7, mode="downscale_in_infer") self._out1 = Linear( 1024, class_dim, weight_attr=xavier(1024, 1, "out1"), bias_attr=ParamAttr(name="out1_offset")) self._pool_o2 = AvgPool2D(kernel_size=5, stride=3) self._conv_o2 = ConvLayer(528, 128, 1, name="conv_o2") self._fc_o2 = Linear( 1152, 1024, weight_attr=xavier(2048, 1, "fc_o2"), bias_attr=ParamAttr(name="fc_o2_offset")) self._drop_o2 = Dropout(p=0.7, mode="downscale_in_infer") self._out2 = Linear( 1024, class_dim, weight_attr=xavier(1024, 1, "out2"), bias_attr=ParamAttr(name="out2_offset")) def forward(self, inputs): x = self._conv(inputs) x = self._pool(x) x = self._conv_1(x) x = self._conv_2(x) x = self._pool(x) x = self._ince3a(x) x = self._ince3b(x) x = self._pool(x) ince4a = self._ince4a(x) x = self._ince4b(ince4a) x = self._ince4c(x) ince4d = self._ince4d(x) x = self._ince4e(ince4d) x = self._pool(x) x = self._ince5a(x) ince5b = self._ince5b(x) x = self._pool_5(ince5b) x = self._drop(x) x = paddle.squeeze(x, axis=[2, 3]) out = self._fc_out(x) x = self._pool_o1(ince4a) x = self._conv_o1(x) x = paddle.flatten(x, start_axis=1, stop_axis=-1) x = self._fc_o1(x) x = F.relu(x) x = self._drop_o1(x) out1 = self._out1(x) x = self._pool_o2(ince4d) x = self._conv_o2(x) x = paddle.flatten(x, start_axis=1, stop_axis=-1) x = self._fc_o2(x) x = self._drop_o2(x) out2 = self._out2(x) return [out, out1, out2]4.4. 模型特色采用不同大小的卷积核意味着不同大小的感受野，最后通过拼接实现不同尺度特征的融合；之所以卷积核大小采用1、3和5，主要是为了方便对齐。设定卷积步长stride=1之后，只要分别设定pad=0、1、2，那么卷积之后便可以得到相同维度的特征，然后这些特征就可以直接拼接在一起了；网络越到后面，特征越抽象，而且每个特征所涉及的感受野也更大了，因此随着层数的增加，3x3和5x5卷积的比例也要增加。但是，使用5x5的卷积核仍然会带来巨大的计算量。 为此，文章采用1x1卷积核来进行降维。4.5. 模型指标GoogLeNet 在 2014 年的 ImageNet 比赛上取得了冠军的好成绩，具体指标如 图3 所示。在测试集上Error rate 达到了6.67%。图3 GoogLeNet模型指标4.6. 参考文献[1] - Going deeper with convolutions.5. DarkNet5.1. 模型介绍在目标检测领域的YOLO系列算法中，作者为了达到更好的分类效果，自己设置并训练了DarkNet网络作为骨干网络。其中，YOLOv2[1]首次提出DarkNet网络，由于其具有19个卷积层，所以也称之为DarkNet19。后来在YOLOv3[2]中，作者继续吸收了当前优秀算法的思想，如残差网络和特征融合等，提出了具有53个卷积层的骨干网络DarkNet53。作者在ImageNet上进行了实验，发现相较于ResNet-152和ResNet-101，DarkNet53在分类精度差不多的前提下，计算速度取得了领先。5.2. 模型结构5.2.1. DarkNet19DarkNet19中，借鉴了许多优秀算法的经验，比如：借鉴了VGG的思想，使用了较多的$3\times 3$卷积，在每一次池化操作后，将通道数翻倍；借鉴了network in network的思想，使用全局平均池化（global average pooling）做预测，并把$1\times 1$的卷积核置于$3\times 3$的卷积核之间，用来压缩特征；同时，使用了批归一化层稳定模型训练，加速收敛，并且起到正则化作用。DarkNet19的网络结构如 图1 所示。图1 DarkNet19模型网络结构示意图DarkNet19精度与VGG网络相当，但浮点运算量只有其 $\frac{1}{5}$ 左右，因此运算速度极快。5.2.2. DarkNet53DarkNet53在之前的基础上，借鉴了ResNet的思想，在网络中大量使用了残差连接，因此网络结构可以设计的很深，并且缓解了训练中梯度消失的问题，使得模型更容易收敛。同时，使用步长为2的卷积层代替池化层实现降采样。DarkNet53的网络结构如 图2 所示。图2 DarkNet53模型网络结构示意图考虑到当前 Darknet19 网络使用频率较低，接下来主要针对Darknet53网络进行实现与讲解。5.3. 模型实现基于Paddle框架，DarkNet53的具体实现的代码如下所示：#!--*-- coding: utf-8 --*-- import paddle from paddle import ParamAttr import paddle.nn as nn import paddle.nn.functional as F from paddle.nn import Conv2D, BatchNorm, Linear, Dropout from paddle.nn import AdaptiveAvgPool2D, MaxPool2D, AvgPool2D from paddle.nn.initializer import Uniform import math # 将卷积和批归一化封装为ConvBNLayer，方便后续复用 class ConvBNLayer(nn.Layer): def __init__(self, input_channels, output_channels, filter_size, stride, padding, name=None): # 初始化函数 super(ConvBNLayer, self).__init__() # 创建卷积层 self._conv = Conv2D( in_channels=input_channels, out_channels=output_channels, kernel_size=filter_size, stride=stride, padding=padding, weight_attr=ParamAttr(name=name + ".conv.weights"), bias_attr=False) # 创建批归一化层 bn_name = name + ".bn" self._bn = BatchNorm( num_channels=output_channels, act="relu", param_attr=ParamAttr(name=bn_name + ".scale"), bias_attr=ParamAttr(name=bn_name + ".offset"), moving_mean_name=bn_name + ".mean", moving_variance_name=bn_name + ".var") def forward(self, inputs): # 前向计算 x = self._conv(inputs) x = self._bn(x) return x # 定义残差块 class BasicBlock(nn.Layer): def __init__(self, input_channels, output_channels, name=None): # 初始化函数 super(BasicBlock, self).__init__() # 定义两个卷积层 self._conv1 = ConvBNLayer( input_channels, output_channels, 1, 1, 0, name=name + ".0") self._conv2 = ConvBNLayer( output_channels, output_channels * 2, 3, 1, 1, name=name + ".1") def forward(self, inputs): # 前向计算 x = self._conv1(inputs) x = self._conv2(x) # 将第二个卷积层的输出和最初的输入值相加 return paddle.add(x=inputs, y=x) class DarkNet53(nn.Layer): def __init__(self, class_dim=1000): # 初始化函数 super(DarkNet, self).__init__() # DarkNet 每组残差块的个数，来自DarkNet的网络结构图 self.stages = [1, 2, 8, 8, 4] # 第一层卷积 self._conv1 = ConvBNLayer(3, 32, 3, 1, 1, name="yolo_input") # 下采样，使用stride=2的卷积来实现 self._conv2 = ConvBNLayer( 32, 64, 3, 2, 1, name="yolo_input.downsample") # 添加各个层级的实现 self._basic_block_01 = BasicBlock(64, 32, name="stage.0.0") # 下采样，使用stride=2的卷积来实现 self._downsample_0 = ConvBNLayer( 64, 128, 3, 2, 1, name="stage.0.downsample") self._basic_block_11 = BasicBlock(128, 64, name="stage.1.0") self._basic_block_12 = BasicBlock(128, 64, name="stage.1.1") # 下采样，使用stride=2的卷积来实现 self._downsample_1 = ConvBNLayer( 128, 256, 3, 2, 1, name="stage.1.downsample") self._basic_block_21 = BasicBlock(256, 128, name="stage.2.0") self._basic_block_22 = BasicBlock(256, 128, name="stage.2.1") self._basic_block_23 = BasicBlock(256, 128, name="stage.2.2") self._basic_block_24 = BasicBlock(256, 128, name="stage.2.3") self._basic_block_25 = BasicBlock(256, 128, name="stage.2.4") self._basic_block_26 = BasicBlock(256, 128, name="stage.2.5") self._basic_block_27 = BasicBlock(256, 128, name="stage.2.6") self._basic_block_28 = BasicBlock(256, 128, name="stage.2.7") # 下采样，使用stride=2的卷积来实现 self._downsample_2 = ConvBNLayer( 256, 512, 3, 2, 1, name="stage.2.downsample") self._basic_block_31 = BasicBlock(512, 256, name="stage.3.0") self._basic_block_32 = BasicBlock(512, 256, name="stage.3.1") self._basic_block_33 = BasicBlock(512, 256, name="stage.3.2") self._basic_block_34 = BasicBlock(512, 256, name="stage.3.3") self._basic_block_35 = BasicBlock(512, 256, name="stage.3.4") self._basic_block_36 = BasicBlock(512, 256, name="stage.3.5") self._basic_block_37 = BasicBlock(512, 256, name="stage.3.6") self._basic_block_38 = BasicBlock(512, 256, name="stage.3.7") # 下采样，使用stride=2的卷积来实现 self._downsample_3 = ConvBNLayer( 512, 1024, 3, 2, 1, name="stage.3.downsample") self._basic_block_41 = BasicBlock(1024, 512, name="stage.4.0") self._basic_block_42 = BasicBlock(1024, 512, name="stage.4.1") self._basic_block_43 = BasicBlock(1024, 512, name="stage.4.2") self._basic_block_44 = BasicBlock(1024, 512, name="stage.4.3") # 自适应平均池化 self._pool = AdaptiveAvgPool2D(1) stdv = 1.0 / math.sqrt(1024.0) # 分类层 self._out = Linear( 1024, class_dim, weight_attr=ParamAttr( name="fc_weights", initializer=Uniform(-stdv, stdv)), bias_attr=ParamAttr(name="fc_offset")) def forward(self, inputs): x = self._conv1(inputs) x = self._conv2(x) x = self._basic_block_01(x) x = self._downsample_0(x) x = self._basic_block_11(x) x = self._basic_block_12(x) x = self._downsample_1(x) x = self._basic_block_21(x) x = self._basic_block_22(x) x = self._basic_block_23(x) x = self._basic_block_24(x) x = self._basic_block_25(x) x = self._basic_block_26(x) x = self._basic_block_27(x) x = self._basic_block_28(x) x = self._downsample_2(x) x = self._basic_block_31(x) x = self._basic_block_32(x) x = self._basic_block_33(x) x = self._basic_block_34(x) x = self._basic_block_35(x) x = self._basic_block_36(x) x = self._basic_block_37(x) x = self._basic_block_38(x) x = self._downsample_3(x) x = self._basic_block_41(x) x = self._basic_block_42(x) x = self._basic_block_43(x) x = self._basic_block_44(x) x = self._pool(x) x = paddle.squeeze(x, axis=[2, 3]) x = self._out(x) return x5.4. 模型特点DarkNet53模型使用了大量的残差连接，缓解了训练中梯度消失的问题，使得模型更容易收敛。DarkNet53模型使用步长为2的卷积层代替池化层实现降采样。5.5. 模型指标在 YOLOv3 论文中，作者在 ImageNet 数据集上对比了 DarkNet 网络与ResNet 网络的精度及速度，如图3所示。可以看到DarkNet53的top-5准确率可以达到93.8%，同时速度也明显超过了ResNet101和ResNet152。图3 DarkNet模型指标5.6. 参考文献[1] - YOLO9000: Better, Faster, Stronger[2] - YOLOv3: An Incremental Improvement6. ResNet相较于VGG的19层和GoogLeNet的22层，ResNet可以提供18、34、50、101、152甚至更多层的网络，同时获得更好的精度。但是为什么要使用更深层次的网络呢？同时，如果只是网络层数的堆叠，那么为什么前人没有获得ResNet一样的成功呢？6.1. 更深层次的网络？从理论上来讲，加深深度学习网络可以提升性能。深度网络以端到端的多层方式集成了低/中/高层特征和分类器，且特征的层次可通过加深网络层次的方式来丰富。举一个例子，当深度学习网络只有一层时，要学习的特征会非常复杂，但如果有多层，就可以分层进行学习，如 图1 所示，网络的第一层学习到了边缘和颜色，第二层学习到了纹理，第三层学习到了局部的形状，而第五层已逐渐学习到全局特征。网络的加深，理论上可以提供更好的表达能力，使每一层可以学习到更细化的特征。图1 特征可视化6.2. 为什么深度网络不仅仅是层数的堆叠？6.2.1. 梯度消失 or 爆炸但网络加深真的只有堆叠层数这么简单么？当然不是！首先，最显著的问题就是梯度消失/梯度爆炸。我们都知道神经网络的参数更新依靠梯度反向传播（Back Propagation），那么为什么会出现梯度的消失和爆炸呢？举一个例子解释。如 图2 所示，假设每层只有一个神经元，且激活函数使用Sigmoid函数，则有：$$ z_{i+1} = w_ia_i+b_i\\ a_{i+1} = \sigma(z_{i+1}) $$其中，$\sigma(\cdot)$ 为sigmoid函数。图2 单个神经元的神经网络根据链式求导和反向传播，我们可以得到：$$ \frac{\partial y}{\partial a_1} = \frac{\partial y}{\partial a_4}\frac{\partial a_4}{\partial z_4}\frac{\partial z_4}{\partial a_3}\frac{\partial a_3}{\partial z_3}\frac{\partial z_3}{\partial a_2}\frac{\partial a_2}{\partial z_2}\frac{\partial z_2}{\partial a_1} \\ = \frac{\partial y}{\partial a_4}\sigma^{'}(z_4)w_3\sigma^{'}(z_3)w_2\sigma^{'}(z_2)w_1 $$Sigmoid 函数的导数 $\sigma^{'}(x)$ 如 图3 所示：图3 sigmoid函数的导数我们可以看到sigmoid的导数最大值为0.25，那么随着网络层数的增加，小于1的小数不断相乘导致 $\frac{\partial y}{\partial a_1}$ 逐渐趋近于零，从而产生梯度消失。那么梯度爆炸又是怎么引起的呢？同样的道理，当权重初始化为一个较大值时，虽然和激活函数的导数相乘会减小这个值，但是随着神经网络的加深，梯度呈指数级增长，就会引发梯度爆炸。但是从AlexNet开始，神经网络中就使用ReLU函数替换了Sigmoid，同时BN（Batch Normalization)层的加入，也基本解决了梯度消失/爆炸问题。6.2.2. 网络退化现在，梯度消失/爆炸的问题解决了是不是就可以通过堆叠层数来加深网络了呢？Still no！我们来看看ResNet论文中提到的例子（见 图4），很明显，56层的深层网络，在训练集和测试集上的表现都远不如20层的浅层网络，这种随着网络层数加深，accuracy逐渐饱和，然后出现急剧下降，具体表现为深层网络的训练效果反而不如浅层网络好的现象，被称为网络退化（degradation）。图4 CIFAR-10上20层和56层网络的训练误差和测试误差为什么会引起网络退化呢？按照理论上的想法，当浅层网络效果不错的时候，网络层数的增加即使不会引起精度上的提升也不该使模型效果变差。但事实上非线性的激活函数的存在，会造成很多不可逆的信息损失，网络加深到一定程度，过多的信息损失就会造成网络的退化。而ResNet就是提出一种方法让网络拥有恒等映射能力，即随着网络层数的增加，深层网络至少不会差于浅层网络。6.3. 残差块现在我们明白了，为了加深网络结构，使每一次能够学到更细化的特征从而提高网络精度，需要实现的一点是恒等映射。那么残差网络如何能够做到这一点呢？恒等映射即为 $H(x) = x$，已有的神经网络结构很难做到这一点，但是如果我们将网络设计成 $H(x) = F(x) + x$，即 $F(x) = H(x) - x$，那么只需要使残差函数 $F(x) = 0$，就构成了恒等映射 $H(x) = F(x)$。图5 恒等映射残差结构的目的是，随着网络的加深，使 $F(x)$ 逼近于0，使得深度网络的精度在最优浅层网络的基础上不会下降。看到这里你或许会有疑问，既然如此为什么不直接选取最优的浅层网络呢？这是因为最优的浅层网络结构并不易找寻，而ResNet可以通过增加深度，找到最优的浅层网络并保证深层网络不会因为层数的叠加而发生网络退化。6.4. 参考文献[1] - Visualizing and Understanding Convolutional Networks[2] - Deep Residual Learning for Image Recognition
主要参考：[1] - Illustrated Guide to Transformers- Step by Step Explanation-2020.05.01[2] - Github - hyunwoongko/transformerTransformer 结构如图：两层编码器+两层解码器组成的Transformer结构如图：1. Attention 机制Attention 机制使得 Transformer 能够获取非常长的记忆力(extremely long term memory).Transformer 能够参与(attend)或关注(focus)所有先前所生成的 tokens.token(符号): 包括单词和标点tokenization(分词)：词语是什么->['词语', '是', '什么']基于示例，假如想要采用生成式 Transformer 写一部科幻小说，采用 Hugging Face 实现的 Write With Transformer 即可. 给定模型输入，模型会自动生成其余部分.输入：“As Aliens entered our planet”.Transformer输出： “and began to colonized Earth, a certain group of extraterrestrials began to manipulate our society through their influences of a certain number of the elite to keep and iron grip over the populace.”虽然生成的故事有点黑暗，但有意思的是模型如何生成的故事. 当模型逐个单词生成文本时，其可以参与或关注与生成的单词相关的单词. 这种能够知道哪些单词被参与的能力是通过反向传播的训练过程中学会的.图：在逐个生成单词的过程中，Attention 机制关注于不同的 tokens.RNN(Recurrent neural networks) 也具有捕捉先前输入的能力. 但是，Attention 机制的能力在于，其不在受限于短时记忆. RNN 的参考窗口更短，当故事变长时，RNN 将无法访问序列中较早生成的单词. 且，对于GRU(Gated Recurrent Units) 和 LSTM(Long-short Term Memory)，虽然能够具有更大的容量来实现长期记忆，并因此具有更长的参考窗口，但其存在和 RNN 类似的问题.而，Attention 机制，从理论上将，如果具有足够的计算资源，其具有无限的参考窗口，因此能够在生成文本的同时，使用故事的全部上下文信息.图： Attention, RNN’s, GRU’s 和 LSTM 的假设参考窗口2. Attention Is All You Need 逐步说明Attention Is All You NeedTransformers 基于 Attention 的 encoder-decoder 结构，如图：图：Transformer模型概括来说，编码器将输入序列映射为抽象的连续表示形式，这种表示形式能够保留所有已经学习到的输入信息. 然后，解码器采用这种连续表示，并逐步生成单个输出，同时还接收先前的输出.这里逐步说明，并给出参考实现，示例：输入：“Hi how are you”Transformer输出：“I am fine”2.1. Input EmbeddingsTransformer的第一步是，将输入送人 word embdding 层. word embedding 层可以看作是一个查询表(lookup table)，以获取每个单词的学习向量表示形式. 神经网络的学习是通过数字进行的，因此每个单词都被映射到具有连续值的向量来表示该单词.图：将 Words 转化为 Input Embeddings实现如：models/embedding/token_embeddings.pyfrom torch import nn class TokenEmbedding(nn.Embedding): """ Token Embedding using torch.nn they will dense representation of word using weighted matrix """ def __init__(self, vocab_size, d_model): """ class for token embedding that included positional information :param vocab_size: size of vocabulary :param d_model: dimensions of model """ super(TokenEmbedding, self).__init__(vocab_size, d_model, padding_idx=1)2.2. Positional EncodingTransformer 下一步是对输入 embeddings 注入位置信息. 因为 Transformer 编码器没有类似于RNNs 的递归，因此必须将一些关于位置的信息添加到单词的 input embedding 中.这里不深入位置编码的数学细节，但其基本原理是，对于输入向量的每个奇数索引，采用 cos 函数创建一个向量；对于每个偶数索引索引，采用 sin 函数创建一个向量；然后，将这些向量添加到对应的 input embedding 中，即可成功实现在每个向量的位置上给定网络信息. sin 函数和 cos 函数的一并选择，因为他们具有线性属性，模型可以很容易的学习.实现如：models/embedding/positional_encoding.pyimport torch from torch import nn class PostionalEncoding(nn.Module): """ compute sinusoid encoding. """ def __init__(self, d_model, max_len, device): """ constructor of sinusoid encoding class :param d_model: dimension of model :param max_len: max sequence length :param device: hardware device setting """ super(PostionalEncoding, self).__init__() # same size with input matrix (for adding with input matrix) self.encoding = torch.zeros(max_len, d_model, device=device) self.encoding.requires_grad = False # we don't need to compute gradient pos = torch.arange(0, max_len, device=device) pos = pos.float().unsqueeze(dim=1) # 1D => 2D unsqueeze to represent word's position _2i = torch.arange(0, d_model, step=2, device=device).float() # 'i' means index of d_model (e.g. embedding size = 50, 'i' = [0,50]) # "step=2" means 'i' multiplied with two (same with 2 * i) self.encoding[:, 0::2] = torch.sin(pos / (10000 ** (_2i / d_model))) self.encoding[:, 1::2] = torch.cos(pos / (10000 ** (_2i / d_model))) # compute positional encoding to consider positional information of words def forward(self, x): # self.encoding # [max_len = 512, d_model = 512] batch_size, seq_len = x.size() # [batch_size = 128, seq_len = 30] return self.encoding[:seq_len, :] # [seq_len = 30, d_model = 512] # it will add with tok_emb : [128, 30, 512]2.3. TransformerEmbeddingInput Embeddings 和 Postional Encoding 即为 Transformer 的 Embedding 层，实现如：models/embedding/transformer_embedding.pyfrom torch import nn from models.embedding.positional_encoding import PostionalEncoding from models.embedding.token_embeddings import TokenEmbedding class TransformerEmbedding(nn.Module): """ token embedding + positional encoding (sinusoid) positional encoding can give positional information to network """ def __init__(self, vocab_size, d_model, max_len, drop_prob, device): """ class for word embedding that included positional information :param vocab_size: size of vocabulary :param d_model: dimensions of model """ super(TransformerEmbedding, self).__init__() self.tok_emb = TokenEmbedding(vocab_size, d_model) self.pos_emb = PostionalEncoding(d_model, max_len, device) self.drop_out = nn.Dropout(p=drop_prob) def forward(self, x): tok_emb = self.tok_emb(x) pos_emb = self.pos_emb(x) return self.drop_out(tok_emb + pos_emb)2.3. Encoder Layer接着是 Transformer 的编码器层.编码器层的工作是，将所有输入序列映射为抽象的连续表示形式，这种连续表示形式能够保留整个序列的所有学习到的信息. 其包含 2 个子模块，multi-headed attention 以及其后接的 fully connected network. 此外，对于每个子模块还有残差连接，然后接 layer normalization 层. 如图：图：编码器层的子模块其实现如：models/blocks/encoder_layer.pyfrom torch import nn from models.layers.layer_norm import LayerNorm from models.layers.multi_head_attention import MultiHeadAttention from models.layers.position_wise_feed_forward import PositionwiseFeedForward class EncoderLayer(nn.Module): def __init__(self, d_model, ffn_hidden, n_head, drop_prob): super(EncoderLayer, self).__init__() self.attention = MultiHeadAttention(d_model=d_model, n_head=n_head) self.norm1 = LayerNorm(d_model=d_model) self.dropout1 = nn.Dropout(p=drop_prob) self.ffn = PositionwiseFeedForward(d_model=d_model, hidden=ffn_hidden, drop_prob=drop_prob) self.norm2 = LayerNorm(d_model=d_model) self.dropout2 = nn.Dropout(p=drop_prob) def forward(self, x, s_mask): # 1. compute self attention _x = x x = self.attention(q=x, k=x, v=x, mask=s_mask) # x->q,k,v # 2. add and norm x = self.norm1(x + _x) x = self.dropout1(x) # 3. positionwise feed forward network _x = x x = self.ffn(x) # 4. add and norm x = self.norm2(x + _x) x = self.dropout2(x) return x2.3.1. Multi-Headed Attention编码器中的 Multi-headed attention 模块采用了一种特殊的 Attention 机制，即：Self-Attention(自注意力机制).Self-Attention 使得模型能够关联输入中的每个单词和其他单词. 因此，对于示例，模型可以学习到单词 “you” 和 “how” 及 “are” 之间的关联性. 模型还可能会学习到，在这种模式里的单词往往是一个问题，并做出适当的回答.图：Encoder Self-Attention Operations. 其实现如：models/layers/multi_head_attention.pyfrom torch import nn from models.layers.scale_dot_product_attention import ScaleDotProductAttention class MultiHeadAttention(nn.Module): def __init__(self, d_model, n_head): super(MultiHeadAttention, self).__init__() self.n_head = n_head self.attention = ScaleDotProductAttention() # self.w_q = nn.Linear(d_model, d_model) self.w_k = nn.Linear(d_model, d_model) self.w_v = nn.Linear(d_model, d_model) self.w_concat = nn.Linear(d_model, d_model) def forward(self, q, k, v, mask=None): # 1. dot product with weight matrices q, k, v = self.w_q(q), self.w_k(k), self.w_v(v) # 2. split tensor by number of heads q, k, v = self.split(q), self.split(k), self.split(v) # 3. do scale dot product to compute similarity out, attention = self.attention(q, k, v, mask=mask) # 4. concat and pass to linear layer out = self.concat(out) out = self.w_concat(out) # 5. visualize attention map # TODO : we should implement visualization return out def split(self, tensor): """ split tensor by number of head :param tensor: [batch_size, length, d_model] :return: [batch_size, head, length, d_tensor] """ batch_size, length, d_model = tensor.size() d_tensor = d_model // self.n_head tensor = tensor.view(batch_size, self.n_head, length, d_tensor) # it is similar with group convolution (split by number of heads) return tensor def concat(self, tensor): """ inverse function of self.split(tensor : torch.Tensor) :param tensor: [batch_size, head, length, d_tensor] :return: [batch_size, length, d_model] """ batch_size, head, length, d_tensor = tensor.size() d_model = head * d_tensor tensor = tensor.view(batch_size, length, d_model) return tensor2.3.1.1. ScaleDotProductAttention其实现如：scale_dot_product_attention.pyimport math from torch import nn class ScaleDotProductAttention(nn.Module): """ compute scale dot product attention Query : given sentence that we focused on (decoder) Key : every sentence to check relationship with Qeury(encoder) Value : every sentence same with Key (encoder) """ def __init__(self): super(ScaleDotProductAttention, self).__init__() self.softmax = nn.Softmax() def forward(self, q, k, v, mask=None, e=1e-12): # input is 4 dimension tensor # [batch_size, head, length, d_tensor] batch_size, head, length, d_tensor = k.size() # 1. dot product Query with Key^T to compute similarity k_t = k.view(batch_size, head, d_tensor, length) # transpose score = (q @ k_t) / math.sqrt(d_tensor) # scaled dot product # 2. apply masking (opt) if mask is not None: score = score.masked_fill(mask == 0, -e) # 3. pass them softmax to make [0, 1] range score = self.softmax(score) # 4. multiply with Value v = score @ v return v, score逐步分析如下：[1] - Query, Key, Value Vectors为了实现 Self-Attention，首先将输入送到 3 个不同的 fully connected layers，以得到 query, key, 和 value vectors.而，query, key, value vectors 这三个向量表示什么呢，可以参考：what-exactly-are-keys-queries-and-values-in-attention-mechanisms.query,key,value的概念来自于检索系统. 例如：当在 Youtube 输入 query 来搜索某些视频时，搜索引擎会将 query 映射到一组 keys 集合(包含，视频标题、描述等)，其关联了数据库中的候选视频，然后返回最佳的匹配视频结果(values).[2] - Dot Product of Query and Key将 query, key 和 value 向量经过 linear 层处理后，queries 和 keys 经过点积矩阵相乘，得到得分(score)矩阵.图：query 和 key 的点积矩阵乘法score 矩阵确定了一个单词应该放到其他单词上的重要成都. 因此，每个单词和其他单词都有一个对应的分数. 分数越高，关联性越高. 这就是 queries 如何被映射到 keys 上的原理.图：点积得到的 Attention scores 矩阵.[3] - Scaling Down the Attention Scores接着，attentions scores 通过除以 query 和 key 的维度的平方根进行缩放. 以得到更稳定的梯度，因为相乘的值可能会导致梯度爆炸.(使得 scores 的方差变小.)图：Attention scores 缩放[4] - Softmax of the Scaled Scores接着，可以对缩放后的 scores 采用 softmax 处理，以得到 attention 权重，其是 0-1之间的概率值. Softmax 处理后，较高的分数会提高，而较低的分数会抑制，使得模型更加自信地关注要参与的单词.图：scaled scores 的 softmax 处理，以得到概率值[5] - Multiply Softmax Output with Value vector接着，将 attention 权重和 value 向量相乘，以得到输出向量. 较高的 softmax scores 值将使模型学习的单词的 value 值更重要. 而较低的 scores 值将淹没为无关的单词. 然后，将相乘的输出送到线性层进行处理.2.3.1.2. Computing Multi-headed Attentionmulti-headed attention 的计算，需要在 Self-Attention 前，将 query, key, value f分别划分为 N 个向量. 然后，划分后的向量分别经过 Self-Attention 处理. 每个 Self-Attention 处理称为一个 head. 每个 head 输出一个向量，该向量在经过最后的线性层处理之前，被连接为一个向量.理论上来说，每个 head 会学习到一些不同的信息，因此可以赋予编码器模型更多的表示能力.图：Self-Attention 处理前，将 Q, K, V 分为 N 块综上，multi-headed attention 是 Transformer 网络中的一个模块，其可以计算输入的 attention 权重，并输出向量；该输出向量编码了序列中单词和其他单词之间的关联性信息.2.3.2. Residual Connections, Layer Normalization 和 Feed Forward Networkmulti-headed attention 的输出向量，被添加到原始的 postional input embedding. 即残差连接(residual connection). 残差连接的输出再经过 layer normalization 处理.图：postional input embedding 和 multi-headed attention 输出的残差连接归一化化后的残差输出再通过 pointwise feed-forward network 进一步处理. pointwise feed-forward network 是一对线性层，中间有一个 ReLU 激活层. 然后，将其输出再次添加到 pointwise feed-forward network 的输入中，并进一步进行归一化.图：point-wise feedforward layer 网络层输入和输出的残差连接残差连接通过允许梯度直接流过网络，有助于网络训练. Layer normalizations 用于稳定网络，从而大大减少训练所需时间. pointwise feedforward layer 用于投影 attention 输出，从而可能使其具有更丰富的表示形式.2.3.2.1. LayerNorm实现如：models/layers/layer_norm.pyimport torch from torch import nn class LayerNorm(nn.Module): def __init__(self, d_model, eps=1e-12): super(LayerNorm, self).__init__() self.gamma = nn.Parameter(torch.ones(d_model)) self.beta = nn.Parameter(torch.zeros(d_model)) self.eps = eps def forward(self, x): mean = x.mean(-1, keepdim=True) std = x.std(-1, keepdim=True) # '-1' means last dimension. out = (x - mean) / (std + self.eps) out = self.gamma * out + self.beta return out2.3.2.2. PositionwiseFeedForward实现如：models/layers/position_wise_feed_forward.pyfrom torch import nn class PositionwiseFeedForward(nn.Module): def __init__(self, d_model, hidden, drop_prob=0.1): super(PositionwiseFeedForward, self).__init__() self.linear1 = nn.Linear(d_model, hidden) self.linear2 = nn.Linear(hidden, d_model) self.relu = nn.ReLU() self.dropout = nn.Dropout(p=drop_prob) def forward(self, x): x = self.linear1(x) x = self.relu(x) x = self.dropout(x) x = self.linear2(x) return x2.3.3. Encoder Wrap-up封装编码器层. 所有的编码器操作都是将输入编码为带有 attention 信息的连续表示形式. 其将有助于解码器在解码过程中，更着重于输入中的适当单词. 可以将编码器堆叠 N 次，以进一步对信息进行编码，其中每一层都有可能学习不同的 Attention 表示，因此有可能提升 Transformer 的预测能力.实现如：models/model/encoder.pyfrom torch import nn from models.blocks.encoder_layer import EncoderLayer from models.embedding.transformer_embedding import TransformerEmbedding class Encoder(nn.Module): def __init__(self, enc_voc_size, max_len, d_model, ffn_hidden, n_head, n_layers, drop_prob, device): super().__init__() self.emb = TransformerEmbedding(d_model=d_model, max_len=max_len, vocab_size=enc_voc_size, drop_prob=drop_prob, device=device) self.layers = nn.ModuleList([EncoderLayer(d_model=d_model, ffn_hidden=ffn_hidden, n_head=n_head, drop_prob=drop_prob) for _ in range(n_layers)]) def forward(self, x, s_mask): x = self.emb(x) for layer in self.layers: x = layer(x, s_mask) return x2.4. Decoder Layer解码器的工作是，生成文本序列.解码器具有和编码器相似的子网络层(sub-layer)，其包含 2 个 multi-headed attention 层，1 个 pointwise feed-forward 层，残差连接， layer normalization 层. 这些 sub-layers 具有类似于在解码器中的相似作用，但是，每个 multi-headed attention 层的作用却不用. 解码器的输出为采用线性层和 softmax 层作为分类器，以得到单词的概率.图：Decoder Layer解码器是自回归的(autoregressive)，其以起始 token 作为开始，并且将先前输出的列表，以及包含来自输入的 Attention 信息的编码器输出，作为输入. 当解码器生成 token 作为输出时，停止解码器解码.解码器实现如：models/blocks/decoder_layer.pyfrom torch import nn from models.layers.layer_norm import LayerNorm from models.layers.multi_head_attention import MultiHeadAttention from models.layers.position_wise_feed_forward import PositionwiseFeedForward class DecoderLayer(nn.Module): def __init__(self, d_model, ffn_hidden, n_head, drop_prob): super(DecoderLayer, self).__init__() self.self_attention = MultiHeadAttention(d_model=d_model, n_head=n_head) self.norm1 = LayerNorm(d_model=d_model) self.dropout1 = nn.Dropout(p=drop_prob) self.enc_dec_attention = MultiHeadAttention(d_model=d_model, n_head=n_head) self.norm2 = LayerNorm(d_model=d_model) self.dropout2 = nn.Dropout(p=drop_prob) self.ffn = PositionwiseFeedForward(d_model=d_model, hidden=ffn_hidden, drop_prob=drop_prob) self.norm3 = LayerNorm(d_model=d_model) self.dropout3 = nn.Dropout(p=drop_prob) def forward(self, dec, enc, t_mask, s_mask): # 1. compute self attention _x = dec x = self.self_attention(q=dec, k=dec, v=dec, mask=t_mask) # 2. add and norm x = self.norm1(x + _x) x = self.dropout1(x) if enc is not None: # 3. compute encoder - decoder attention _x = x x = self.enc_dec_attention(q=x, k=enc, v=enc, mask=s_mask) # 4. add and norm x = self.norm2(x + _x) x = self.dropout2(x) # 5. positionwise feed forward network _x = x x = self.ffn(x) # 6. add and norm x = self.norm3(x + _x) x = self.dropout3(x) return x2.4.1. Input Embeddings 和 Positional Encoding解码器的开始部分与编码器几乎相同. 输入经过 input embedding 层和 positional encoding 层处理. positional embeddings 被送入第一个 multi-head attention 层，以计算解码器输入的 attention scores.2.4.2. Decoders First Multi-Headed Attention解码器第一个 multi-head attention 层的操作略有不同. 因为解码器是自回归的，且逐个单词的生成序列，需要避免其受到未来 tokens 的约数限制. 例如，当计算单词 "am" 的 attention scores 时，不应该包含单词 "fine"，因为这个单词是未来才生成的单词. 单词 "am" 应该仅能访问其自己以及其前面的单词. 同理，对于其他所有单词，也智能访问其先前的单词.因此需要一种方法来避免计算未来单词的 attention scores. 即：masking. 为了避免解码器的查看到未来 tokens，可以采用 look ahead mask. 再计算 softmax 前，和scores 缩放之后，添加 mask. 其工作如下.2.4.2.1. Look-Ahead Maskmask 是一个与 attention scores 相同大小的矩阵，其值填充为 0 和 -inf. 当将 mask 添加到缩放 attention socres 后，得到一个 scores 的矩阵，其右上三角填充为 -inf.图: 添加 look-ahead mask 到 scaled scores设置 mask 的原因是，一旦得到 masked scores 的 softmax 值，-inf 会被清零，从而使得未来 tokens 的 attention scores 值为0. 如下图所示，"am" 的 attention scores值，有其自身以及其之前的值，而对于单词 "fine" 的值为0. 本质上是告诉模型不要将重点放到这些单词上.masking 是第一个 multi-headed attention 层的 attention scores 计算中的唯一区别. 第一个 multi-headed attention 层的输出是 masked 输出向量，其包含了模型应该如何参与解码器输入的信息.图：Multi-Headed Attention with Masking#### 2.4.3. Decoder Second Multi-Headed Attention 和 Point-wise Feed Forward Layer对于解码器的第二个 multi-headed attention 层，编码器的输出是 queries 和 keys，第一个 multi-headed attention 的输出是 values. 此过程使得编码器的输入和解码器的输入相匹配，从而使解码器可以确定哪些编码器输入是与重点关注的是相关的. 第二个 multi-headed attention 层的输出经过 pointwise feedforward 进一步处理.2.4.4. Linear Classifier and Final Softmax for Output Probabilitiespointwise feedforward 的输出经过最终的线性层处理，该线性层作为作为分类器. 分类器是与类别数一样大的. 例如，假设 10000 个单词有 10000 个类，则分类器的输出是 10000. 分类器的输出被送入 Softmax 层处理，以输出 0-1 之间的概率值. 采用最大概率的索引，该索引对应于要预测的单词.图：Linear Classifier with Softmax to get the Output Probabilities然后，解码器得到输出，将其添加到解码器的输入列表中，然后再次继续解码，知道预测处 token 位置. 比如，最高的概率预测为分配给结束 token 的类别.解码器也可以堆叠 N 层，每层接收来自编码器及其之前各层的输入. 通过堆叠网络层，模型可以学习从其 attention heads 中提取并关注与不同的 attention 组合，从而有可能增强其预测能力.图：Stacked Encoder and Decoder其实现如：models/model/decoder.pyimport torch from torch import nn from models.blocks.decoder_layer import DecoderLayer from models.embedding.transformer_embedding import TransformerEmbedding class Decoder(nn.Module): def __init__(self, dec_voc_size, max_len, d_model, ffn_hidden, n_head, n_layers, drop_prob, device): super().__init__() self.emb = TransformerEmbedding(d_model=d_model, drop_prob=drop_prob, max_len=max_len, vocab_size=dec_voc_size, device=device) self.layers = nn.ModuleList([DecoderLayer(d_model=d_model, ffn_hidden=ffn_hidden, n_head=n_head, drop_prob=drop_prob) for _ in range(n_layers)]) self.linear = nn.Linear(d_model, dec_voc_size) def forward(self, trg, enc_src, trg_mask, src_mask): trg = self.emb(trg) for layer in self.layers: trg = layer(trg, enc_src, trg_mask, src_mask) # pass to LM head output = self.linear(trg) return output2.5. Transformer 实现如：models/model/transformer.pyimport torch from torch import nn from models.model.decoder import Decoder from models.model.encoder import Encoder class Transformer(nn.Module): def __init__(self, src_pad_idx, trg_pad_idx, trg_sos_idx, enc_voc_size, dec_voc_size, d_model, n_head, max_len, ffn_hidden, n_layers, drop_prob, device): super().__init__() self.src_pad_idx = src_pad_idx self.trg_pad_idx = trg_pad_idx self.trg_sos_idx = trg_sos_idx self.device = device self.encoder = Encoder(d_model=d_model, n_head=n_head, max_len=max_len, ffn_hidden=ffn_hidden, enc_voc_size=enc_voc_size, drop_prob=drop_prob, n_layers=n_layers, device=device) self.decoder = Decoder(d_model=d_model, n_head=n_head, max_len=max_len, ffn_hidden=ffn_hidden, dec_voc_size=dec_voc_size, drop_prob=drop_prob, n_layers=n_layers, device=device) def forward(self, src, trg): src_mask = self.make_pad_mask(src, src) src_trg_mask = self.make_pad_mask(trg, src) trg_mask = self.make_pad_mask(trg, trg) * \ self.make_no_peak_mask(trg, trg) enc_src = self.encoder(src, src_mask) output = self.decoder(trg, enc_src, trg_mask, src_trg_mask) return output def make_pad_mask(self, q, k): len_q, len_k = q.size(1), k.size(1) # batch_size x 1 x 1 x len_k k = k.ne(self.src_pad_idx).unsqueeze(1).unsqueeze(2) # batch_size x 1 x len_q x len_k k = k.repeat(1, 1, len_q, 1) # batch_size x 1 x len_q x 1 q = q.ne(self.src_pad_idx).unsqueeze(1).unsqueeze(3) # batch_size x 1 x len_q x len_k q = q.repeat(1, 1, 1, len_k) mask = k & q return mask def make_no_peak_mask(self, q, k): len_q, len_k = q.size(1), k.size(1) # len_q x len_k mask = torch.tril(torch.ones(len_q, len_k)).type(torch.BoolTensor).to(self.device) return mask3. Transformer 训练与测试3.1. Transformer 训练实现如：hyunwoongko/transformer/train.pyimport math import time from torch import nn, optim from torch.optim import Adam from data import * from models.model.transformer import Transformer from util.bleu import idx_to_word, get_bleu def epoch_time(start_time, end_time): elapsed_time = end_time - start_time elapsed_mins = int(elapsed_time / 60) elapsed_secs = int(elapsed_time - (elapsed_mins * 60)) return elapsed_mins, elapsed_secs def count_parameters(model): return sum(p.numel() for p in model.parameters() if p.requires_grad) def initialize_weights(m): if hasattr(m, 'weight') and m.weight.dim() > 1: nn.init.kaiming_uniform(m.weight.data) # model = Transformer(src_pad_idx=src_pad_idx, trg_pad_idx=trg_pad_idx, trg_sos_idx=trg_sos_idx, d_model=d_model, enc_voc_size=enc_voc_size, dec_voc_size=dec_voc_size, max_len=max_len, ffn_hidden=ffn_hidden, n_head=n_heads, n_layers=n_layers, drop_prob=drop_prob, device=device).to(device) print(f'The model has {count_parameters(model):,} trainable parameters') model.apply(initialize_weights) optimizer = Adam(params=model.parameters(), lr=init_lr, weight_decay=weight_decay, eps=adam_eps) scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer=optimizer, verbose=True, factor=factor, patience=patience) criterion = nn.CrossEntropyLoss(ignore_index=src_pad_idx) def train(model, iterator, optimizer, criterion, clip): model.train() epoch_loss = 0 for i, batch in enumerate(iterator): src = batch.src trg = batch.trg optimizer.zero_grad() output = model(src, trg[:, :-1]) output_reshape = output.contiguous().view(-1, output.shape[-1]) trg = trg[:, 1:].contiguous().view(-1) loss = criterion(output_reshape, trg) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), clip) optimizer.step() epoch_loss += loss.item() print('step :', round((i / len(iterator)) * 100, 2), '% , loss :', loss.item()) return epoch_loss / len(iterator) def evaluate(model, iterator, criterion): model.eval() epoch_loss = 0 batch_bleu = [] with torch.no_grad(): for i, batch in enumerate(iterator): src = batch.src trg = batch.trg output = model(src, trg[:, :-1]) output_reshape = output.contiguous().view(-1, output.shape[-1]) trg = trg[:, 1:].contiguous().view(-1) loss = criterion(output_reshape, trg) epoch_loss += loss.item() total_bleu = [] for j in range(batch_size): try: trg_words = idx_to_word(batch.trg[j], loader.target.vocab) output_words = output[j].max(dim=1)[1] output_words = idx_to_word(output_words, loader.target.vocab) bleu = get_bleu(hypotheses=output_words.split(), reference=trg_words.split()) total_bleu.append(bleu) except: pass total_bleu = sum(total_bleu) / len(total_bleu) batch_bleu.append(total_bleu) batch_bleu = sum(batch_bleu) / len(batch_bleu) return epoch_loss / len(iterator), batch_bleu def run(total_epoch, best_loss): train_losses, test_losses, bleus = [], [], [] for step in range(total_epoch): start_time = time.time() train_loss = train(model, train_iter, optimizer, criterion, clip) valid_loss, bleu = evaluate(model, valid_iter, criterion) end_time = time.time() if step > warmup: scheduler.step(valid_loss) train_losses.append(train_loss) test_losses.append(valid_loss) bleus.append(bleu) epoch_mins, epoch_secs = epoch_time(start_time, end_time) if valid_loss < best_loss: best_loss = valid_loss torch.save(model.state_dict(), 'saved/model-{0}.pt'.format(valid_loss)) f = open('result/train_loss.txt', 'w') f.write(str(train_losses)) f.close() f = open('result/bleu.txt', 'w') f.write(str(bleus)) f.close() f = open('result/test_loss.txt', 'w') f.write(str(test_losses)) f.close() print(f'Epoch: {step + 1} | Time: {epoch_mins}m {epoch_secs}s') print(f'\tTrain Loss: {train_loss:.3f} | Train PPL: {math.exp(train_loss):7.3f}') print(f'\tVal Loss: {valid_loss:.3f} | Val PPL: {math.exp(valid_loss):7.3f}') print(f'\tBLEU Score: {bleu:.3f}') if __name__ == '__main__': run(total_epoch=epoch, best_loss=inf)其中涉及的函数有：3.1.1. 配置confconf.pyimport torch # GPU device setting device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") # model parameter setting batch_size = 16 # 128 max_len = 256 d_model = 512 n_layers = 6 n_heads = 8 ffn_hidden = 2048 drop_prob = 0.1 # optimizer parameter setting init_lr = 1e-5 factor = 0.9 adam_eps = 5e-9 patience = 10 warmup = 100 epoch = 1000 clip = 1.0 weight_decay = 5e-4 inf = float('inf')3.1.2. 数据datadata.pyfrom conf import * from util.data_loader import DataLoader from util.tokenizer import Tokenizer tokenizer = Tokenizer() loader = DataLoader(ext=('.en', '.de'), tokenize_en=tokenizer.tokenize_en, tokenize_de=tokenizer.tokenize_de, init_token='<sos>', eos_token='<eos>') train, valid, test = loader.make_dataset() loader.build_vocab(train_data=train, min_freq=2) train_iter, valid_iter, test_iter = loader.make_iter(train, valid, test, batch_size=batch_size, device=device) src_pad_idx = loader.source.vocab.stoi['<pad>'] trg_pad_idx = loader.target.vocab.stoi['<pad>'] trg_sos_idx = loader.target.vocab.stoi['<sos>'] enc_voc_size = len(loader.source.vocab) dec_voc_size = len(loader.target.vocab)util/bleu.pyimport math from collections import Counter import numpy as np def bleu_stats(hypothesis, reference): """Compute statistics for BLEU.""" stats = [] stats.append(len(hypothesis)) stats.append(len(reference)) for n in range(1, 5): s_ngrams = Counter( [tuple(hypothesis[i:i + n]) for i in range(len(hypothesis) + 1 - n)] ) r_ngrams = Counter( [tuple(reference[i:i + n]) for i in range(len(reference) + 1 - n)] ) stats.append(max([sum((s_ngrams & r_ngrams).values()), 0])) stats.append(max([len(hypothesis) + 1 - n, 0])) return stats def bleu(stats): """Compute BLEU given n-gram statistics.""" if len(list(filter(lambda x: x == 0, stats))) > 0: return 0 (c, r) = stats[:2] log_bleu_prec = sum( [math.log(float(x) / y) for x, y in zip(stats[2::2], stats[3::2])] ) / 4. return math.exp(min([0, 1 - float(r) / c]) + log_bleu_prec) def get_bleu(hypotheses, reference): """Get validation BLEU score for dev set.""" stats = np.array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]) for hyp, ref in zip(hypotheses, reference): stats += np.array(bleu_stats(hyp, ref)) return 100 * bleu(stats) def idx_to_word(x, vocab): words = [] for i in x: word = vocab.itos[i] if '<' not in word: words.append(word) words = " ".join(words) return wordsutil/tokenizer.pyimport spacy class Tokenizer: def __init__(self): self.spacy_de = spacy.load('de_core_news_sm') self.spacy_en = spacy.load('en_core_web_sm') def tokenize_de(self, text): """ Tokenizes German text from a string into a list of strings """ return [tok.text for tok in self.spacy_de.tokenizer(text)] def tokenize_en(self, text): """ Tokenizes English text from a string into a list of strings """ return [tok.text for tok in self.spacy_en.tokenizer(text)]3.1.3. DataLoaderutil/data_loader.pyfrom torchtext.legacy.data import Field, BucketIterator from torchtext.legacy.datasets.translation import Multi30k class DataLoader: source: Field = None target: Field = None def __init__(self, ext, tokenize_en, tokenize_de, init_token, eos_token): self.ext = ext self.tokenize_en = tokenize_en self.tokenize_de = tokenize_de self.init_token = init_token self.eos_token = eos_token print('dataset initializing start') def make_dataset(self): if self.ext == ('.de', '.en'): self.source = Field(tokenize=self.tokenize_de, init_token=self.init_token, eos_token=self.eos_token, lower=True, batch_first=True) self.target = Field(tokenize=self.tokenize_en, init_token=self.init_token, eos_token=self.eos_token, lower=True, batch_first=True) elif self.ext == ('.en', '.de'): self.source = Field(tokenize=self.tokenize_en, init_token=self.init_token, eos_token=self.eos_token, lower=True, batch_first=True) self.target = Field(tokenize=self.tokenize_de, init_token=self.init_token, eos_token=self.eos_token, lower=True, batch_first=True) train_data, valid_data, test_data = Multi30k.splits(exts=self.ext, fields=(self.source, self.target)) return train_data, valid_data, test_data def build_vocab(self, train_data, min_freq): self.source.build_vocab(train_data, min_freq=min_freq) self.target.build_vocab(train_data, min_freq=min_freq) def make_iter(self, train, validate, test, batch_size, device): train_iterator, valid_iterator, test_iterator = BucketIterator.splits((train, validate, test), batch_size=batch_size, device=device) print('dataset initializing done') return train_iterator, valid_iterator, test_iterator3.2. Transformer 测试hyunwoongko/transformer/test.pyimport math from collections import Counter import numpy as np from data import * from models.model.transformer import Transformer from util.bleu import get_bleu, idx_to_word def count_parameters(model): return sum(p.numel() for p in model.parameters() if p.requires_grad) # model = Transformer(src_pad_idx=src_pad_idx, trg_pad_idx=trg_pad_idx, trg_sos_idx=trg_sos_idx, d_model=d_model, enc_voc_size=enc_voc_size, dec_voc_size=dec_voc_size, max_len=max_len, ffn_hidden=ffn_hidden, n_head=n_heads, n_layers=n_layers, drop_prob=0.00, device=device).to(device) print(f'The model has {count_parameters(model):,} trainable parameters') def test_model(num_examples): iterator = test_iter model.load_state_dict(torch.load("./saved/model-saved.pt")) with torch.no_grad(): batch_bleu = [] for i, batch in enumerate(iterator): src = batch.src trg = batch.trg output = model(src, trg[:, :-1]) total_bleu = [] for j in range(num_examples): try: src_words = idx_to_word(src[j], loader.source.vocab) trg_words = idx_to_word(trg[j], loader.target.vocab) output_words = output[j].max(dim=1)[1] output_words = idx_to_word(output_words, loader.target.vocab) print('source :', src_words) print('target :', trg_words) print('predicted :', output_words) print() bleu = get_bleu(hypotheses=output_words.split(), reference=trg_words.split()) total_bleu.append(bleu) except: pass total_bleu = sum(total_bleu) / len(total_bleu) print('BLEU SCORE = {}'.format(total_bleu)) batch_bleu.append(total_bleu) batch_bleu = sum(batch_bleu) / len(batch_bleu) print('TOTAL BLEU SCORE = {}'.format(batch_bleu)) if __name__ == '__main__': test_model(num_examples=batch_size)
关于 RNN 和 LSTM 比较经典的基础知识及图例.1. RNNRNN，循环神经网络，其结构如：图：RNN 包含循环结构如图，神经网络 $A$，某个输入 $x_t$，对应输出值 $h_t$. RNN 可以是信息从当前状态传递到下一状态.数据传递效果：RNN 可以看作是同一神经网络的多次复制，每个神经网络模块将信息传递到下一个神经网络. RNN 的展开如图：图：RNN 网络展开形式，链式结构RNN 动图版：1.1. RNN 一般结构给定函数 $f:h',y=f(h,x)$，这里暂时忽略偏置bias，如图：其中，[1] - $x$ 为当前状态的输入[2] - $h$ 表示接收到的上一个节点网络的输入[3] - $y$ 为当前节点状态的输出[4] - $h'$ 为传递到下一个节点网络的输出. $h$ 和 $h'$ 是具有相同维度的向量.[5] - $\sigma$ 为激活函数，一般是 tanh 函数，以及会加一个偏置 $b$. 由图可知，输出 $h'$ 与 $x$ 和 $h$ 的值相关，而 $y$ 一般使用 $h'$ 送入到一个线性层(如，这里是维度映射)，在使用 softmax 激活函数进行分类，得到输出值.RNN 的标准结构如图：其中，函数 $f$ 是各个节点网络所共享的.RNN中，将过去的输出和当前的输入链接到一起，通过tanh来控制两者的输出，它只考虑最近时刻的状态. 在RNN中有两个输入和一个输出.单个RNN模块传递，动图：RNN 还可细分为，单输入序列输出、序列输入单输出、序列输入序列输出(分等长和不等长)等一系列结构.1.1.1. tanh 函数tanh 激活函数用于调节在神经网络中传递的值，它会将输入值压缩到-1 到 1 之间.1.2. RNN - PyTorchPytorch 模块 - torch.nn.RNNCell一个 RNN Cell 模块可以表示为：$$ h' = tanh(W_{ih}x + b_{ih} + W_{hh}h + b_{hh}) $$其中，$h'$ 是输出的下一个隐状态.函数定义为：torch.nn.RNNCell(input_size, hidden_size, bias=True, nonlinearity='tanh')其中，[1] - input_size - 输入 x 中的期望特征数[2] - hidden_size - 隐状态 h 的特征数[3] - bias - 若为 False，则不适用偏置权重 $b_{ih}$ 和 $b_{hh}$[4] - nonlinearity - 采用的非线性函数，tanh 或 relu.函数使用示例：rnn = nn.RNNCell(10, 20) input = torch.randn(6, 3, 10) hx = torch.randn(3, 20) output = [] for i in range(6): hx = rnn(input[i], hx) output.append(hx)1.3. Long-Term 依赖问题RNN 的关键点之一，是可以用来连接先前的信息到当前的任务上，例如使用先前信息来推测对当前状态的理解. 两种场景：[1] - 先前信息与预测信息之间的间隔非常小此种场景，RNN 可以学会使用先前信息. 仅仅需要知道先前的信息来执行当前的任务.例如，对于一个语言模型用来基于先前的词来预测下一个词，如果预测 "the clouds are in the sky" 最后的词，其并不需要任何其他的上下文. 因此下一个词很显然就应该是 sky.图：不太长的先前信息和位置间隔.[2] - 更复杂的场景，先前信息和当前预测的位置间隔比较大随着信息间隔不断增大时，RNN 会丧失学习到连接如此远的信息的能力.例如，预测 "I grew up in France... I speak fluent French" 最后的词. 当前的信息建议下一个词可能是一种语言的名字，但是如果弄清楚是什么语言，需要先前提到的离当前位置很远的 France 的上下文信息.图：较长的先前信息和位置间隔RNN 存在梯度消失问题，通常可以采用两种解决办法：[1] - 选取更好的激活函数，如ReLU函数.[2] - 改变传播网络结构，如LSTM.2. LSTMLSTM，Long short-term memory，长短期记忆. 是 RNN 的一种改进，主要为了解决长序列训练过程中的梯度小时问题，使得 RNN 具有更好更强的记忆能力.如图：图：LSTM 结构，类似于 RNN，各重复模块包含四种交互.LSTM 为了能记住长期的状态，在RNN的基础上增加了一路输入和一路输出，增加的这一路就是细胞状态，也就是图中最上面的一条通路.2.1. LSTM 图例上图中，包含的图标例示有：其中，[1] - 黄色矩形框是学习到的神经网络层[2] - 粉色圈代表按位 pointwise 操作，如向量和等[3] - 每一条黑线传输着整个向量，从一个节点的输出到其他节点的输入[4] - 合在一起的黑色线表示向量的连接[5] - 分开的黑色线表示内容被复制，然后分发到不同的位置2.1.1. sigmoid 激活函数sigmoid 激活函数类似于 tanh 激活函数，它会将数值控制在 0 到 1 之间，而不是-1 到 1. 其有助于更新或丢弃数据，因为任何数乘以 0 都是 0，这将导致数值消失或被遗忘；任何数字乘以 1 都是其本身，因此这个值不变或者保存. 网络可以知道哪些数据不重要，可以被遗忘，或者哪些数据需要保存.2.2. LSTM 核心思想LSTM 每个神经网络模块处理流程如：另一种图示：LSTM 的核心是细胞状态(cell state)，即下图中在图上方运行的黑色水平线.细胞状态类似于传送带，直接在整个链上运行，只有一些少量的线性操作，信息在上面流传保持不变会很容易.事实上整个LSTM分成了三个部分：[1] - 哪些细胞状态应该被遗忘 - 遗忘门[2] - 哪些新的状态应该被加入 - 输入门[3] - 根据当前的状态和现在的输入，输出应该是什么 - 输出门2.2.1. 遗忘门遗忘门主要是对上一个阶段传来的输入，决定从细胞状态中丢弃(或遗忘)哪些信息. 遗忘门的位置和结构公式如图.右边公式中，$f_t$ 是由 $h_{t-1}$ 和 $x_t$ 拼接向量乘以权重矩阵$W_f$ 后，再通过一个 sigmoid 激活函数，将值转换为 0 和 1 之间的数值.动图版：2.2.2. 输入门输入门主要是将输入有选择性地进行记忆，确定什么样的新信息被存放在细胞状态中，输入门的位置和结构公式如图.动图版：2.2.3. 当前状态根据遗忘门的输出 $f_t$、上一层记忆细胞状态值 $C_{t-1}$、输入门的输出 $i_t$ 和 $\tilde{C}_{t}$，共同决定当前细胞状态，如图：当前状态 $C_t$ 的更新，是将旧状态$C_{t-1}$ 与 $f_t$ 相乘，以丢弃需要被丢弃的信息；并加上 $i_t * \tilde{C}_{t}$.动图版：2.2.4. 输出门输出门决定了当前状态的哪些信息会被输出.首先通过一个 sigmoid 激活函数做门控制，以确定当前细胞状态的哪些信息要被输出；然后，通过 tanh 函数处理(其输出值在 -1 到 1 之间)，再与 sigmoid 函数的输出相乘，最终实现仅输出确定要输出的信息.动图版：2.3. LSTM - PyTorchPytorch 模块 - torch.nn.LSTMCell一个 LSTM 细胞模块的数学表达式如：$$ i = \sigma (W_{ii} x + b_{ii} + W_{hi}h + b_{hi}) $$$$ f = \sigma (W_{if} x + b_{if} + W_{hf} h + b_{hf}) $$$$ g = tanh(W_{io} x + b_{io} + W_{ho} h + b_{ho}) $$$$ c' = f * c + i*g $$$$ h' = o * tanh(c') $$其中，$\sigma$ 是 sigmoid 激活函数，$*$ 是 Hadamard 乘积.函数定义如：torch.nn.LSTMCell(input_size, hidden_size, bias=True)函数使用示例如：rnn = nn.LSTMCell(10, 20) # (input_size, hidden_size) # input = torch.randn(2, 3, 10) # (time_steps, batch, input_size) hx = torch.randn(3, 20) # (batch, hidden_size) cx = torch.randn(3, 20) # output = [] for i in range(input.size()[0]): hx, cx = rnn(input[i], (hx, cx)) output.append(hx) output = torch.stack(output, dim=0)2.4. LSTM 伪代码如：def LSTMCELL(prev_ct,prev_ht,input): #1.连接上一个细胞状态和当前的输入 combine = prev_ht + input #2.遗忘门处理，删除不相关数据 ft = forget_layer(combine) #3.创建候选层，候选项保存要添加到细胞状态的可能值 candidate = candidate_layer(combine) #4.输入层处理，决定应该添加到新的细胞状态的候选数据 it = input_layer(combine) #5.计算新的细胞状态 ct = prev_ct * ft + candidate * it #6.计算输出 ot = output_layer(combine) #7.计算新的隐藏状态，将输出和新的细胞状态的对应元素乘积 ht = ot * tanh(ct) return ht,ct ct = [0,0,0] ht = [0,0,0] for input in inputs: ct,ht = LSTMCELL(ct,ht,input)3. 材料[1] - Illustrated Guide to Recurrent Neural Networks - 2018.09.20 强烈推荐[2] - Understanding LSTM Networks - 2015.08.27 强烈推荐[3] - Illustrated Guide to LSTM’s and GRU’s: A step by step explanation - 2018.09.25 强烈推荐[4] - 一步一步，看图理解长短期记忆网络与门控循环网络 - 2018.10.13 - 对应中文翻译版[5] - RNN梯度消失和爆炸的原因 - 2017.08.24[6] - LSTM如何解决梯度消失问题 - 2017.08.24[7] - Why LSTMs Stop Your Gradients From Vanishing: A View from the Backwards Pass - 2017.11.15[8] - Visualizing memorization in RNNs
Github - tensorflow/servinghttps://github.com/tensorflow/serving/tree/master/tensorflow_serving/g3doc1. TF Serving 示例#拉取镜像 # docker docker pull tensorflow/serving # nvidia-docker docker pull tensorflow/serving:latest-gpu # git clone https://github.com/tensorflow/serving # Location of demo models TESTDATA="$(pwd)/serving/tensorflow_serving/servables/tensorflow/testdata" # Start TensorFlow Serving container and open the REST API port docker run -t --rm -p 8501:8501 \ -v "$TESTDATA/saved_model_half_plus_two_cpu:/models/half_plus_two" \ -e MODEL_NAME=half_plus_two \ tensorflow/serving & # Query the model using the predict API curl -d '{"instances": [1.0, 2.0, 5.0]}' \ -X POST http://localhost:8501/v1/models/half_plus_two:predict # Returns => { "predictions": [2.5, 3.0, 4.5] }2. TF Serving 参数TF Serving 运行示例：docker run -p 8501:8501 \ --mount type=bind,source=/path/to/local/saved_model/,target=/models/model_name \ -t tensorflow/serving \ -e MODEL_NAME=model_name --model_base_path=/models/model_name/ --per_process_gpu_memory_fraction=0.5 #或 docker run -t --rm -p 8501:8501 \ -v /path/to/local/saved_model/:/models/model_name \ -e MODEL_NAME=test-model tensorflow/serving:1.15.0 &[1] - -p 8501:8501 - 服务端口[2] - -t - 指定镜像[3] - --mount - 挂载路径，其中type=bind是选择挂载模式，注：中间不能有空格. source 是模型在机器中的绝对存储路径，target 是模型容器中存储路径[4] - per_process_gpu_memory_fraction 是设定运行时所需的GPU显存资源最大不允许超过的比率的设定值；[5] - model_name 命名3. 参考[1] - Tensorflow笔记：通过tf.Serving+Docker部署 - 2020.04.14[2] - TensorFlow Serving with Docker