原文：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)
基于深度神经网络的噪声标签学习问题[转]
基于深度神经网络的噪声标签学习-云社区-华为云 (huaweicloud.com)神经网络的成功建立在大量的干净数据和很深的网络模型基础上。但是在现实场景中数据和模型往往不会特别理想，比如数据层面有误标记的情况，像小狗被标注成狼，而且实际的业务场景讲究时效性，神经网络的层数不能特别深。我们尝试不断迭代数据和模型缺陷情况下神经网络的有效训练方法，通过noisy label learning技术，解决网络训练过程中noisy data的问题，该技术已经在团队实际业务场景中落地，通过从损失函数、网络结构、模型正则化、损失函数调整、样本选择、标签纠正等多个模块的优化，不局限于全监督、半监督和自监督学习方法，提升整个模型的鲁棒性1. 损失函数主要是从损失函数去修改，核心思路是当数据整体是干净的时候，传统的交叉熵损失函数学习到少量的负样本，可以提升模型的鲁棒性；当数据噪声比较大时，CE会被噪声数据带跑偏，我们要修改损失函数使其在训练中每个样本的权重都是一样重要的，因此不难想到采用GCE Loss，控制超参数，结合了CE Loss和MAE LossA. Ghosh, H. Kumar, and P. Sastry,“Robust loss functions under label noise for deep neural networks,” in Proc. AAAI, 2017Generalized Cross Entropy Loss for Training Deep Neural Networks with Noisy Labels, NeurlPS 2018另外，还有从KL散度想法借鉴过来的，作者认为在计算熵的时候，原始q, p代表的真实数据分布和预测值在较为干净的数据上没有问题，但是在噪声比较大的数据上，可能q并不能代表真实数据分布，相反的是不是p可以表示真实数据分布，因此提出基于对称的交叉熵损失函数（Symmetric cross entropy ）Y. Wang, X. Ma, Z. Chen, Y. Luo, J. Yi, and J. Bailey, “Symmetric cross entropy for robust learning with noisy labels,” in Proc. ICCV, 2019, pp. 322–3302. 模型结构主要通过借鉴巧妙的网络结构，在模型训练过程中，通过模型对数据进行挑选，选择一批较为干净的数据，逐步提升模型的鲁棒性。首先要介绍的就是coteaching framework，首先是基于两个模型相互挑选数据输入给对方计算loss，传递给对方网络的数据是每个min-batch里面loss最低的一些数据，随着epoch增加，数据量有所变化，另外每一轮epoch结束，会shuffle数据，保证数据不会被永久遗忘How does Disagreement Help Generalization against Label Corruption? ICML 2019Co-teaching: Robust Training of Deep Neural Networks with Extremely Noisy Labels, NeurlPS 2018还有一种思路，是基于attention注意力机制给干净样本和噪声数据进行打分，文章叫做Attention Feature Mixup，在计算最终loss的时候有两部分，一部分是同一个类的每张图和标签计算的交叉熵损失；另外一个损失是数据mixup得到的新的数据x'和标签y'计算的loss3. 正则化主要是通过一些添加正则ticks，防止模型过拟合到噪声数据上，常用的正则方法包含：label smooth、l1、l2、MixUp等4. 样本选择主要是从如何挑选出更好的数据为出发点，一种方法，叫做Area Under the Margin metric (AUM)，是我们去年参加CVPR WebVision 2020（图像识别领域最高级别的比赛，接替ImageNet）取得冠军的方案。该方案是一种在训练过程中一边训练一边筛选数据的方式，具体思想是在一个min-batch利用模型计算每张图片的logits值和其它类中最大的logits求差值当做area，这样多轮epoch求平均，得到每张图的aum值。试验发现如果这个数据是较为干净的数据area的值会比较大，如果是存在mis-label的数据area值比较小，甚至是负值，作者就是通过这个思想将一个类的干净数据和噪声数据分离开的。当然论文在最后也指出，干净数据和噪声数据占比99%的阈值是最优的。Pleiss, Geoff, et al. "Identifying mislabeled data using the area under the margin ranking.“, NeurlPS 2020.另外一篇，数据划分是通过密度聚类的思路，将一个类的数据分成easy dataset、smi-hard dataset和hard dataset，一般噪声数据是较为困难训练的数据，对于每张图分配一个权重，文中建议1.0、0.5和0.5；模型的训练借鉴了课程学习的思路Guo, Sheng, et al. "Curriculumnet: Weakly supervised learning from large-scale web images." Proceedings of the European Conference on Computer Vision (ECCV). 2018.5. 标签纠正标签纠正(label correction)方法思路很简单就是相当于是一个重新打一个伪标签的概念，但是完全放弃原始的标签也过于暴力，ICCV2019的这篇文章在“label correction phase”通过一个pre-trained模型得到随机选择每个类中的几张图采用聚类的方法得到Prototype样本的每个类的聚类中心，对输入图片得到的特征向量和各类聚类中心计算距离，得到图片的伪标签，最后的loss是原始标签计算的交叉熵损失和伪标签计算的伪标签的求和Han, Jiangfan, Ping Luo, and Xiaogang Wang. "Deep self-learning from noisy labels.“, ICCV 2019
淘宝首页那些辣眼睛的图片过滤[转]
原文：淘宝首页那些“辣眼睛”的图去哪了？- 2019.12.13出处：天池大数据科研平台 - 微信公众号作者：忘返导读：在淘宝这个平台上，消费者每天都在浏览着琳琅满目的图片，图片是消费者与商品交互的第一媒介. 图片的质量会极大影响消费者的消费体验和转化效果，高质量的图片会让消费者怦然心动，流连忘返，而低质量的“辣眼睛”图片会让消费者望而却步. 在淘宝首页猜你喜欢(以下简称首猜)场景下，由于图片数量太多，让人工来审核所有图片是不现实的. 作为阿里技术人，我们的使命就是使用技术手段过滤掉低质量的图片，将高质量的图片和素材呈现给消费者.一. 摘要恶心图片，顾名思义，就是让人感到恶心，不适的图片. 其大致可以被分为跟动物有关的，跟人有关的，以及物体这三种类型. 要从数量巨大的商品池中精确地将恶心图片过滤出来，面临着以下几个技术难点：[1] - 初始样本很少且难收集，面临模型冷启动的问题；[2] - 线上真实数据场景下，正负样本比例差距过大，面临严重的类别不平衡问题；[3] - 恶心样本多种多样，如昆虫，爬行动物，人的各个身体部位等等，样本的特征分布非常分散. 针对第一个问题，我们通过本团队的小样本平台从淘宝内容池中召回恶心图片扩充样本量，并通过半监督算法利用淘宝的无标签数据进行训练. 针对第二个问题，我们在训练中通过 online hard example mining +级联的方法，并且通过 active learning和噪声样本识别算法加快模型迭代. 针对第三个问题，我们在模型中引入了attention机制，让模型focus在恶心的局部区域. 本文的二、三、四节分别论述了这三个解决方案. 模型于19年10月初在首猜上线，在集团的各个业务场景下累计召回了几万张恶心图片. 最新版的模型的精确率为95%，召回率为94%.二. 冷启动方案由于初始样本量比较少，面临冷启动的问题. 我们从首猜负反馈数据里拿到了几百张恶心图片，并通过本团队的小样本召回平台，用图像检索的方法从淘宝内容池中召回了几百张恶心图片，组成了最开始的训练样本. 为了防止过拟合，我们采用了轻量级的网络 mobilenet v2. 另外，我们通过半监督算法利用淘宝的无标签数据进行训练，提高模型的泛化能力. 具体思想为：一个网络的输出可以被认为是 $Y=\mathbf{f}_{\mathbf{w}}(X)$，其中 W 是网络参数，X 是输入图片. 一个好的模型应该足够鲁棒，无论是 W 的扰动(本文称为参数扰动)还是X的扰动(本文称为 输入扰动)，输出都应该保持不变. 训练流程如下所示：对于有标签数据，对 student 网络的输出构造交叉熵损失函数，对于所有数据(无标签+有标签)，对student 网络和 teacher网络(两个网络结构一致)的输出构造 MSE 损失函数. 每次迭代的时候使用梯度下降法更新student网络参数并使得teacher网络的参数成为student网络参数的滑动指数平均. (具体可参考论文 Mean teachers are better rolemodels: Weight-averaged consistency targets improve semi-supervised deeplearning results - 2017)如下式所示：$$ \theta^{\prime}_t = \alpha \theta^{\prime}_{t-1} + (1-\alpha) \theta_t $$Github - CuriousAI/mean-teacher在参数扰动上，我们使用的是上述公式中的滑动指数平均. 在输入扰动上，我们对student网络和teacher网络的输入做了不同的数据增强. 我们使用了随机翻转，random resized crop,random erasing等方法，在后来召回来的 bad case中，发现有一部分召回来的负样本是化了妆的嘴唇、眼部、脸部等，这些区域和某些恶心图正样本比较相似. 因此尝试了random_distort 的方法,经过处理之后，类似的 bad case 基本消除.三. 算法迭代流程在线上数据中，正负样本的比例非常不均衡(远低于千分之一)，大部分都是各种各样的正常图片，只有少量的恶心图. 这意味着，即使模型在验证集上能达到99%的准确率，剩下的1%的错误也会在线上被无限放大，而且线上还有很多训练集里没出现过的负样本. 在模型版本快速的更替中，我们采取了 active learning，噪声样本识别，OHEM+级联的迭代流程，如下图所示. 3.1. active learning在每次的版本迭代过程中，模型都会召回一批数据. 在一开始数据量比较少的情况下，可以全部标注然后进行训练. 但是当数据变多了以后，一方面标注成本是非常巨大的，而另一方面根据curriculum learning的思想，应该让模型的学习由易到难，先学习简单的样本，然后逐渐学习更难的样本，提高难例样本在训练集的权重，这样能够加快模型的训练，找到更好的局部最优. 因此如果每次都将所有的召回样本全部标注投入训练，结果是吃力又不太讨好的. 在此我们借鉴active learning的思想，去找出有标注价值的难例样本. 样本选择策略有两方面：[1] -在每次召回来的样本中，选择置信度在 $\mathbf{P}_h$ 以上的和在 $\mathbf{P}_l$ 以下的样本进行标注，挑选出前者中的负样本和后者中的正样本；[2] - 在训练时引入损失预测模块，训练的时候预测各个样本的相对损失大小. 通过该模块对召回来的样本进行loss预测，并取出排名靠前的样本进行标注. 上面两个策略都是挑选出难例样本进行标注. 选择第一个策略是因为，一般情况下，我们希望模型对正样本预测的置信度越高越好，希望模型对负样本的置信度越低越好，而置信度在 $\mathbf{P}_h$ 以上的负样本和 $\mathbf{P}_l$ 以下的正样本对于模型来说都是难例样本. 实际中选择的阈值可以根据情况来进行调节. 选择第二个策略是因为，我们可以根据loss的大小来判断是否是难例样本，但是这些样本都没有现成的label，因此我们借鉴论文 Learning loss for active learning - CVPR2019 的方法，在训练的时候加入一个损失预测(loss prediction,以下简称LP)模块，来预测各个样本的loss.Github - Mephisto405/Learning-Loss-for-Active-LearningLP模块从 target prediction模块(简称TP)的一些中间层抽取特征(使其特征更加丰富)，组合起来用于loss预测. 训练流程为：每次一个batch进来B个样本(B为偶数)，将这B个样本拆分成B/2个样本对，分别送入 TP 模块和下面的 LP 模块. LP模块的loss如下所示：对于每个样本对，LP模块预测出来的loss的相对大小要与TP模块预测出来的相对大小保持一致，且绝对值要大于一定的阈值，才会使loss等于0. TP模块的 loss 在这里就是Ground Truth. 由于Ground Truth是随着训练不断变化的，直接对两个模块的输出计算MSE损失函数会导致效果非常差. 而且在做inference的时候，并不需要知道每个样本具体的loss大小，而只需要知道他们的loss的排名. 在实际场景运用中，我们同时运用了置信度选择和LP模块两个策略，并且在线上做了测试. 结果如下：每一行表示的是，从上一版的模型召回的数据中，用相应的策略选择样本，进行标注后加入训练，然后相比于上一版的模型的精确率提升量和召回率提升量(注：本文所有的提升量都是指的绝对值). 最后一行是指标注全部数据，|代表取并集. 从中可以看出，相比于最后一行，只需要标注30%的数据(25%的数据和置信度选择的数据取并集)，就获得大部分的精确率提升和召回数量提升. 也就是说，后续performance的提升主要来源于这些难例数据，而在这个过程中可以减少约70%的标注量.3.2. 噪声样本识别在解决业务问题时，我们总希望能获得高质量的训练样本，而从业务场景中拿到的数据都含有一定比例的噪声样本，可能是因为标注规则不清晰，培训不够完善，或者是有一些边界样本，或者是其他的一些原因，即使是外包同学标注的数据往往也存在误标的情况. 如下图所示，这些都被标注成了正样本，如果将这些数据送入模型训练，很容易造成误杀. 在实验中我们发现，如果不对噪声样本进行处理的话，无论怎么改进算法本身，在验证集的精度一直上不去，到达了一个瓶颈. 经过调研之后，我们发现，针对噪声样本问题，大致有以下四种解决方案：[1] - 多人标注后进行投票，这种方法能在根源上消除噪声样本，但是成本巨大；[2] - 训练时采取更大的batch size，这种方法能够在一定程度上降低噪声样本带来的影响，但是无法完全消除，而且容易受到计算资源的限制；[3] - 设计一个noise-robust model，这种方法一般会设计特殊的网络结构或者loss函数，来提高模型的抗噪性能，但是其设计往往过于复杂；[4] - 通过噪声样本识别方法来识别出噪声样本并进行校验，然后在干净的数据集重新训练. 最终，我们选择了上面第四种方案. 我们使用本团队发表在 ICCV2019 的论文 O2U-Net: A Simple Noisy Label Detection Approachfor Deep Neural Networks. 其基本思想为：在训练过程中循环调整学习率，使得模型在过拟合和欠拟合状态间反复切换. 突然增大学习率，使得网络跳出局部最优，进入欠拟合状态，此时噪声样本产生的loss较大，而干净样本的loss偏小，随着训练，学习率衰减，网络慢慢进入过拟合状态. 此时噪声样本和干净样本的loss都很小. 在此过程中噪声样本的loss的均值和方差都比干净样本大. 论文中学习率和样本loss的周期性变化如下图所示：网络的训练分为三步：[1] - pre-training阶段. 学习率设为正常值，训练直到在验证集的accuracy达到稳定. [2] - 循环学习率阶段. [3] - 对疑似噪声样本进行校验，在干净的数据集上重新训练. 我们对loss均值排名前5%的疑似噪声数据重新进行了标注. 发现其中有约71%的是错标的噪声样本(其他的是难例样本)，将这些噪声样本重新标注后加入训练集训练，在验证集的精度提高了0.74%，在线上的精确率提高了约8%. 从此可以看出，验证集的一个小的进步可以在线上改进很多，反之一个小的错误在线上会被放大很多倍.[淘宝噪声标签自动识别算法转] - AIUAI](https://www.aiuai.cn/aifarm1260.html)3.3. OHEM +级联在数据量达到一定的规模之后，此时mobilenet_v2的performance表现非常疲软，在线上的结果表明，densenet的精确率比mobilenet_v2高5个百分点，于是我们果断切到了densenet. 另一方面，面临样本不平衡问题，如何保证精确率达到90%以上，同时要保证召回率，就构成一个挑战. 我们调研了一些处理样本不平衡问题的方法. 其中，我们注意到目标检测领域的经典的 adaboost+cascade 的算法. 目标检测领域里会生成很多候选区域，所以会产生很多负样本. 用 adaboost 算法构建强分类器，然后将多个分类器级联就可以使大部分正样本通过的同时拒绝掉几乎所有的负样本. 在我们的业务场景下，我们采用了OHEM(online hard example mining)+级联的方法，如下图所示：在训练的时候，第一个网络densenet161的batch size为A，然后将其 loss 最大的B个样本，送入到第二个网络 resnet50 进行训练. 这样每次第二个网络学习到的是第一个网络觉得比较难的样本. 在inference的时候，对两个模型的结果取与. 第二个网络选择resnet50是因为其表现优异，在验证集的精度只比densenet161 低 0.2%，之所以不选择同系列更大的网络是为了防止过拟合. 另一方面，OHEM算法本身就可以处理样本不平衡问题，因此不需要像某些two stage的目标检测算法，在第一个网络中设置1:3的超参数来进行欠采样. 采用级联的方法后，在线上的精确率和召回率的变化见下表. 相比于单网络densenet161，精确率提高了21%，召回率降低了4.3%. 四. attention机制恶心图大致可以分为三类. 第一种类型是跟动物有关的，包括各种令人感到惊悚的爬行动物(从无腿的到无数条腿的都有)，昆虫，以及一些颜色鲜红的死体活体，动物内脏等. 第二种类型，是跟人有关的，展示的是人体的各个丑陋、病态的部位. 如过度肥胖，脏牙齿，脸部、鼻子、身体、手脚等各种皮肤问题. 第三种是其他类型，即一些物体，包括惊悚的衣服、模型，脏乱的洗衣机、房间，马桶等. (下图高能，可直接跳过) 恶心样本多种多样，样本的特征分布非常分散. 我们分析样本后观察到，大部分图片都是某个或者某几个局部区域表现出令人恶心的特征，而不是全局都呈现出恶心的特征. 所以针对各种各样的恶心图，如何让模型focus到这些局部的恶心区域，对于提高模型的performance和泛化能力至关重要. 我们首先调研了论文 Learning Deep Features for Discriminative Localization - CVPR2016，作者将分类网络中最后的全连接层换成了GAP(Global average pooling)层，针对某一个类别，将最后一次卷积操作得到的feature map与其权重进行累加求和，于是就得到了这个类别的class activation map(CAM)，如下图所示：CAM反映了这个类别关注图片中的哪个区域. 那么反过来，如果我们能够调整各个feature map的权重，对每个通道进行加权，那么就能够调整网络所关心的区域. 另一方面，卷积操作本身涉及到通道和空间两个维度，我们不仅可以对每个通道的特征进行加权，也可以对每个空间位置的特征进行加权. 因此我们采用了下图的方案. 我们在网路的基础block中嵌入了上图中的attention block. 其中，每个attention block由一个channel attention module和一个spatial attention module组成. (具体实现细节可参考 CBAM:Convolutional Block Attention Module]()). 我们使用Grad-CAM的方法，利用梯度信息对网络关注的特征进行了可视化，如下图所示(高能，可直接跳过)，反映出模型focus在恶心的区域上. 五. 业务结果目前，我们的恶心图模型已经在首猜上线，持续扫描首猜的增量商品池. 主要取得的业务结果如下：[1] - 在今年双十一的前两天，对首猜的全量商品池进行扫描，为双十一期间消费者购物体验的提升作出了贡献；[2] - 对便宜好货和阿里妈妈的广告图片进行扫描，运营根据扫描结果进行手动商品下线；[3] - 最新一版的模型在首猜达到了95%的精确率，召回率为94%. 目前，恶心图的精确率和召回率都达到了要求. 在图像质量维度上，淘宝对素材质量有以下诉求：[1] - 电商图像信息表达准确度要高(卖家秀和买家秀差异吐槽，误导购买的平台风险)[2] - 高质量图片的转化效率更高；[3] - 不同场景运营标准和颗粒度差异. 因此，我们打造了一个水滴平台，对于各个业务方关注的图像质量问题，比如清晰度、牛皮癣、拼接图、白底图、多主体、插画图、纯色背景图、低俗、恶心图等等，利用各任务间的共享特征、以及海量的无标签数据，通过 multi-task learning 提高图像任务的识别精度同时加快模型迭代的效率、减少数据标注量. 目前已用于素材质量检测、基于视觉的自动化测试、基础处理(空窗、天坑、错误页)等方面.参考资料[1] - MobileNetV2: Inverted Residuals and Linear Bottlenecks[2] - Mean teachers are better role models:Weight-averaged consistencytargets improve semi-supervised deep learning results[3] - Learning Loss for Active Learning[4] - O2U-Net: A Simple Noisy Label Detection Approach for Deep NeuralNetworks[5] - Training Region-Based Object Detectors With Online Hard ExampleMining[6] - Deep Residual Learning for Image Recognition[7] - Densely Connected Convolutional Networks
原文：每天审核淘宝性感图的工程师，竟然还做了这个 - 2019.09.11出处：淘系技术 - 微信公众号作者：胡佳洁(佳婕) 、黄锦池(尘漠)，曲烈(汤问)出品：阿里巴巴新零售淘系技术部论文：O2U-Net: A Simple Noisy Label Detection Approach for Deep Neural Networks - ICCV2019导读：获取高置信标注的大规模数据集是有监督学习算法的一个难点问题，训练集中的噪声标签会严重降低模型的精度. 通过所提出的噪声标签自动识别算法，无需人工干涉就可获取高质量的干净数据集，可以充分发挥海量弱标签数据的潜力，并提升模型的精度. 本文相关工作收录于 ICCV2019(IEEE International Conference on Computer Vision，CV 领域三大顶会之首)，详细介绍了淘系技术部算法团队提出的一种简单、高效的噪声标签识别算法，只需调节训练时的学习率，就可以让 90% 的噪声标签原形毕露.1. 背景训练数据的规模及其标注质量对有监督学习算法的性能影响重大. 互联网上虽然有海量的可爬取的数据，但这些按照标签语义收集来的数据往往带有大量噪声. 因此，要在这些弱标签数据上训练出高性能的深度神经网络模型，研究人员往往面临着从噪声中提取足够多有用信息的挑战. 本文将介绍一种简单高效的噪声标签自动识别算法 ，只需设置学习率的变更策略就能识别出 90% 的噪声样本，进一步提高训练模型的精度和收敛速度. 通常基于 human-supervision 的标签净化方法中，为保障训练数据的质量往往需要投入高昂的成本. 著名的图像数据集 ImageNet 有 120 万带标数据，标注过程中李飞飞实施多轮多人带验证题的复杂标注策略，才确保了数据集的质量. 但在实际业务中我们不可能投入如此巨大的成本，外包标注的数据往往是单次打标，许多因素会导致我们拿到的训练集存在不同程度的噪声，例如下图是外包对性感图任务打标结果的截图，对类似的图片标注结果完全相反，用这批带噪数据进行训练得到的模型精度无法达到上线要求. 因此，对高质量数据的需求与低效率的人工标注，构成了广大算法攻城狮们在模型开发的初级阶段所要解决的主要矛盾. 针对噪声问题，一味地调整模型和算法治标不治本. 我们在淘宝内容库类目分类识别的业务问题中发现，如果训练数据的精度不到 80% ，那么训练出来的模型精度是 72% 左右，无论模型怎么调整，精度只有 2%～3% 的提高，无法达到上线要求，因此还是需要从数据源头上解决问题. 依赖 human supervision 的方式净化数据集需要耗费大量的时间，让多个外包统一打标的边界也需要付出很大的沟通成本. 为提高业务效率，我们沉淀出一套简单高效易移植的噪声样本识别算法，自动找出可能是噪声的样本，并通过剔除可疑噪声数据在干净训练集上训练以提高模型精度. 在解决业务问题中，噪声样本识别算法给我们带来了以下两方面显著的好处：[1] - 提高标注效率. 通过算法找出最可疑的样本，只需要对筛选出来的最可疑的样本进行二次审核，减少标注人员的工作量，也能大幅提高迭代速率. [2] - 提高模型精度. 如果我们的噪声样本识别精度足够高，甚至识别精度达到95%以上，我们甚至不需要对可疑的噪声样本做人工复审，直接剔除掉训练模型以提高精度. 接下来，我们将详细介绍噪声样本标签识别算法，相关工作已被计算机视觉领域的顶会ICCV2019接收《O2U-Net: A Simple Noisy Label Detection Approach for Deep Neural Networks》.2. 解决思路我们调研了在噪声样本识别、抗噪模型等领域的前沿技术. 以下是针对 state-of-the-art paper 的调研分析. [1] - 《Understanding Black-box Predictions via Influence Functions》 论文主要利用 influence function 识别出噪声样本. 算法计算每个训练样本对验证集 loss 变化影响，如果某个训练样本对于验证数据集的精度起到负影响，那么就可能是不好的样本，这些样本可能包含了比较多的噪声样本. 论文的创新点在于提出了计算每个训练样本对当前模型性能的影响大小的方法，即 influence function . 对于影响为负值的训练样本，且该绝对值很大时，那么该样本就不是好的样本. [2] - 《CurriculumNet: Weakly Supervised Learning from Large-Scale Web Images》文章采用课程学习的思想：人类学习知识的过程是从简单到难，读书教学，我们的学习过程都是从简单的课程到复杂的课程. 分成四个阶段训练：[a] - 初始学习，把所有的训练样本仍进去，得到初始网络模型 first_model ;[b] - 根据初始网络模型，来判断每个训练样本学习的难易程度，把训练数据分成：容易学习样本集 A 、难学习样本集 B . 判断难易程度的方法，就是 paper 的创新点，主要思路是采用模型抽取出来的特征向量来聚类，来判断每个样本是否是离群，以此判断学习的难易程度. [c] - 课程学习：把容易学习的样本集 A ，在 first_model 模型的基础上继续训练得到第二阶段的模型 second_model. [d] - 把 A+B 混合在一起，在 second_model 的基础上继续训练，得到最终的模型. 之所以把 B 样本重新混到所有的样本里面重新训练，主要是因为B样本不全是噪声样本，干净的样本占了比例比较大，如果强行扔掉 B 数据集，那么对模型精度会比 first_model 得到的精度还低，主要原因是因为被误杀的干净样本，往往是很重要的，这些样本对于模型精度的提升有很大的贡献. [3] - 《MentorNet: Regularizing Very Deep Neural Networks on Corrupted Labels》文章提出了一种在数据维(data dimension)中正则化深度 CNN 的全新技术，文中称之为数据正则化(data regularization). 目标是通过正则化在有损标签上训练的 CNN 来提升其在清洁测试数据上的泛化表现. 具体来说，提出用于训练该分类网络(即 StudentNet)的每个样本学习随时间变化的权重，并引入了 MentorNet 来监督该 StudentNet 的训练. ，MentorNet 学习为每个训练样本分配一个权重. 通过学习不均衡的权重，MentorNet 鼓励某些样本学得更早，并且得到更多注意，由此对学习工作进行优先级排列. 对于 MentorNet 训练，文章首先预训练一个 MentorNet 来近似得到有标签数据中特定的一些预定义权重. 然后在具有清洁标签的第三个数据集上对它进行微调. 在测试的时候，StudentNet 独自进行预测，不使用 MentorNet.3. 算法设计3.1. 算法思路及测评指标以上方法通过判断样本的难易程度，设计不同学习阶段进行网络训练. 受此启发，我们在ICCV2019的论文《O2U-Net: A Simple Noisy Label Detection Approach for Deep Neural Networks》中提出了噪声样本识别的算法. 我们知道，在一次训练中，随着迭代轮次增加，网络逐渐从欠拟合逐渐过渡到过拟合状态，在训练的初期，模型精度的提升是非常明显的，因为网络很快学会了那部分“简单的“样本，因此这类样本的 loss 比较小，与之相反，那些“困难的”样本通常在训练的后期才逐渐被学会. 有论文指出如果训练的轮次足够多，巨大参数量使得模型能记住几乎所有的样本，因此过拟合训练数据中的噪声常常会让模型的表现变差. 观察训练过程发现，噪声样本通常是在训练的后期才被学会，因而在训练的早期，噪声样本的平均 loss 是远大于干净样本的，而在训练的后期，因为网络逐渐学会了所有样本，两类样本的loss区别不大. 纵观整个训练过程，从欠拟合到过拟合，噪声样本loss的均值和方差都比干净样本要大. 实验发现，只统计一次训练过程中不同样本的 loss 分布得到的结论不够可信，易造成对正常难样本的误杀，为了提高算法的鲁棒性，我们通过循环学习率策略，使网络在欠拟合和过拟合之间多次切换，并追踪不同阶段不同训练参数的模型对样本的 loss ，通过在时间维度上捕获多样性足够丰富的模型(类似集成学习，对满足多样性和准确性的多个模型进行 ensemble )，统计各个样本 loss 的均值和方差，均值和方差越大，样本属于噪声样本的概率也就越大. 大量实验表明，加入这个 trick 后，此算法在噪声样本识别和抗噪学习方面均取得了 outperform 当前其他算法的性能. 该算法可分为三个阶段：[1] - stage1: 训练得到收敛的模型. 超参数选择时，学习率设为固定值即可，训练可能会达到过拟合状态. [2] - stage2: 循环学习率阶段. 学习率周期性衰减，学习率突然增大，使得网络跳出过拟合状态，欠拟合时噪声样本产生的 loss 较大，而干净样本易于学习，loss 值偏小，随着训练进行学习率减小，网络慢慢进入过拟合阶段，能正确拟合噪声样本和干净样本，此时两类样本带来的 loss 都比较小. 通过不连续周期循环学习率来训练模型，模型在过拟合和欠拟合之间反复切换，在这个过程中，噪声样本 loss 的均值和方差均比干净样本高，通过多轮切换，可以基于 loss 筛选出数据中的疑似噪声样本. 下图展示了我们的实验过程中，当学习率周期震荡时，噪声样本(绿色曲线)的loss也会出现周期性震荡，而干净样本(黄色曲线)的 loss 总是比较小，在论文《O2U-Net: A Simple Noisy Label Detection Approach for Deep Neural Networks》中有更详尽的实验结果分析. [3] - stage3: 在干净数据集训练. 剔除算法识别出的疑似噪声样本后，在干净数据集上重新训练模型. 我们使用以下两个指标评价算法的质量：噪声识别精度指标(1)PR 曲线:主要是通过对噪声样本，根据不同的阈值，可视化绘制 PR 曲线：精确度与召回率曲线​ (2)根据固定的阈值，评估精确度：如果训练数据集噪声比例为 10% ，那么就召回 10% 最有可能为标错的样本，然后统计召回精度(精确度)抗噪模型精度指标​ 把可疑的噪声样本剔除，对模型进行重新训练，在干净的测试集上，计算模型精度提高的百分点.3.2. 算法性能基于上文构造的噪声样本数据集，对比了所提算法和几种 state-of-the-art 算法的性能. 下表展示了不同抗噪模型精度指标：在基于 resnet101 和浅层的 9 层 CNN 模型中，我们所提算法的抗噪性能在不同数据集和不同的噪声比例上，都较以往的方法有了较大提升. 下表是几种不同算法对样本集中噪声标签的识别精度，在不同数据集和不同的噪声比例上，我们的方法都能更准确地识别出噪声，我们把这归功于，算法集成了同一网络结构在各种不同训练阶段、大量不同参数的情况下，对样本对 loss 的分布进行统计，因此能准确识别出疑似噪声样本. 并且算法不需要集成多个模型，仅仅是通过调整学习率来控制某一个模型的进入不同状态，因此在算法实现起来比较容易，针对各种模型的可迁移性也很强. 算法同学在解决业务问题时，总希望能获得高质量的训练样本，而从业务场景中拿到的数据大多需要重新进行标注，有时由于标注规则不清晰，即使是外包同学标注的数据往往也存在误标现象. 在训练流程之前，先使用噪声标签识别算法将疑似噪声样本挑选出来，直接剔除这些样本或对其进行二次打标，可以快速训练样本的质量. 例如，我们在解决图片里多主体的任务时，拿到的原始数据集噪声比例约为 15% ，通过算法我们找出了 5k 个单主体的样本集中，为噪声样本概率最大的 30 个样本. 从召回的噪声样本中可以看出， top30 全部都是多主体的样本，在 top 100 的噪声样本中，我们算法识别的精度约为 93% . 当正负样本的距离很接近时，例如在对商品图进行肢体检测时，对包含肢体的数据集进行噪声样本检测，在 top30 的疑似噪声样本中，也包含了一个正确的样本，这也是后续优化需要解决的问题. 4. 应用场景人工标注带来的样本噪声问题不可避免，清洗样本获得干净的数据集，是许多算法同学需要解决的问题，自己多轮打标剔除噪声样本需要掌握分类规则，也需要耗费大量时间精力. 而我们提出的噪声样本自动识别算法，大大节约了我们的进行样本筛选的成本，有效帮助我们在解决在各种业务问题时提高效率. 除此之外，在验收外包同学的标注数据时，也可以通过噪声样本识别算法来判断标注精度是否达到一定标准. 噪声样本识别能力和相似样本召回能力，有力地帮助我们解决了许多图像质量相关的业务问题，并在业务过程中积累了大量的干净样本. 目前我们已具备的质量能力主要包含：牛皮癣、软色情、模糊图、白底图、纯色背景图、优质图识别、真实场景图、多主体、拼接图、是否包含肢体、插画素材图等多个维度的检测判断，也实现了数据和算法相互优化. 牛皮癣、软色情、清晰度、白底图、纯色背景图、多主体识别等，均已上线用于淘宝内容库封面图审核过滤，过滤精度均超过 90% .5. 参考文献1 - Classification in the Presence of Label Noise: A Survey2 - Co-teaching: Robust Training of Deep Neural Networks with Extremely Noisy labels3 - Mentor-Net: Learning Data-Driven Curriculum for Very Deep Neural Networks on Corrupted Labels4 - Understanding Black-box Predictions via Influence Functions5 - Making Deep Neural Networks Robust to Label Noise: A Loss Correction Approach6 - Learning from Massive Noisy Labeled Data for Image Classification7 - A Closer Look at Memorization in Deep Networks8 - Training Deep Neuralnetwork Using a Noise Adaptation Layer9 - CurriculumNet: Weakly Supervised Learning from Large-Scale Web Images10 - CleanNet: Transfer Learning for Scalable Image Classifier Training With Label Noise
计算机视觉八大任务全概述：PaddlePaddle工程师详解热门视觉模型[转]
原文： 计算机视觉八大任务全概述：PaddlePaddle工程师详解热门视觉模型 - 2019.03.22出处：量子位 - 微信公众号本文转载自 PaddlePaddle.编者按：来自百度的深度学习工程师，围绕计算机视觉领域的八大任务，包括：图像分类、目标检测、图像语义分割、场景文字识别、图像生成、人体关键点检测、视频分类、度量学习等，进行了较为详细的综述并形成此文. 这篇综述中，介绍了这些任务的基本情况，以及目前的技术进展、主要的模型和性能对比等. 而且还逐一附上了GitHub传送门，用于更进一步的学习与安装实践指南. 其中不少教程还是用中文写成，非常友好. 总之，这篇综述全程干货，推荐收藏阅读. 计算机视觉（Computer Vision）是研究如何使机器“看”的科学，更进一步的说，是使用摄像机机和电脑代替人眼对目标进行识别、跟踪和测量等的机器视觉，并通过电脑处理成为更适合人眼观察或传送给仪器检测的图像. 形象地说，就是给计算机安装上眼睛（摄像机）和大脑（算法），让计算机像人一样去看、去感知环境. 计算机视觉技术作为人工智能的重要核心技术之一，已广泛应用于安防、金融、硬件、营销、驾驶、医疗等领域. 本文中，一方面，介绍基于PaddlePaddle的计算机视觉技术在图像分类、目标检测、图像语义分割、场景文字识别四大基本任务场景下，如何帮助计算机从单个或者一系列的图片中提取分析和理解的关键信息及其相关的深度学习模型. 另一方面，视频在人们生活中越来越重要，伴随着技术成熟度的提高，计算机视觉技术的突破也从静态图像识别的“看到”转向了视频理解的“看懂”，故还介绍了基于PaddlePaddle的图像生成、人体关键点检测、视频分类相关的深度学习模型.一. 图像分类图像分类是根据图像的语义信息对不同类别图像进行区分，是计算机视觉中重要的基础问题，是物体检测、图像分割、物体跟踪、行为分析、人脸识别等其他高层视觉任务的基础. 图像分类在许多领域都有着广泛的应用. 如：安防领域的人脸识别和智能视频分析等，交通领域的交通场景识别，互联网领域基于内容的图像检索和相册自动归类，医学领域的图像识别等.1.1. 模型结构得益于深度学习的推动，图像分类的准确率大幅度提升. 在经典的数据集ImageNet上，训练图像分类任务常用的模型，包括AlexNet、VGG、GoogLeNet、ResNet、Inception-v4、MobileNet、MobileNetV2、DPN(Dual Path Network)、SE-ResNeXt、ShuffleNet等. △ AlexNet△ VGG△ GoogLeNet△ ResNet△ Inception-v4△ MobileNet△ SE-ResNeXt△ ShuffleNet1.2. 精度对比PaddlePaddle - 预训练模型下载，及 PaddlePaddle Fluid 文档：(还包括将Caffe模型转换为PaddlePaddle Fluid模型配置和参数文件的工具. )https://github.com/PaddlePaddle/models/blob/develop/fluid/PaddleCV/image_classification模型的结构和复杂程度都不一样，最终得到的准确率也有所区别. 下面这个表格中，列出了在ImageNet 2012数据集上，不同模型的top-1/top-5验证准确率.modeltop-1/top-5 accuracy(PIL)top-1/top-5 accuracy(CV2)AlexNet56.71%/79.18%55.88%/78.65%VGG1169.22%/89.09%69.01%/88.90%VGG1370.14%/89.48%69.83%/89.13%VGG1672.08%/90.63%71.65%/90.57%VGG1972.56%/90.83%72.32%/90.98%MobileNetV170.91%/89.54%70.51%/89.35%MobileNetV271.90%/90.55%71.53%/90.41%ResNet5076.35%/92.80%76.22%/92.92%ResNet10177.49%/93.57%77.56%/93.64%ResNet15278.12%/93.93%77.92%/93.87%SE_ResNeXt50_32x4d78.50%/94.01%78.44%/93.96%SE_ResNeXt101_32x4d79.26%/94.22%79.12%/94.20%△ 图像分类系列模型评估结果二. 目标检测目标检测任务的目标是给定一张图像或是一个视频帧，让计算机找出其中所有目标的位置，并给出每个目标的具体类别. 对于人类来说，目标检测是一个非常简单的任务. 然而，计算机能够“看到”的是图像被编码之后的数字，很难解图像或是视频帧中出现了人或是物体这样的高层语义概念，也就更加难以定位目标出现在图像中哪个区域. 与此同时，由于目标会出现在图像或是视频帧中的任何位置，目标的形态千变万化，图像或是视频帧的背景千差万别，诸多因素都使得目标检测对计算机来说是一个具有挑战性的问题. 在目标检测任务中，主要介绍如何基于PASCAL VOC、MS COCO数据训练通用物体检测模型，包括SSD模型、PyramidBox模型、R-CNN模型.2.1. SSD模型Single Shot MultiBox Detector，是一种单阶段的目标检测器. 与两阶段的检测方法不同，单阶段目标检测并不进行区域推荐，而是直接从特征图回归出目标的边界框和分类概率. SSD 运用了这种单阶段检测的思想，并且对其进行改进：在不同尺度的特征图上检测对应尺度的目标，是目标检测领域较新且效果较好的检测算法之一，具有检测速度快且检测精度高的特点. △ SSD目标检测模型结构△ SSD目标检测可视化SSD 模型下载、训练与使用：https://github.com/PaddlePaddle/models/blob/develop/fluid/PaddleCV/object_detectionModelPre-trained ModelTraining dataTest datamAPMobileNet-v1-SSD 300x300COCO MobileNet SSDVOC07+12 trainvalVOC07 test73.32%△ 目标检测SSD模型评估结果2.2. PyramidBox模型PyramidBox模型 是百度自主研发的人脸检测模型，是一种语境辅助的单次人脸检测新方法，能够解决在不受控制的环境中检测小的、模糊的及部分遮挡的人脸时的问题，模型于18年3月份在WIDER Face数据集上取得第一名. △ Pyramidbox 人脸检测模型 △ Pyramidbox 预测可视化Pyramidbox 模型(全中文文档)：https://github.com/PaddlePaddle/models/tree/develop/fluid/PaddleCV/face_detection模型预训练模型训练数据测试数据mAPPyramidbox-v1-SSD 640x640VGGNetWIDER FACE trainWIDER FACE Val96.0%/ 94.8%/ 88.8%△ PyramidBox模型评估结果2.3. R-CNN系列模型区域卷积神经网络（R-CNN）系列模型是典型的两阶段目标检测器，相较于传统提取区域的方法，R-CNN 中 RPN 网络通过共享卷积层参数大幅提高提取区域的效率，并提出高质量的候选区域. Faster R-CNN和Mask R-CNN是R-CNN系列的典型模型. Faster R-CNN是区域生成网络(RPN) + Fast R-CNN的实现，将候选区域生成、特征提取、分类、位置精修统一到一个深度网络框架，大大提高运行速度. Mask R-CNN在原有Faster R-CNN模型基础上添加分割分支，得到掩码结果，实现了掩码和类别预测关系的解藕，是经典的实例分割模型. △ Faster R-CNN 结构△ Faster R-CNN 预测可视化△ Mask R-CNN结构△ Mask R-CNN 预测可视化R-CNN安装、准备、训练等：https://github.com/PaddlePaddle/models/tree/develop/fluid/PaddleCV/rcnnModelRoI functionBatch sizeMax iterationmAPFluid RoIPool minibatch paddingRoIPool81800000.316Fluid RoIPool no paddingRoIPool81800000.318Fluid RoIAlign no paddingRoIAlign81800000.348Fluid RoIAlign no padding 2xRoIAlign83600000.367△ Faster R-CNN评估结果ModelBatch sizeMax iterationbox mAPmask mAPFluid mask no padding81800000.3590.314△ Mask R-CNN评估结果2.4. YOLOV3 模型YOLOv3 是一阶段End2End的目标检测器。其目标检测原理如下图所示:△ YOLOv3 检测原理YOLOv3 将输入图像分成 SxS 个格子，每个格子预测 B 个bounding box，每个bounding box预测内容包括: Location(x, y, w, h)、Confidence Score和C个类别的概率，因此 YOLOv3 输出层的 channel 数为SxSxBx(5 + C). YOLOv3 的 loss 函数也有三部分组成：Location误差，Confidence误差和分类误差.YOLOv3的网络结构如下图所示:△ YOLOv3 网络结构YOLOv3 的网络结构由基础特征提取网络、multi-scale特征融合层和输出层组成。[1] - 特征提取网络YOLOv3使用 DarkNet53作为特征提取网络：DarkNet53 基本采用了全卷积网络，用步长为2的卷积操作替代了池化层，同时添加了 Residual 单元，避免在网络层数过深时发生梯度弥散。[2] - 特征融合层为了解决之前YOLO版本对小目标不敏感的问题，YOLOv3采用了3个不同尺度的特征图来进行目标检测，分别为13x13,26x26,52x52,用来检测大、中、小三种目标。特征融合层选取 DarkNet 产出的三种尺度特征图作为输入，借鉴了FPN(feature pyramid networks)的思想，通过一系列的卷积层和上采样对各尺度的特征图进行融合。[3] - 输出层同样使用了全卷积结构，其中最后一个卷积层的卷积核个数是255：3*(80+4+1)=255，3表示一个grid cell包含3个bounding box，4表示框的4个坐标信息，1表示Confidence Score，80表示COCO数据集中80个类别的概率。△ YOLOv3 可视化检测结果PaddlePaddle YOLOV3 实现：https://github.com/PaddlePaddle/models/tree/develop/fluid/PaddleCV/yolov3input size608x608416x416320x320infer speed50 ms/frame29 ms/frame24 ms/frame三. 图像语义分割图像语义分割，顾名思义是将图像像素按照表达的语义含义的不同进行分组/分割. 图像语义是指对图像内容的理解，例如，能够描绘出什么物体在哪里做了什么事情等，分割是指对图片中的每个像素点进行标注，标注属于哪一类别. 近年来用在无人车驾驶技术中分割街景来避让行人和车辆、医疗影像分析中辅助诊断等. 分割任务主要分为实例分割和语义分割.实例分割是物体检测加上语义分割的综合体.Mask R-CNN是实例分割的经典网络结构之一. 在图像语义分割任务中，主要介绍兼顾准确率和速度的 ICNet，DeepLab中最新、执行效果最好的DeepLab v3+.3.1. DeepLab v3+DeepLab v3+，DeepLab语义分割系列网络的最新作，通过encoder-decoder进行多尺度信息的融合，同时保留了原来的空洞卷积和ASSP层， 其骨干网络使用了Xception模型，提高了语义分割的健壮性和运行速率，在 PASCAL VOC 2012 dataset取得新的state-of-art performance，89.0mIOU. △ DeepLab v3+ 基本结构DeepLab v3+ - PaddlePaddle 实现：https://github.com/PaddlePaddle/models/tree/develop/fluid/PaddleCV/deeplabv3%2B数据集norm typepretrained modeltrained modelmean IoUCityScapebatch normdeeplabv3plus_xception65_initialize.tgzdeeplabv3plus.tgz0.7873CityScapegroup normdeeplabv3plus_gn_init.tgzdeeplabv3plus_gn.tgz0.7881△ DeepLab v3+ 评估结果3.2. ICNetICNet, Image Cascade Network，主要用于图像实时语义分割，主要思想是将输入图像变换为不同的分辨率，然后用不同计算复杂度的子网络计算不同分辨率的输入，然后将结果合并. ICNet由三个子网络组成，计算复杂度高的网络处理低分辨率输入，计算复杂度低的网络处理分辨率高的网络，通过这种方式在高分辨率图像的准确性和低复杂度网络的效率之间获得平衡. △ ICNet网络结构△ ICNet可视化ICNet - PaddlePaddle 实现(中文文档)：https://github.com/PaddlePaddle/models/tree/develop/fluid/PaddleCV/icnet数据集pretrained modelmean IoUCityScapepretrained_model0.67△ ICNet评估结果四. 场景文字识别许多场景图像中包含着丰富的文本信息，对理解图像信息有着重要作用，能够极大地帮助人们认知和理解场景图像的内容. 场景文字识别是在图像背景复杂、分辨率低下、字体多样、分布随意等情况下，将图像信息转化为文字序列的过程，可认为是一种特别的翻译过程：将图像输入翻译为自然语言输出. 场景图像文字识别技术的发展也促进了一些新型应用的产生，如通过自动识别路牌中的文字帮助街景应用获取更加准确的地址信息等. 在场景文字识别任务中，介绍如何将基于CNN的图像特征提取和基于RNN的序列翻译技术结合，免除人工定义特征，避免字符分割，使用自动学习到的图像特征，完成字符识别. 这里主要介绍CRNN-CTC 模型和基于注意力机制的序列到序列模型.4.1. CRNN-CTC模型CRNN-CTC模型, 采取CNN+RNN+CTC架构，卷积层使用CNN，从输入图像中提取特征序列、循环层使用RNN，预测从卷积层获取的特征序列的标签（真实值）分布、转录层使用CTC，把从循环层获取的标签分布通过去重整合等操作转换成最终的识别结果.4.2. 基于注意力机制的序列到序列模型基于注意力机制的序列到序列模型, 提出了基于attention机制的文本识别方法，不需要检测，直接输入图片进行识别，对于识别字符类别数很少的场景很实用，例如车牌识别、自然场景图片的主要关键词提取等. 同时也不要求识别文本必须单行排列，双行排列，多行排列都可以. 在训练过程中则不需要文本框的标注，训练数据的收集变得很方便. △ CRNN-CTC模型结构△ 基于注意力机制的序列到序列模型结构https://github.com/PaddlePaddle/models/tree/develop/fluid/PaddleCV/ocr_recognition模型错误率ocr_ctc_params22.3%ocr_attention_params15.8%△ OCR模型评估结果五. 图像生成图像生成是指根据输入向量，生成目标图像. 这里的输入向量可以是随机的噪声或用户指定的条件向量. 具体的应用场景有：手写体生成、人脸合成、风格迁移、图像修复、超分重建等. 当前的图像生成任务主要是借助生成对抗网络（GAN）来实现. 生成对抗网络（GAN）由两种子网络组成：生成器和识别器. 生成器的输入是随机噪声或条件向量，输出是目标图像. 识别器是一个分类器，输入是一张图像，输出是该图像是否是真实的图像. 在训练过程中，生成器和识别器通过不断的相互博弈提升自己的能力. 在图像生成任务中，主要介绍如何使用 DCGAN 和 ConditioanlGAN 来进行手写数字的生成，另外还介绍了用于风格迁移的CycleGAN.5.1. ConditioanlGANConditioanlGAN ，顾名思义是带条件约束的生成对抗模型，它在生成模型和判别模型的建模中均引入了额外的条件变量，对于生成器对数据的生成具有指导作用. ConditioanlGAN是把无监督的GAN变成有监督模型的改进，为后续的工作提供了指导作用. △ ConditioanlGAN结构△ ConditioanlGAN预测效果图中文文档:https://github.com/PaddlePaddle/models/tree/develop/fluid/PaddleCV/gan/c_gan5.2. DCGANDCGAN，为了填补CNN在监督学习和无监督学习之间的gap，此篇论文提出了将CNN和GAN结合的DCGAN(深度卷积生成对抗网络)，并且DCGAN在无监督学习中取得不错的结果. △ DCGAN结构△ DCGAN预测效果图中文文档：https://github.com/PaddlePaddle/models/tree/develop/fluid/PaddleCV/gan/c_gan5.3. CycleGANCycleGAN，将一类图片转换成另一类图片. 传统的 GAN 是单向生成，CycleGAN 是互相生成，本质上是两个镜像对称的GAN，构成了一个环形网络，所以命名为 Cycle. 风格迁移类任务一般都需要两个域中具有相同内容的成对图片作为训练数据，CycleGAN的创新点就在于其能够在没有成对训练数据的情况下，将图片内容从源域迁移到目标域. △ CycleGAN 结构△ CycleGAN预测可视化中文文档：https://github.com/PaddlePaddle/models/tree/develop/fluid/PaddleCV/gan/cycle_gan六. 人体关键点检测人体关键点检测，通过人体关键节点的组合和追踪来识别人的运动和行为，对于描述人体姿态，预测人体行为至关重要，是诸多计算机视觉任务的基础，例如动作分类，异常行为检测，以及自动驾驶等等，也为游戏、视频等提供新的交互方式. 在人体关键点检测任务中，主要介绍网络结构简单的coco2018关键点检测项目的亚军方案.6.1. Simple Baselines for Human Pose Estimation in FluidSimple Baselines for Human Pose Estimation in Fluid，coco2018关键点检测项目的亚军方案，没有华丽的技巧，仅仅是在ResNet中插入了几层反卷积，将低分辨率的特征图扩张为原图大小，以此生成预测关键点需要的Heatmap. 没有任何的特征融合，网络结构非常简单，但是达到了state of the art 效果.https://github.com/PaddlePaddle/models/tree/develop/fluid/PaddleCV/human_pose_estimationArchHeadShoulderElbowWristHipKneeAnkleMeanMean@0.1Models383x384_pose_resnet_50 in PyTorch96.65895.75489.79084.61488.52384.66679.28789.06638.046-383x384_pose_resnet_50 in Fluid96.24895.34689.80784.87388.29883.67978.64988.76737.374link△ Simple Baselines for Human Pose Estimation in Fluid 评估结果七. 视频分类视频分类是视频理解任务的基础，与图像分类不同的是，分类的对象不再是静止的图像，而是一个由多帧图像构成的、包含语音数据、包含运动信息等的视频对象，因此理解视频需要获得更多的上下文信息，不仅要理解每帧图像是什么、包含什么，还需要结合不同帧，知道上下文的关联信息. 视频分类方法主要包含基于卷积神经网络、基于循环神经网络、或将这两者结合的方法. 在视频分类任务中，主要介绍视频分类方向的多个主流领先模型，其中Attention LSTM，Attention Cluster 和 NeXtVLAD 是比较流行的特征序列模型，TSN 和 StNet 是两个End-to-End的视频分类模型. Attention LSTM模型速度快精度高，NeXtVLAD 是 2nd-Youtube-8M 比赛中最好的单模型, TSN是基于2D-CNN的经典解决方案. Attention Cluster 和 StNet是百度自研模型，分别发表于CVPR2018和AAAI2019，是Kinetics600比赛第一名中使用到的模型.7.1. Attention Cluster模型Attention Cluster模型为ActivityNet Kinetics Challenge 2017中最佳序列模型，通过带Shifting Opeation的Attention Clusters处理已抽取好的RGB、Flow、Audio数据. △ Attention Cluster模型结构Attention LSTM 模型，采用了双向长短记忆网络（LSTM），将视频的所有帧特征依次编码. 与传统方法直接采用LSTM最后一个时刻的输出不同，该模型增加了一个Attention层，每个时刻的隐状态输出都有一个自适应权重，然后线性加权得到最终特征向量. △ Attention LSTM 模型结构7.2. NeXtVLAD模型NeXtVLAD模型，第二届Youtube-8M视频理解竞赛中效果最好的单模型，提供了一种将桢级别的视频特征转化并压缩成特征向量，以适用于大尺寸视频文件的分类的方法. 其基本出发点是在NetVLAD模型的基础上，将高维度的特征先进行分组，通过引入attention机制聚合提取时间维度的信息，这样既可以获得较高的准确率，又可以使用更少的参数量. △ NeXtVLAD模型结构7.3. StNet模型StNet模型，框架为ActivityNet Kinetics Challenge 2018中夺冠的基础网络框架，提出“super-image”的概念，在super-image上进行2D卷积，建模视频中局部时空相关性. 另外通过temporal modeling block建模视频的全局时空依赖，最后用一个temporal Xception block对抽取的特征序列进行长时序建模. △ StNet模型结构7.4. Temporal Segment Network (TSN)Temporal Segment Network (TSN) ，视频分类领域经典的基于2D-CNN的解决方案，主要解决视频的长时间行为判断问题，通过稀疏采样视频帧的方式代替稠密采样，既能捕获视频全局信息，也能去除冗余，降低计算量. 最终将每帧特征平均融合后得到视频的整体特征，并用于分类. △ TSN模型结构中文文档：https://github.com/PaddlePaddle/models/tree/develop/fluid/PaddleCV/video模型Batch Size环境配置cuDNN版本GAP下载链接Attention Cluster20488卡P407.10.84modelAttention LSTM10248卡P407.10.86modelNeXtVLAD1604卡P407.10.87model△ 基于Youtube-8M数据集的视频分类模型 评估结果模型Batch Size环境配置cuDNN版本Top-1下载链接StNet1288卡P405.10.69modelTSN2568卡P407.10.67model△ 基于Kinetics数据集的视频分类模型 评估结果八. 度量学习度量学习也称作距离度量学习、相似度学习，通过学习对象之间的距离，度量学习能够用于分析对象时间的关联、比较关系，在实际问题中应用较为广泛，可应用于辅助分类、聚类问题，也广泛用于图像检索、人脸识别等领域. 以往，针对不同的任务，需要选择合适的特征并手动构建距离函数，而度量学习可根据不同的任务来自主学习出针对特定任务的度量距离函数. 度量学习和深度学习的结合，在人脸识别/验证、行人再识别(human Re-ID)、图像检索等领域均取得较好的性能，在这个任务中我们主要介绍基于Fluid的深度度量学习模型，包含了三元组、四元组等损失函数.https://github.com/PaddlePaddle/models/tree/develop/fluid/PaddleCV/metric_learningpretrain modelsoftmaxarcmarginwithout fine-tuned77.42%78.11%fine-tuned with triplet78.37%79.21%fine-tuned with quadruplet78.10%79.59%fine-tuned with eml79.32%80.11%△ 度量学习模型 评估结果
TensorFlow 提供了多种图片数据读取的方法 - TensroFlow - 数据读取[转] 基于 TFRecord Flowers 数据集 fine-tune InceptionV1 模型.TensorFlow 数据数据读取方法主要有： 直接从磁盘读取 - 在 train_op 运行会话Session 时，采用 feed_dict 传递数据. 但是对于大规模数据集可能不太适用，因为需要足够的 GPU 内存来存储训练数据. 从 CSV 文件读取 - 不适用于图片. 从 TFRecord 文件读取 - 将图片转化为 TensorFlow 可读取格式的 TFRecord 文件，在训练是不用再读取原始图像文件，具有更高的读取效率. 这里主要基于 TFRecord 处理大规模数据集. 虽然 TFRecord 文件的创建不如从 HDF5 格式读取数据的方式(如 Keras 采用的)直接，但这种方式更便于采用数据管道工具(data pipeline tools) 进行图片训练，比如 queue runners，coordinaors 和 supervisors，有益于训练数据流的管理.TensorFlow 提供了 TF-Slim 封装用于 TFRecord 文件的创建与读取 - slim/datasets. TensorFlow - TF-Slim 使用总览 - 有关于基于 TFRecord Flowers 数据集的模型(不包括 TFRecord 文件的创建) TensorFlow - TF-Slim Data TensorFlow - TF-Slim 封装模块 1. 创建 TFRecord Flowers 数据集Flowers 数据集下载 - Flowers Dataset解压后的目录结构为：flowers_photos/ |----daisy | -------- *.jpg (633 张) |----dandelion | -------- *.jpg (898 张) |----roses | -------- *.jpg (641 张) |----sunflowers | -------- *.jpg (699 张) |----tulips | -------- *.jpg (799 张) TensorFlow 提供了将 Flowers 数据集转换为 TFRecord 的脚本： dataset_utils.py """ 用于数据集下载和转换. """ from __future__ import absolute_import from __future__ import division from __future__ import print_function import os import sys import tarfile from six.moves import urllib import tensorflow as tf LABELS_FILENAME = 'labels.txt' def int64_feature(values): """Returns a TF-Feature of int64s. Args: values: A scalar or list of values. Returns: A TF-Feature. """ if not isinstance(values, (tuple, list)): values = [values] return tf.train.Feature(int64_list=tf.train.Int64List(value=values)) def bytes_feature(values): """Returns a TF-Feature of bytes. Args: values: A string. Returns: A TF-Feature. """ return tf.train.Feature(bytes_list=tf.train.BytesList(value=[values])) def float_feature(values): """Returns a TF-Feature of floats. Args: values: A scalar of list of values. Returns: A TF-Feature. """ if not isinstance(values, (tuple, list)): values = [values] return tf.train.Feature(float_list=tf.train.FloatList(value=values)) def image_to_tfexample(image_data, image_format, height, width, class_id): return tf.train.Example(features=tf.train.Features(feature={ 'image/encoded': bytes_feature(image_data), 'image/format': bytes_feature(image_format), 'image/class/label': int64_feature(class_id), 'image/height': int64_feature(height), 'image/width': int64_feature(width), })) def download_and_uncompress_tarball(tarball_url, dataset_dir): """Downloads the `tarball_url` and uncompresses it locally. Args: tarball_url: The URL of a tarball file. dataset_dir: The directory where the temporary files are stored. """ filename = tarball_url.split('/')[-1] filepath = os.path.join(dataset_dir, filename) def _progress(count, block_size, total_size): sys.stdout.write('\r>> Downloading %s %.1f%%' % ( filename, float(count * block_size) / float(total_size) * 100.0)) sys.stdout.flush() filepath, _ = urllib.request.urlretrieve(tarball_url, filepath, _progress) print() statinfo = os.stat(filepath) print('Successfully downloaded', filename, statinfo.st_size, 'bytes.') tarfile.open(filepath, 'r:gz').extractall(dataset_dir) def write_label_file(labels_to_class_names, dataset_dir, filename=LABELS_FILENAME): """ Writes a file with the list of class names. Args: labels_to_class_names: A map of (integer) labels to class names. dataset_dir: The directory in which the labels file should be written. filename: The filename where the class names are written. """ labels_filename = os.path.join(dataset_dir, filename) with tf.gfile.Open(labels_filename, 'w') as f: for label in labels_to_class_names: class_name = labels_to_class_names[label] f.write('%d:%s\n' % (label, class_name)) def has_labels(dataset_dir, filename=LABELS_FILENAME): """Specifies whether or not the dataset directory contains a label map file. Args: dataset_dir: The directory in which the labels file is found. filename: The filename where the class names are written. Returns: `True` if the labels file exists and `False` otherwise. """ return tf.gfile.Exists(os.path.join(dataset_dir, filename)) def read_label_file(dataset_dir, filename=LABELS_FILENAME): """Reads the labels file and returns a mapping from ID to class name. Args: dataset_dir: The directory in which the labels file is found. filename: The filename where the class names are written. Returns: A map from a label (integer) to class name. """ labels_filename = os.path.join(dataset_dir, filename) with tf.gfile.Open(labels_filename, 'rb') as f: lines = f.read().decode() lines = lines.split('\n') lines = filter(None, lines) labels_to_class_names = {} for line in lines: index = line.index(':') labels_to_class_names[int(line[:index])] = line[index+1:] return labels_to_class_names download_and_convert_flowers.py """ Flowers 数据集下载和转化为TFRecords 格式(TF-Example protos). Flowers 数据集的下载，解压，读取数据，创建两个 TFRecord 数据集：训练数据集和测试数据集. 每个数据集是由 TF-Example protocol buffers 构成，每个 TF-Example protocol buffer 包含一张图片和对应的标签. 该脚本大概需要耗时一分钟. """ from __future__ import absolute_import from __future__ import division from __future__ import print_function import math import os import random import sys import tensorflow as tf from datasets import dataset_utils # Flowers 数据集的 URL. _DATA_URL = 'http://download.tensorflow.org/example_images/flower_photos.tgz' # 验证数据集的图片数. _NUM_VALIDATION = 350 # Seed for repeatability. _RANDOM_SEED = 0 # The number of shards per dataset split. _NUM_SHARDS = 5 class ImageReader(object): """ 用于 TensorFlow 图片编码的辅助类 """ def __init__(self): # 初始化解码decode RGB JPEG 格式数据的函数. self._decode_jpeg_data = tf.placeholder(dtype=tf.string) self._decode_jpeg = tf.image.decode_jpeg(self._decode_jpeg_data, channels=3) def read_image_dims(self, sess, image_data): image = self.decode_jpeg(sess, image_data) return image.shape[0], image.shape[1] def decode_jpeg(self, sess, image_data): image = sess.run(self._decode_jpeg, feed_dict={self._decode_jpeg_data: image_data}) assert len(image.shape) == 3 assert image.shape[2] == 3 return image def _get_filenames_and_classes(dataset_dir): """ 返回文件名和类别名列表. Args: dataset_dir: 包含多个图片子路径的路径. class names. 每个图片子路径包含 PNG 或 JPG 编码的图片. Returns: 图片文件列表，相对于 `dataset_dir`； 图片子路经列表，表示类比名字. """ flower_root = os.path.join(dataset_dir, 'flower_photos') directories = [] class_names = [] for filename in os.listdir(flower_root): path = os.path.join(flower_root, filename) if os.path.isdir(path): directories.append(path) class_names.append(filename) photo_filenames = [] for directory in directories: for filename in os.listdir(directory): path = os.path.join(directory, filename) photo_filenames.append(path) return photo_filenames, sorted(class_names) def _get_dataset_filename(dataset_dir, split_name, shard_id): output_filename = 'flowers_%s_%05d-of-%05d.tfrecord' % ( split_name, shard_id, _NUM_SHARDS) return os.path.join(dataset_dir, output_filename) def _convert_dataset(split_name, filenames, class_names_to_ids, dataset_dir): """ 将给定文件名转换为 TFRecord 格式数据集. Args: split_name: 数据集的名字，train 或 validation. filenames: png 或 jpg 图片的绝对路径列表. class_names_to_ids: 类别名字(字符串strings) 到类别 ids(整数integers ) 映射的字典. dataset_dir: 转换后的 TFRecord 数据集所保存的路径. """ assert split_name in ['train', 'validation'] num_per_shard = int(math.ceil(len(filenames) / float(_NUM_SHARDS))) with tf.Graph().as_default(): image_reader = ImageReader() with tf.Session('') as sess: for shard_id in range(_NUM_SHARDS): output_filename = _get_dataset_filename(dataset_dir, split_name, shard_id) with tf.python_io.TFRecordWriter(output_filename) as tfrecord_writer: start_ndx = shard_id * num_per_shard end_ndx = min((shard_id+1) * num_per_shard, len(filenames)) for i in range(start_ndx, end_ndx): sys.stdout.write('\r>> Converting image %d/%d shard %d' % ( i+1, len(filenames), shard_id)) sys.stdout.flush() # 读取文件名数据: image_data = tf.gfile.FastGFile(filenames[i], 'rb').read() height, width = image_reader.read_image_dims(sess, image_data) class_name = os.path.basename(os.path.dirname(filenames[i])) class_id = class_names_to_ids[class_name] example = dataset_utils.image_to_tfexample( image_data, b'jpg', height, width, class_id) tfrecord_writer.write(example.SerializeToString()) sys.stdout.write('\n') sys.stdout.flush() def _clean_up_temporary_files(dataset_dir): """ 删除创建数据集时产生的临时文件. Args: dataset_dir: 临时文件的路径. """ filename = _DATA_URL.split('/')[-1] filepath = os.path.join(dataset_dir, filename) tf.gfile.Remove(filepath) tmp_dir = os.path.join(dataset_dir, 'flower_photos') tf.gfile.DeleteRecursively(tmp_dir) def _dataset_exists(dataset_dir): for split_name in ['train', 'validation']: for shard_id in range(_NUM_SHARDS): output_filename = _get_dataset_filename( dataset_dir, split_name, shard_id) if not tf.gfile.Exists(output_filename): return False return True def run(dataset_dir): """ 运行数据集下载和转换. Args: dataset_dir: 数据集所在的路径. """ if not tf.gfile.Exists(dataset_dir): tf.gfile.MakeDirs(dataset_dir) if _dataset_exists(dataset_dir): print('Dataset files already exist. Exiting without re-creating them.') return # 如果已经下载解压过 Flowers 数据集，可以跳过此步. # dataset_utils.download_and_uncompress_tarball(_DATA_URL, dataset_dir) photo_filenames, class_names = _get_filenames_and_classes(dataset_dir) class_names_to_ids = dict(zip(class_names, range(len(class_names)))) # 数据集分为：train 和 test: random.seed(_RANDOM_SEED) random.shuffle(photo_filenames) training_filenames = photo_filenames[_NUM_VALIDATION:] validation_filenames = photo_filenames[:_NUM_VALIDATION] # 首先, 分别转换 training 和 validation 数据集. _convert_dataset('train', training_filenames, class_names_to_ids, dataset_dir) _convert_dataset('validation', validation_filenames, class_names_to_ids, dataset_dir) # 最后, 写入标签label 文件: labels_to_class_names = dict(zip(range(len(class_names)), class_names)) dataset_utils.write_label_file(labels_to_class_names, dataset_dir) # 会自动删除 flower_photos.tgz 和 flower_photo 文件夹. _clean_up_temporary_files(dataset_dir) print('\nFinished converting the Flowers dataset!') if __name__ == '__main__': dataset_dir = '/path/to/flower_photos/' run(dataset_dir) print('Done.') 输出类似于如下： 2. Fine-tune InceptionV1 模型TensorFlow - TF-Slim 使用总览 有相关介绍. flowers.py #!/usr/bin/python # -*- coding: utf-8 -*- """ Provides data for the flowers dataset. """ from __future__ import absolute_import from __future__ import division from __future__ import print_function import os import tensorflow as tf slim = tf.contrib.slim import dataset_utils _FILE_PATTERN = 'flowers_%s_*.tfrecord' SPLITS_TO_SIZES = {'train': 3320, 'validation': 350} _NUM_CLASSES = 5 _ITEMS_TO_DESCRIPTIONS = { 'image': 'A color image of varying size.', 'label': 'A single integer between 0 and 4', } def get_split(split_name, dataset_dir, file_pattern=None, reader=None): """ 获取数据集元组，以读取 flowers 数据. Gets a dataset tuple with instructions for reading flowers. Args: split_name: A train/validation split name. dataset_dir: 数据集路径. file_pattern: The file pattern to use when matching the dataset sources. It is assumed that the pattern contains a '%s' string so that the split name can be inserted. reader: The TensorFlow reader type. Returns: A `Dataset` namedtuple. Raises: ValueError: if `split_name` is not a valid train/validation split. """ if split_name not in SPLITS_TO_SIZES: raise ValueError('split name %s was not recognized.' % split_name) if not file_pattern: file_pattern = _FILE_PATTERN file_pattern = os.path.join(dataset_dir, file_pattern % split_name) # Allowing None in the signature so that dataset_factory can use the default. if reader is None: reader = tf.TFRecordReader keys_to_features = { 'image/encoded': tf.FixedLenFeature((), tf.string, default_value=''), 'image/format': tf.FixedLenFeature((), tf.string, default_value='png'), 'image/class/label': tf.FixedLenFeature( [], tf.int64, default_value=tf.zeros([], dtype=tf.int64)), } items_to_handlers = { 'image': slim.tfexample_decoder.Image(), 'label': slim.tfexample_decoder.Tensor('image/class/label'), } decoder = slim.tfexample_decoder.TFExampleDecoder( keys_to_features, items_to_handlers) labels_to_names = None if dataset_utils.has_labels(dataset_dir): labels_to_names = dataset_utils.read_label_file(dataset_dir) return slim.dataset.Dataset( data_sources=file_pattern, reader=reader, decoder=decoder, num_samples=SPLITS_TO_SIZES[split_name], items_to_descriptions=_ITEMS_TO_DESCRIPTIONS, num_classes=_NUM_CLASSES, labels_to_names=labels_to_names) train.py #!/usr/bin/python # -*- coding: utf-8 -*- import os import flowers from nets import inception from preprocessing import inception_preprocessing import tensorflow as tf import tensorflow.contrib.slim as slim image_size = inception.inception_v1.default_image_size flowers_data_dir = '/path/to/flower/tfrecords' checkpoints_dir = '/path/to/flower/checkpoints' train_dir = '/path/to/flower/outputs' def load_batch(dataset, batch_size=32, height=299, width=299, is_training=False): """ 加载单个 bacth 的数据. Args: dataset: 待加载数据. batch_size: batch 内图片数量. height: 预处理后的每张图片的 height. width: 预处理后的每张图片的 width. is_training: 当前数据是否处于 training 还是 evaluating. Returns: images: [batch_size, height, width, 3] 大小的 Tensor, 预处理后的图片样本. images_raw: [batch_size, height, width, 3] 大小的 Tensor, 用于可视化的图片样本. labels: [batch_size] 大小的 Tensor, 其值范围为 [0，dataset.num_classes]. """ data_provider = slim.dataset_data_provider.DatasetDataProvider( dataset, common_queue_capacity=32, common_queue_min=8) image_raw, label = data_provider.get(['image', 'label']) # Inception 的图片预处理. image = inception_preprocessing.preprocess_image(image_raw, height, width, is_training=is_training) # 预处理图片的可视化. image_raw = tf.expand_dims(image_raw, 0) image_raw = tf.image.resize_images(image_raw, [height, width]) image_raw = tf.squeeze(image_raw) # Batch 化. images, images_raw, labels = tf.train.batch( [image, image_raw, label],batch_size=batch_size, num_threads=1, capacity=2 * batch_size) return images, images_raw, labels def get_init_fn(): """ 训练热身函数. 权重参数初始化. """ checkpoint_exclude_scopes=["InceptionV1/Logits", "InceptionV1/AuxLogits"] #原输出层 # finetune 时更改原输出层，初始化权重时，不更新输出层的权重参数 exclusions = [scope.strip() for scope in checkpoint_exclude_scopes] variables_to_restore = [] for var in slim.get_model_variables(): for exclusion in exclusions: if var.op.name.startswith(exclusion): break else: variables_to_restore.append(var) return slim.assign_from_checkpoint_fn( os.path.join(checkpoints_dir, 'inception_v1.ckpt'), variables_to_restore) with tf.Graph().as_default(): tf.logging.set_verbosity(tf.logging.INFO) dataset = flowers.get_split('train', flowers_data_dir) images, _, labels = load_batch(dataset, height=image_size, width=image_size) # 模型创建，采用默认的arg scope 配置 batch norm 参数. with slim.arg_scope(inception.inception_v1_arg_scope()): logits, _ = inception.inception_v1(images, num_classes=dataset.num_classes, is_training=True) # 设定 loss 函数: one_hot_labels = slim.one_hot_encoding(labels, dataset.num_classes) slim.losses.softmax_cross_entropy(logits, one_hot_labels) total_loss = slim.losses.get_total_loss() # 创建 summaries 以可视化训练过程: tf.summary.scalar('losses/Total Loss', total_loss) # 设定 optimizer，创建 train op: optimizer = tf.train.AdamOptimizer(learning_rate=0.01) train_op = slim.learning.create_train_op(total_loss, optimizer) # 开始训练: final_loss = slim.learning.train(train_op, logdir=train_dir, log_every_n_steps=10, init_fn=get_init_fn(), number_of_steps=3000, save_summaries_secs=600, save_interval_secs=1200) print('Finished training. Last batch loss %f' % final_loss) test.py #!/usr/bin/python # -*- coding: utf-8 -*- import numpy as np import matplotlib.pyplot as plt import tensorflow as tf from tensorflow.contrib import slim from nets import inception import flowers from preprocessing import inception_preprocessing image_size = inception.inception_v1.default_image_size batch_size = 30 flowers_data_dir = '/path/to/flower/tfrecords' train_dir = '/path/to/flower/outputs' with tf.Graph().as_default(): tf.logging.set_verbosity(tf.logging.INFO) dataset = flowers.get_split('validation', flowers_data_dir) images, images_raw, labels = load_batch(dataset, height=image_size, width=image_size) # Create the model, use the default arg scope to configure the batch norm parameters. with slim.arg_scope(inception.inception_v1_arg_scope()): logits, _ = inception.inception_v1(images, num_classes=dataset.num_classes, is_training=True) probabilities = tf.nn.softmax(logits) checkpoint_path = tf.train.latest_checkpoint(train_dir) init_fn = slim.assign_from_checkpoint_fn(checkpoint_path, slim.get_variables_to_restore()) with tf.Session() as sess: with slim.queues.QueueRunners(sess): sess.run(tf.initialize_local_variables()) init_fn(sess) np_probabilities, np_images_raw, np_labels = sess.run([probabilities, images_raw, labels]) for i in range(batch_size): image = np_images_raw[i, :, :, :] true_label = np_labels[i] predicted_label = np.argmax(np_probabilities[i, :]) predicted_name = dataset.labels_to_names[predicted_label] true_name = dataset.labels_to_names[true_label] plt.figure() plt.imshow(image.astype(np.uint8)) plt.title('Ground Truth: [%s], Prediction [%s]' % (true_name, predicted_name)) plt.axis('off') plt.show() print('Done.') 3. Related[1] - tensorflow之从文件中读取数据（适用场景：大规模数据集，亲测有效~） [2] - tensorflowxun训练自己的数据集之从tfrecords读取数据 [3] - TensorFlow高效读取数据的方法
论文：Age and Gender Classification using Convolutional Neural Networks - 2015. Multi-task Multi-label 类似问题.主要是针对年龄和性别分别建立分类问题，基于CaffeNet，年龄转化为 8 类别、性别为 2 分类问题，训练两个网络模型，实现原理较为直接简单.项目路径 - Caffe源码路径 - Caffe年龄分类网络结构 - 对应caffemodel性别分类网络结构 - 对应caffemodel1. 年龄和性别分类的CNN网络CNN的第一个应用是用于字符识别的LeNet5 网络. 随着GPU发展，训练数据的快速增长，用于图像分类的深度CNN网络显著提高了准确率. 随后，CNN成功应用于更多领域，如人体姿态估计、人脸关键点检测、语音识别及动作分类等. 这里将CNN网络用于年龄和性别分类任务.收集大型的、标签化的年龄和性别图像训练集有两种方式： 用户的个人注册信息，如出生日期和性别，往往是私人信息. 手工标注，繁琐且耗时. 现阶段，尚无该类型数据集. 针对小型图像数据集，机器学习方法易出现过拟合问题. 深度CNN网络方法由于其具有大量的模型参数，过拟合问题更加突出. 因此，过拟合问题需要慎重考虑.网络结构说明 Figure2. CNN网络结构例示. 网络包括 3 个卷积层，每一个卷积层后接一个 ReLU 和 Pooling 层. 此外，前两层采用LRN(local response normalization)归一化. 第一个卷积层是 96 个 7×7 fliters， 第二个卷积层是 256 个5×5 fliters，第三个和最后一个卷积层均是 384 个 3×3 fliters. 网络最后是两个全连接层，均是 512 个输出单元. 如Figure3. 主要是基于CaffeNet. 分类问题： 年龄分类: 8 类 age = "0 - 2" age = "4 - 6" age = "8 - 13" age = "15 - 20" age = "25 - 32" age = "38 - 43" age = "48 - 53" age = "60 -" 性别分类: 2 类 网络输入图片是从 256×256 中裁剪 227×227 大小，输出的全连接层后接 softmax layer，以估计各类的概率. 针对测试图片，概率最大的类即为预测结果.2. 联合训练年龄和性别分类网络根据对论文方法及其实现的学习，个人考虑，可以将年龄和性别分类问题转化为二标签多任务分类问题，即每张人脸图像均有两个标签——年龄和性别，训练数据格式为：# train.txt faceimg1.jpg 1 0 faceimg2.jpg 0 1 faceimg3.jpg 2 0 ... 具体操作可参考 - Caffe 实践 - Multi-label 多标签标注的训练与部署. - Caffe实践 - 多标签图片分类的训练与部署
采用 LMDB 读取图片数据和标签, 处理 multi-label 问题. 基于 ResNet101 backbone 网络 multi-task 输出 类似问题, 图像自动标注上一篇博文曾尝试过修改 Caffe ImageDataLayer 源码的方式来读取多个 labels - ImageMultilabelDataLayer [Caffe实践 - 多标签图片分类的训练与部署].修改源码的方式可能显得稍微有点繁琐, 毕竟需要重新编译.这里尝试了一种新的方式来进行多标签自动标注.与 [Caffe实践 - 多标签图片分类的训练与部署] 不同的是, 前者是以 Multi-task 的方式进行处理的,每一个 task 分别是一个label的分类问题. 而这里是以多标签标注方式进行的.1. 数据集1.1 数据格式转换数据形式如, images_labels.txt:img1.jpg 1 0 1 ... 0 img2.jpg 0 1 0 ... 1 img3.jpg 1 1 0 ... 0 ...... 每一行是一个数据样本, 其对应的 multilabels 为一个 01 向量的形式. 且所有的样本所对应的 multilabels 向量长度是相同的.可以类似于 [Caffe实践 - 多标签图片分类的训练与部署] 来读取 multilabels 数据.但这里采用了另外的一种方式:首先将数据转换为两个文件, 其内容格式分别为: imageslist.txt img1.jpg 2 img1.jpg 5 img1.jpg 3 ...... 每一行对应一个数据样本, 图片 + 标签labels数 的形式. labelslist.txt 1 0 1 ... 0 0 1 0 ... 1 1 1 0 ... 0 ...... 每一行对应与一个数据样本的 multilabels. 01 向量. 1.2 生成 lmdb 数据gen_label_lmdb.py:#! --*-- coding: utf-8 --*-- import numpy import argparse from caffe.proto import caffe_pb2 import lmdb def parse_args(): parser = argparse.ArgumentParser(description='End-to-end inference') parser.add_argument('--labels', dest='labels', help='label txt file', default=None, type=str ) parser.add_argument('--images', dest='images', help='image txt file, for keys of datum', default=None, type=str ) parser.add_argument('--lmdb', dest='lmdb', help='label lmdb file', default=None, type=str ) if len(sys.argv) == 1: parser.print_help() sys.exit(1) return parser.parse_args() def array_to_datum(arr, label=0): """ Converts a 3-dimensional array to datum. If the array has dtype uint8, the output data will be encoded as a string. Otherwise, the output data will be stored in float format. """ if arr.ndim != 3: raise ValueError('Incorrect array shape.') datum = caffe_pb2.Datum() datum.channels, datum.height, datum.width = arr.shape if arr.dtype == numpy.uint8: datum.data = arr.tostring() else: datum.float_data.extend(arr.flat) datum.label = label return datum if __name__ == '__main__': print 'Starting Generate lmdb' args = parse_args() labels_array = numpy.loadtxt(args.labels) num_samples, num_classes = labels_array.shape images_keys_list = open(args.images).readlines() assert num_samples == len(images_keys_list) labels_db = lmdb.open(args.lmdb, map_size=20 * 1024 * 1024 * 1024) with labels_db.begin(write=True) as txn: for idx in range(0, num_samples): # label data labels_cur = labels_array[idx,:] num_labels = labels_cur.size labels_cur = labels_cur.reshape(num_labels, 1, 1) # keys for label key_cur = images_keys_list[idx].split(' ')[0] # image name key_cur = "{:0>8d}".format(idx) + '_' + key_cur num_taggs = int(images_keys_list[idx].split(' ')[1]) assert num_taggs == labels_cur.sum() # create datum labels_datum = array_to_datum(labels_cur, num_taggs) # write datum to lmdb txn.put(key_cur.encode('ascii'), labels_datum.SerializeToString()) if (idx+1)%5000 == 0: print 'Processed ', idx + 1, 'files in total.' num_label_db = 0 with labels_db.begin() as txn: cursor = txn.cursor() for key, value in cursor: n_label_db = num_label_db + 1 print 'Total # of item in label lmdb:', num_label_db 命令行运行:python gen_label_lmdb.py --labels ./data/labelslist.txt --images ./data/imageslist.txt --lmdb ./data/output_lmdb/ 2. 模型定义与训练2.1 train_val.prototxtname: "resnet101" layer { name: "data" type: "ImageData" top: "data" top: "dummylabel" image_data_param { source: "/path/to/train_imagelist.txt" root_folder: "/path/to/images/" batch_size: 16 new_height: 256 new_width: 256 } transform_param { mirror: true crop_size: 224 mean_value: 104 mean_value: 117 mean_value: 123 } include { phase: TRAIN } } layer { name: "label" type: "Data" top: "label" data_param { source: "/path/to/train_output_lmdb" batch_size: 16 backend: LMDB } include { phase: TRAIN } } ## layer { name: "data" type: "ImageData" top: "data" top: "dummylabel" image_data_param { source: "/path/to/test_imagelist.txt" root_folder: "/path/to/images/" batch_size: 4 new_height: 256 new_width: 256 } transform_param { crop_size: 224 mean_value: 104 mean_value: 117 mean_value: 123 } include { phase: TEST } } layer { name: "label" type: "Data" top: "label" data_param { source: "/path/to/test_output_lmdb" batch_size: 4 backend: LMDB } include { phase: TEST } } ####### dummylabel ####### # 这里 dummpylabe 是未用到的 layer { name: "label_silence" type: "Silence" bottom: "dummylabel" } #### resnet 101 netword define #### ...... ############################# layer { name: "fc_labels" type: "InnerProduct" bottom: "Pooling_" top: "fc_labels" param { lr_mult: 1 decay_mult: 1 } param { lr_mult: 1 decay_mult: 0 } inner_product_param { num_output: 30 # labels 数 weight_filler { type: "gaussian" std: 0.005 } bias_filler { type: "constant" value: 0 } } } layer { name: "loss" type: "SigmoidCrossEntropyLoss" # bottom: "fc_labels" bottom: "label" top: "loss" } 2.2 deploy.prototxtname: "resnet101" input: "data" input_shape { dim: 1 dim: 3 dim: 224 dim: 224 } #### netword define #### ...... #################### layer { name: "fc_labels" type: "InnerProduct" bottom: "Pooling_" top: "fc_labels" param { lr_mult: 1 decay_mult: 1 } param { lr_mult: 1 decay_mult: 0 } inner_product_param { num_output: 30 # labels 数 weight_filler { type: "gaussian" std: 0.005 } bias_filler { type: "constant" value: 0 } } } 3. 模型部署#!/usr/bin/env python # --*-- coding: utf-8 --*-- import numpy as np from PIL import Image import scipy.misc import matplotlib.pyplot as plt import sys caffe_root = '/path/to/caffe/' sys.path.insert(0, caffe_root + 'python') import caffe caffe.set_mode_gpu() caffe.set_device(0) # caffe.set_mode_cpu() class SimpleTransformer(object): """ SimpleTransformer is a simple class for preprocessing and deprocessing images for caffe. """ def __init__(self, mean=[128, 128, 128]): self.mean = np.array(mean, dtype=np.float32) self.scale = 1.0 def set_mean(self, mean): """ Set the mean to subtract for centering the data. """ self.mean = mean def set_scale(self, scale): """ Set the data scaling. """ self.scale = scale def preprocess(self, im): """ preprocess() emulate the pre-processing occuring in the vgg16 caffe prototxt. """ im = np.float32(im) im = im[:, :, ::-1] # change to BGR im -= self.mean im *= self.scale im = im.transpose((2, 0, 1)) return im if __name__ == '__main__': print 'Start...' multilabels = ['a', 'b', 'c', ...] test_image = '/home/sh/Pictures/upper/10.jpg' im = np.asarray(Image.open(test_image)) im = scipy.misc.imresize(im, [224, 224]) model_def = '/path/to/deploy.prototxt' weight_def = '/path/to/multilabel_vgg16_iter_100000.caffemodel' net = caffe.Net(model_def, weight_def, caffe.TEST) transformer = ImageTransformer({'data': net.blobs['data'].data.shape}) transformer.set_transpose('data', (2, 0, 1)) # h*w*c -> c*h*w transformer.set_mean('data', np.array((104, 117, 123))) # mean pixel transformer.set_raw_scale('data', 255) # the net operates on images in [0,255] transformer.set_channel_swap('data', (2, 1, 0)) # RGB -> BGR transformed_image = transformer.preprocess(im) net.blobs['data'].data[...] = transformed_image outputs = net.forward() img_scores = outputs[net.outputs[0]] img_scores = np.array([1./(1+np.exp(-tmp)) for tmp in img_scores[0]]) pred_labels_index = np.where(img_scores > 0.5)[0].tolist() print 'pred_labels:', pred_labels_index, '----', len(pred_labels_index) for tmp in pred_labels_index: print 'pred_labels:', multilabels[tmp], '----', len(pred_labels_index) print '--------------------------------------------' plt.imshow(im) plt.axis('off') plt.show() print 'Done.' Related[1] - Caffe实践 - 多标签图片分类的训练与部署
Caffe实践 - 多标签图片分类的训练与部署
这里采用从图像直接读取图片数据和标签的方式进行多标签分类. 基于 VGG16 backbone 网络 修改 ImageDataLayer 读取图片和多标签数据 multi-task 输出 类似问题, 图像自动标注1. 问题描述假定每张图片具有 N 个标签(本文N=3)，分别为 label1, label2, label3,...,labelN. 数据集 train.txt 内容格式如下：img1.jpg 5 6 7 ... 8 img2.jpg 1 2 3 ... 6 img3.jpg 7 8 9 ... 10 ...... 以 train.txt 第一行为例， img1.jpg 为图像名，5 为在label1 所对应的索引，6 为在label2 所对应的索引，7 为在label3 所对应的索引，8 为 在labelN 所对应的索引(均为从 0 开始).2. 解决方案基于 Caffe 官方提供的 ImageDataLayer 只能读取单个 label，因此这里参考 ImageDataLayer 添加新的网络数据读取层 —— ImageMultilabelDataLayer.Caffe 添加网络数据层所涉及主要步骤如下： 添加 hpp 头文件，如 include/image_multilabel_data_layer.hpp 添加 cpp 实现文件，如 src/caffe/layers/image_multilabel_data_layer.cpp 添加 Layer 对应的类及其类涉及参数，在 src/caffe/proto/caffe.proto文件中 2.1 添加 image_multilabel_data_layer.hpp#ifndef CAFFE_IMAGE_MULTILABEL_DATA_LAYER_HPP_ #define CAFFE_IMAGE_MULTILABEL_DATA_LAYER_HPP_ #include <string> #include <utility> #include <vector> #include "caffe/blob.hpp" #include "caffe/data_transformer.hpp" #include "caffe/internal_thread.hpp" #include "caffe/layer.hpp" #include "caffe/layers/base_data_layer.hpp" #include "caffe/proto/caffe.pb.h" namespace caffe { /** * @brief Provides data to the Net from image files. * * TODO(dox): thorough documentation for Forward and proto params. */ template <typename Dtype> class ImageMultilabelDataLayer : public BasePrefetchingDataLayer<Dtype> { public: explicit ImageMultilabelDataLayer(const LayerParameter& param) : BasePrefetchingDataLayer<Dtype>(param) {} virtual ~ImageMultilabelDataLayer(); virtual void DataLayerSetUp(const vector<Blob<Dtype>*>& bottom, const vector<Blob<Dtype>*>& top); virtual inline const char* type() const { return "ImageData"; } virtual inline int ExactNumBottomBlobs() const { return 0; } virtual inline int ExactNumTopBlobs() const { return 2; } protected: shared_ptr<Caffe::RNG> prefetch_rng_; virtual void ShuffleImages(); virtual void load_batch(Batch<Dtype>* batch); vector<std::pair<std::string, int *> > lines_; // string 对应 train.txt 中的图片名，int * 实现多label，其中各label间默认以空格隔离. int lines_id_; }; } // namespace caffe #endif // CAFFE_IMAGE_MULTILABEL_DATA_LAYER_HPP_ 2.2 添加 image_multilabel_data_layer.cpp#ifdef USE_OPENCV #include <opencv2/core/core.hpp> #include <fstream> // NOLINT(readability/streams) #include <iostream> // NOLINT(readability/streams) #include <string> #include <utility> #include <vector> #include "caffe/data_transformer.hpp" #include "caffe/layers/base_data_layer.hpp" #include "caffe/layers/image_multilabel_data_layer.hpp" #include "caffe/util/benchmark.hpp" #include "caffe/util/io.hpp" #include "caffe/util/math_functions.hpp" #include "caffe/util/rng.hpp" namespace caffe { template <typename Dtype> ImageMultilabelDataLayer<Dtype>::~ImageMultilabelDataLayer<Dtype>() { this->StopInternalThread(); } template <typename Dtype> void ImageMultilabelDataLayer<Dtype>::DataLayerSetUp(const vector<Blob<Dtype>*>& bottom, const vector<Blob<Dtype>*>& top) { const int new_height = this->layer_param_.image_multilabel_data_param().new_height(); //裁剪后的图片 height const int new_width = this->layer_param_.image_multilabel_data_param().new_width(); //裁剪后的图片 width const bool is_color = this->layer_param_.image_multilabel_data_param().is_color(); string root_folder = this->layer_param_.image_multilabel_data_param().root_folder(); //图片存储路径 CHECK((new_height == 0 && new_width == 0) || (new_height > 0 && new_width > 0)) << "Current implementation requires " "new_height and new_width to be set at the same time."; // 保证裁剪后图片的 height 和 width 相等. // 读取保存着图片名和labels的file const string& source = this->layer_param_.image_multilabel_data_param().source(); LOG(INFO) << "Opening file " << source; std::ifstream infile(source.c_str()); string filename; // get labels classes // label_dim 为新增参数，表示labels类别，比关于人的，共有三类——年龄、性别、肤色，则label_dim=3 int label_dim = this->layer_param_.image_multilabel_data_param().label_dim(); // train.txt中的每行记录了每个图片名称及其labels. while (infile >> filename) { int* labels = new int[label_dim]; for(int i = 0;i < label_dim;++i){ infile >> labels[i]; } lines_.push_back(std::make_pair(filename, labels)); } CHECK(!lines_.empty()) << "File is empty"; if (this->layer_param_.image_multilabel_data_param().shuffle()) { // 随机打乱数据顺序 LOG(INFO) << "Shuffling data"; const unsigned int prefetch_rng_seed = caffe_rng_rand(); prefetch_rng_.reset(new Caffe::RNG(prefetch_rng_seed)); ShuffleImages(); } else { if (this->phase_ == TRAIN && Caffe::solver_rank() > 0 && this->layer_param_.image_multilabel_data_param().rand_skip() == 0) { LOG(WARNING) << "Shuffling or skipping recommended for multi-GPU"; } } LOG(INFO) << "A total of " << lines_.size() << " images."; lines_id_ = 0; // Check if we would need to randomly skip a few data points if (this->layer_param_.image_multilabel_data_param().rand_skip()) { unsigned int skip = caffe_rng_rand() % this->layer_param_.image_multilabel_data_param().rand_skip(); LOG(INFO) << "Skipping first " << skip << " data points."; CHECK_GT(lines_.size(), skip) << "Not enough points to skip"; lines_id_ = skip; } // Read an image, and use it to initialize the top blob. cv::Mat cv_img = ReadImageToCVMat(root_folder + lines_[lines_id_].first, new_height, new_width, is_color); CHECK(cv_img.data) << "Could not load " << lines_[lines_id_].first; // Use data_transformer to infer the expected blob shape from a cv_image. vector<int> top_shape = this->data_transformer_->InferBlobShape(cv_img); this->transformed_data_.Reshape(top_shape); // Reshape prefetch_data and top[0] according to the batch_size. const int batch_size = this->layer_param_.image_multilabel_data_param().batch_size(); CHECK_GT(batch_size, 0) << "Positive batch size required"; top_shape[0] = batch_size; for (int i = 0; i < this->prefetch_.size(); ++i) { this->prefetch_[i]->data_.Reshape(top_shape); } top[0]->Reshape(top_shape); LOG(INFO) << "output data size: " << top[0]->num() << "," << top[0]->channels() << "," << top[0]->height() << "," << top[0]->width(); // labels // 读取图片所对应的多个labels vector<int> label_shape(2); label_shape[0] = batch_size; label_shape[1] = label_dim; top[1]->Reshape(label_shape); for (int i = 0; i < this->prefetch_.size(); ++i) { this->prefetch_[i]->label_.Reshape(label_shape); } } template <typename Dtype> void ImageMultilabelDataLayer<Dtype>::ShuffleImages() { caffe::rng_t* prefetch_rng = static_cast<caffe::rng_t*>(prefetch_rng_->generator()); shuffle(lines_.begin(), lines_.end(), prefetch_rng); } // This function is called on prefetch thread template <typename Dtype> void ImageMultilabelDataLayer<Dtype>::load_batch(Batch<Dtype>* batch) { CPUTimer batch_timer; batch_timer.Start(); int label_dim = this->layer_param_.image_multilabel_data_param().label_dim(); // 获取 label_dim 参数 double read_time = 0; double trans_time = 0; CPUTimer timer; CHECK(batch->data_.count()); CHECK(this->transformed_data_.count()); ImageMultilabelDataParameter image_multilabel_data_param = this->layer_param_.image_multilabel_data_param(); const int batch_size = image_multilabel_data_param.batch_size(); const int new_height = image_multilabel_data_param.new_height(); const int new_width = image_multilabel_data_param.new_width(); const bool is_color = image_multilabel_data_param.is_color(); string root_folder = image_multilabel_data_param.root_folder(); // Reshape according to the first image of each batch // on single input batches allows for inputs of varying dimension. cv::Mat cv_img = ReadImageToCVMat(root_folder + lines_[lines_id_].first, new_height, new_width, is_color); CHECK(cv_img.data) << "Could not load " << lines_[lines_id_].first; // Use data_transformer to infer the expected blob shape from a cv_img. vector<int> top_shape = this->data_transformer_->InferBlobShape(cv_img); this->transformed_data_.Reshape(top_shape); // Reshape batch according to the batch_size. top_shape[0] = batch_size; batch->data_.Reshape(top_shape); Dtype* prefetch_data = batch->data_.mutable_cpu_data(); Dtype* prefetch_label = batch->label_.mutable_cpu_data(); // datum scales const int lines_size = lines_.size(); for (int item_id = 0; item_id < batch_size; ++item_id) { // get a blob timer.Start(); CHECK_GT(lines_size, lines_id_); cv::Mat cv_img = ReadImageToCVMat(root_folder + lines_[lines_id_].first, new_height, new_width, is_color); CHECK(cv_img.data) << "Could not load " << lines_[lines_id_].first; read_time += timer.MicroSeconds(); timer.Start(); // Apply transformations (mirror, crop...) to the image int offset = batch->data_.offset(item_id); this->transformed_data_.set_cpu_data(prefetch_data + offset); this->data_transformer_->Transform(cv_img, &(this->transformed_data_)); trans_time += timer.MicroSeconds(); // 图片的多 labels 预读取 for(int i = 0;i < label_dim;++i){ // lines_[lines_id_].second就是最开始改为的int*,多label prefetch_label[item_id * label_dim + i] = lines_[lines_id_].second[i]; } // go to the next iter lines_id_++; if (lines_id_ >= lines_size) { // We have reached the end. Restart from the first. DLOG(INFO) << "Restarting data prefetching from start."; lines_id_ = 0; if (this->layer_param_.image_multilabel_data_param().shuffle()) { ShuffleImages(); } } } batch_timer.Stop(); DLOG(INFO) << "Prefetch batch: " << batch_timer.MilliSeconds() << " ms."; DLOG(INFO) << " Read time: " << read_time / 1000 << " ms."; DLOG(INFO) << "Transform time: " << trans_time / 1000 << " ms."; } INSTANTIATE_CLASS(ImageMultilabelDataLayer); REGISTER_LAYER_CLASS(ImageMultilabelData); } // namespace caffe #endif // USE_OPENCV 2.3 添加 ImageMultilabelDataParameter 到 caffe.proto在 message LayerParameter 中添加 ImageMultilabelDataParameter:optional ImageMultilabelDataParameter image_multilabel_data_param = 147; //如果冲突，147更改为其它可用ID 新增 ImageMultilabelDataParameter:message ImageMultilabelDataParameter { optional string source = 1; optional uint32 batch_size = 4 [default = 1]; optional uint32 rand_skip = 7 [default = 0]; optional bool shuffle = 8 [default = false]; optional uint32 new_height = 9 [default = 0]; optional uint32 new_width = 10 [default = 0]; optional bool is_color = 11 [default = true]; optional float scale = 2 [default = 1]; optional string mean_file = 3; optional uint32 crop_size = 5 [default = 0]; optional bool mirror = 6 [default = false]; optional string root_folder = 12 [default = ""]; optional uint32 label_dim = 13 [default = 1]; } 2.4 编译并测试修改完成后，重新编译Caffe.cd /path/to/caffe_multilabel/ make clean && make all -j4 make test -j4 make pycaffe 成功后可以定义一个简单的网络数据读取层，验证是否正确读取图片数据标签.train_multilabel_test.prototxt 定义：name: "multilabel-datalayer" layer { name: "data" type: "ImageMultilabelData" top: "data" top: "label" transform_param { mean_value: 128 mean_value: 128 mean_value: 128 } image_multilabel_data_param { mirror: true source: "/path/to/train.txt" root_folder: "/path/to/images/" new_height: 224 new_width: 224 batch_size: 32 shuffle: true label_dim: 3 } } layer { name: "slice" type: "Slice" bottom: "label" top: "label1" top: "label2" top: "label3" slice_param { axis: 1 slice_point:1 slice_point:2 } } train_multilabel_solver.prototxt 定义：net: "/path/to/train_multilabel_test.prototxt.prototxt" test_iter: 100 test_interval: 100 base_lr: 0.001 lr_policy: "step" gamma: 0.1 stepsize: 1000 display: 20 max_iter: 10000 momentum: 0.9 weight_decay: 0.0005 snapshot: 1000 snapshot_prefix: "/path/to/out/caffemodel" solver_mode: GPU data_read_test.py#!/usr/bin/env python #! --*-- coding: utf-8 --*-- import numpy as np import matplotlib.pyplot as plt import sys caffe_root = '/path/to/caffe-multilabel/' sys.path.insert(0, caffe_root + 'python') import caffe caffe.set_mode_gpu() caffe.set_device(0) # caffe.set_mode_cpu() print 'Start...' solver_def = '/path/to/vgg16/solver.prototxt' solver = caffe.SGDSolver(solver_def) solver.step(1) data = solver.net.blobs['data'].data labels = solver.net.blobs['label'].data img = np.transpose(data[0], (1, 2, 0)) gt = labels[0] plt.imshow(img) plt.show() print 'Done.' 3. 基于VGG16多标签分类3.1 train_val.prototxtname: "vgg16-multilabel" layer { name: "data" type: "ImageMultilabelData" top: "data" top: "label" include { phase: TRAIN } transform_param { mean_value: 128 mean_value: 128 mean_value: 128 } image_multilabel_data_param { mirror: true source: "/path/to/train.txt" root_folder: "/path/to/images/" new_height: 224 new_width: 224 batch_size: 32 shuffle: true label_dim: 3 } } layer { name: "data" type: "ImageMultilabelData" top: "data" top: "label" include { phase: TEST } transform_param { mean_value: 128 mean_value: 128 mean_value: 128 } image_multilabel_data_param { mirror: false source: "/path/to/test.txt" root_folder: "/path/to/images/" new_height: 224 new_width: 224 batch_size: 4 shuffle: false label_dim: 3 } } ##### vgg16 layers #### ...... #### labels ################## layer { name: "slice" type: "Slice" bottom: "label" top: "label1" top: "label2" top: "label3" slice_param { axis: 1 slice_point:1 slice_point:2 } } #### label1 ################# layer { bottom: "fc7" top: "fc8_label1" name: "fc8_label1" type: "InnerProduct" param { lr_mult: 1 decay_mult: 1 } param { lr_mult: 2 decay_mult: 0 } inner_product_param { num_output: number1 # label1 包含的类别数 weight_filler { type: "gaussian" std: 0.005 } bias_filler { type: "constant" value: 0.1 } } } layer { name: "accuracy_label1" type: "Accuracy" bottom: "fc8_label1" bottom: "label1" top: "accuracy_label1" accuracy_param { top_k: 1 ignore_label: 0 } include { phase: TEST } } layer { bottom: "fc8_label1" bottom: "label1" top: "loss_label1" name: "loss_label1" type: "SoftmaxWithLoss" loss_param{ ignore_label: 0 } } #### label2 ################# layer { bottom: "fc7" top: "fc8_label2" name: "fc8_label2" type: "InnerProduct" param { lr_mult: 1 decay_mult: 1 } param { lr_mult: 2 decay_mult: 0 } inner_product_param { num_output: number2 # label2 包含的类别数 weight_filler { type: "gaussian" std: 0.005 } bias_filler { type: "constant" value: 0.1 } } } layer { name: "accuracy_label2" type: "Accuracy" bottom: "fc8_label2" bottom: "label2" top: "accuracy_label2" accuracy_param { top_k: 1 ignore_label: 0 } include { phase: TEST } } layer { bottom: "fc8_label2" bottom: "label2" top: "loss_label2" name: "loss_label2" type: "SoftmaxWithLoss" loss_param{ ignore_label: 0 } } #### label3 ################# layer { bottom: "fc7" top: "fc8_label3" name: "fc8_label3" type: "InnerProduct" param { lr_mult: 1 decay_mult: 1 } param { lr_mult: 2 decay_mult: 0 } inner_product_param { num_output: number3 # label3 包含的类别数 weight_filler { type: "gaussian" std: 0.005 } bias_filler { type: "constant" value: 0.1 } } } layer { name: "accuracy_label3" type: "Accuracy" bottom: "fc8_label3" bottom: "label3" top: "accuracy_label3" accuracy_param { top_k: 1 ignore_label: 0 } include { phase: TEST } } layer { bottom: "fc8_label3" bottom: "label3" top: "loss_label3" name: "loss_label3" type: "SoftmaxWithLoss" loss_param{ ignore_label: 0 } } 3.2 solver.prototxtnet: "/path/to/train_val.prototxt" test_iter: 1000 test_interval: 20000 base_lr: 0.001 lr_policy: "step" gamma: 0.1 stepsize: 50000 display: 20 max_iter: 200000 momentum: 0.9 weight_decay: 0.0005 snapshot: 10000 snapshot_prefix: "/path/to/out/caffemodel/" solver_mode: GPU 3.3 deploy.prototxtname: "vgg16-multilabel" input: "data" input_dim: 1 input_dim: 3 input_dim: 224 input_dim: 224 ##### vgg16 layers #### ...... #### label1 ################# layer { bottom: "fc7" top: "fc8_label1" name: "fc8_label1" type: "InnerProduct" param { lr_mult: 1 decay_mult: 1 } param { lr_mult: 2 decay_mult: 0 } inner_product_param { num_output: number1 # label1 包含的类别数 weight_filler { type: "gaussian" std: 0.005 } bias_filler { type: "constant" value: 0.1 } } } layer { bottom: "fc8_label1" top: "prob_label1" name: "prob_label1" type: "Softmax" } #### label2 ################# layer { bottom: "fc7" top: "fc8_label2" name: "fc8_label2" type: "InnerProduct" param { lr_mult: 1 decay_mult: 1 } param { lr_mult: 2 decay_mult: 0 } inner_product_param { num_output: number2 # label2 包含的类别数 weight_filler { type: "gaussian" std: 0.005 } bias_filler { type: "constant" value: 0.1 } } } layer { bottom: "fc8_label2" top: "prob_label2" name: "prob_label2" type: "Softmax" } #### label3 ################# layer { bottom: "fc7" top: "fc8_label3" name: "fc8_label3" type: "InnerProduct" param { lr_mult: 1 decay_mult: 1 } param { lr_mult: 2 decay_mult: 0 } inner_product_param { num_output: number3 # label3 包含的类别数 weight_filler { type: "gaussian" std: 0.005 } bias_filler { type: "constant" value: 0.1 } } } layer { bottom: "fc8_label3" top: "prob_label3" name: "prob_label3" type: "Softmax" } 3.5 多标签分类部署 deploy.py#!/usr/bin/env python # --*-- coding: utf-8 --*-- import numpy as np from PIL import Image import scipy.misc import matplotlib.pyplot as plt import sys caffe_root = '/path/to/caffe-multilabel/' sys.path.insert(0, caffe_root + 'python') import caffe caffe.set_mode_gpu() caffe.set_device(0) # caffe.set_mode_cpu() class SimpleTransformer(object): """ SimpleTransformer is a simple class for preprocessing and deprocessing images for caffe. """ def __init__(self, mean=[128, 128, 128]): self.mean = np.array(mean, dtype=np.float32) self.scale = 1.0 def set_mean(self, mean): """ Set the mean to subtract for centering the data. """ self.mean = mean def set_scale(self, scale): """ Set the data scaling. """ self.scale = scale def preprocess(self, im): """ preprocess() emulate the pre-processing occuring in the vgg16 caffe prototxt. """ im = np.float32(im) im = im[:, :, ::-1] # change to BGR im -= self.mean im *= self.scale im = im.transpose((2, 0, 1)) return im def deprocess(self, im): """ inverse of preprocess() """ im = im.transpose(1, 2, 0) im /= self.scale im += self.mean im = im[:, :, ::-1] # change to RGB return np.uint8(im) if __name__ == '__main__': print 'Start...' test_image = '/home/sh/Pictures/upper/10.jpg' im = np.asarray(Image.open(test_image)) im = scipy.misc.imresize(im, [224, 224]) model_def = '/path/to/deploy.prototxt' weight_def = '/path/to/multilabel_vgg16_iter_100000.caffemodel' net = caffe.Net(model_def, weight_def, caffe.TEST) transformer = SimpleTransformer() transformed_image = transformer.preprocess(im) net.blobs['data'].data[...] = transformed_image outputs = net.forward() prob_label1 = outputs['prob_label1'][0].argmax() prob_label2 = outputs['prob_label2'][0].argmax() prob_label3 = outputs['prob_label3'][0].argmax() plt.imshow(im) plt.axis('off') plt.show() print '****** pred results ****************' print ' label1: ', label1_names[prob_label1] print ' label2: ', label2_names[prob_label2] print ' label3: ', label3_names[prob_label3] print '************************************' Reference[1] - caffe实现多label输入(修改源码版)
Caffe实践 - 单标签图片分类的训练与部署
单标签图片分类是指, 每张图片只能有一个标签, 比如是狗, 或猫. 一般来说的图片分类就是单标签的. 这也是 CNN 成功最初很好解决的问题.1. 训练数据制作主要是生成 train.txt、 val.txt 和 test.txt 文件，使其内容格式为：image_name1 label1 image_name2 label2 image_name3 label3 ...... 如：img1.jpg 0 img2.jpg 0 img3.jpg 1 img4.jpg 1 img5.jpg 2 img6.jpg 2 ...... 2. 生成数据集的 lmdb主要是利用 caffe 里自带的 create_imagenet.sh 和 make_imagenet_mean.sh 脚本，主要修改的地方有： - create_imagenet.sh#!/usr/bin/env sh # Create the imagenet lmdb inputs # N.B. set the path to the imagenet train + val data dirs set -e EXAMPLE=/path/to/save/lmdb/ # 生成lmdb的保存路径 DATA=/path/to/tranorval/txt/ # train.txt, val.txt 所在路径 TOOLS=/path/to/caffe/build/tools # caffe 编译后的工具 TRAIN_DATA_ROOT=/path/to/images/train/ # train 图像数据集的根目录 VAL_DATA_ROOT=/path/to/images/val/ # val 图像数据集的根目录 # Set RESIZE=true to resize the images to 256x256. Leave as false if images have # already been resized using another tool. RESIZE=false ## 修改为 true，将图片尺寸统一为 256x256 if *RESIZE; then RESIZE_HEIGHT=256 RESIZE_WIDTH=256 else RESIZE_HEIGHT=0 RESIZE_WIDTH=0 fi if [ ! -d "*TRAIN_DATA_ROOT" ]; then echo "Error: TRAIN_DATA_ROOT is not a path to a directory: *TRAIN_DATA_ROOT" echo "Set the TRAIN_DATA_ROOT variable in create_imagenet.sh to the path" \ "where the ImageNet training data is stored." exit 1 fi if [ ! -d "*VAL_DATA_ROOT" ]; then echo "Error: VAL_DATA_ROOT is not a path to a directory: *VAL_DATA_ROOT" echo "Set the VAL_DATA_ROOT variable in create_imagenet.sh to the path" \ "where the ImageNet validation data is stored." exit 1 fi echo "Creating train lmdb..." GLOG_logtostderr=1 *TOOLS/convert_imageset \ --resize_height=*RESIZE_HEIGHT \ --resize_width=*RESIZE_WIDTH \ --shuffle \ *TRAIN_DATA_ROOT \ *DATA/train.txt \ *EXAMPLE/train_lmdb # 生成 train 的 lmdb, 可以修改名字 echo "Creating val lmdb..." GLOG_logtostderr=1 *TOOLS/convert_imageset \ --resize_height=*RESIZE_HEIGHT \ --resize_width=*RESIZE_WIDTH \ --shuffle \ *VAL_DATA_ROOT \ *DATA/val.txt \ *EXAMPLE/val_lmdb # 生成 val 的 lmdb, 可以修改名字 echo "Done." make_imagenet_mean.sh #!/usr/bin/env sh # Compute the mean image from the imagenet training lmdb # N.B. this is available in data/ilsvrc12 EXAMPLE=/path/to/save/lmdb/ # 生成lmdb的保存路径 DATA=/path/to/tranorval/txt/ # train.txt, val.txt 所在路径 TOOLS=/path/to/caffe/build/tools # caffe 编译后的工具 *TOOLS/compute_image_mean *EXAMPLE/train_lmdb \ *DATA/images_mean.binaryproto # 生成的均值文件，名字可修改 echo "Done." 注: * 替换为 $ 符号.分别运行 create_imagenet.sh 和 make_imagenet_mean.sh， 即可生成train训练数据集和val验证数据集的 lmdb 文件，及对应的均值文件.将 images_mean.binaryproto 均值文件转换为 image_mean.npy:import caffe caffeBlob = caffe.proto.caffe_pb2.BlobProto() data = open('imeags_mean.binaryproto', 'rb' ).read() caffeBlob.ParseFromString(data) array = np.array(caffe.io.blobproto_to_array(caffeBlob)) mean_file = 'imeages_mean.npy' np.save(mean_file, array[0]) 3. 网络修改与模型训练对alexnt、caffenet、googlenet等网络数据层和输出层进行修改，即可.3.1 修改网络数据层layer { name: "data" type: "Data" top: "data" top: "label" include { phase: TRAIN } transform_param { mirror: true crop_size: 224 mean_file: "/path/to/images_mean.binaryproto" # 均值文件路径 #mean_value: 104 #mean_value: 117 #mean_value: 123 } data_param { source: "/path/to/train_lmdb" # 训练集的lmdb batch_size: 32 backend: LMDB } } layer { name: "data" type: "Data" top: "data" top: "label" include { phase: TEST } transform_param { mirror: false crop_size: 224 mean_file: "/path/to/images_mean.binaryproto" # 均值文件路径 #mean_value: 104 #mean_value: 117 #mean_value: 123 } data_param { source: "/path/to/val_lmdb" # 验证集的lmdb batch_size: 50 # 一般情况下，batch_size * (solver中)test_iter 约等于验证集大小 backend: LMDB } } 3.2 修改网络输出层一般情况下，网络只有一个输出，只需要一处即可以，比如 alexnet、caffenet，但 googlenet 有三处需要修改——两个辅助loss层. 这里以 alexnet 为例.- train_val.prototxt 修改layer { name: "fc8" # 网络输出数据层名称， 需修改 type: "InnerProduct" bottom: "fc7" top: "fc8" # 网络输出数据层名称， 需修改 param { lr_mult: 1 decay_mult: 1 } param { lr_mult: 2 decay_mult: 0 } inner_product_param { num_output: 1000 # 网络输出数，即类别数据， 需修改 weight_filler { type: "gaussian" std: 0.01 } bias_filler { type: "constant" value: 0 } } } layer { name: "accuracy" type: "Accuracy" bottom: "fc8" # 与网络输出数据层名称一致 bottom: "label" top: "accuracy" include { phase: TEST } } layer { name: "loss" type: "SoftmaxWithLoss" bottom: "fc8" # 与网络输出数据层名称一致 bottom: "label" top: "loss" } deploy.prototxt 修改 layer { name: "fc8" # 与train_val.prototxt中网络输出数据层名称一致 type: "InnerProduct" bottom: "fc7" top: "fc8" # 与train_val.prototxt中网络输出数据层名称一致 param { lr_mult: 1 decay_mult: 1 } param { lr_mult: 2 decay_mult: 0 } inner_product_param { num_output: 1000 # 与train_val.prototxt中网络输出类别数一致 } } layer { name: "prob" type: "Softmax" bottom: "fc8" top: "prob" } 3.3 模型训练 修改 solver.prototxt net: "/path/to/alexnet/train_val.prototxt" test_iter: 1000 # batch_size * test_iter 约等于验证集大小 test_interval: 4000 # 迭代多少次测试一次 test_initialization: false display: 40 average_loss: 40 base_lr: 0.01 lr_policy: "step" stepsize: 320000 gamma: 0.96 max_iter: 10000000 momentum: 0.9 weight_decay: 0.0002 snapshot: 40000 snapshot_prefix: "/path/to/alexnet" # 训练的 caffemodel 保存位置 solver_mode: GPU 修改并运行 trainnet.sh #!/usr/bin/env sh ./build/tools/caffe train \ -solver /path/to/alexnet/solver.prototxt \ -gpu 0 运行脚本：./trainnet.sh 4. 图片测试利用 deploy.prototxt 和训练得到的 caffemodel 进行图片分类测试，其 python 脚本如下：# coding=utf-8 import numpy as np import caffe img = '/path/to/test.jpg' classes = '/class/names/define' # 分类的类别名 model_def = '/path/to/deploy.prototxt' weight_def = '/path/to/alexnet_iter_xxx.caffemodel' # 训练得到的 caffemodel net = caffe.Net(model_def, weight_def, caffe.TEST) # 网络和模型加载，TEST模式 # 图片预处理设置 transformer = caffe.io.Transformer({'data': net.blobs['data'].data.shape}) transformer.set_transpose('data', (2,0,1)) # 改变维度的顺序 transformer.set_mean('data', np.load(mean_file).mean(1).mean(1)) # 减均值 transformer.set_raw_scale('data', 255) # 缩放到[0，255] transformer.set_channel_swap('data', (2,1,0)) #将图片由RGB变为BGR im = caffe.io.load_image(img) # 测试图片加载 net.blobs['data'].data[...] = transformer.preprocess('data',im) # 测试图片预处理，并载入到blob out = net.forward() prob = net.blobs['Softmax1'].data[0].flatten() # 网络输出层(Softmax)值，表示属于某个类别的概率值 idx = prob.argsort()[-1] # 排序概率，取最大值的索引 print 'the class is:', classes[idx] # 将最大值索引转换成对应的类别名称