Fine tuning 模型微调

一. 什么是微调

针对某一个任务,当自己训练数据不多时,我们可以找一个同类的别人训练好的模型,换成自己的数据,调整一下参数,再训练一遍,这就是微调。

为什么要微调
  1. 数据集本身很小,从头开始训练具有几千万参数的大型神经网络是不现实的。
  2. 降低训练成本
  3. 站在巨人的肩膀上,没必要重复造轮子
迁移学习

迁移学习几乎都是用在图像识别方向的。
迁移学习的初衷是节省人工标注样本的时间,让模型可以通过一个已有的标记数据的领域向未标记的数据领域进行迁移,从而训练出适用该领域的模型。

迁移学习按照学习方式可分为:

  • 基于样本的迁移
  • 基于特征的迁移
  • 基于模型的迁移
  • 基于关系的迁移
微调与迁移学习的关系

二者没有严格的区分,含义可以互相交换,微调更常用于形容迁移学习的后期微调中。

二. 如何微调

对于不同的领域微调的方法也不一样,比如语音领域一般微调前几层,图片识别微调后几层。

对于图片来说,我们CNN的前几层学习到的都是低级的特征,比如,点、线、面,这些低级的特征对于任何图片来说都是可以抽象出来的,所以我们将他作为通用数据,只微调这些低级特征组合起来的高级特征即可,例如,这些点、线、面,组成的是圆还是椭圆,还是正方形,这些代表的含义是我们需要后面训练出来的。

对于语音来说,每个单词表达的意思都是一样的,只不过发音或者是单词的拼写不一样,比如 苹果,apple,apfel(德语),都表示的是同一个东西,只不过发音和单词不一样,但是他具体代表的含义是一样的,就是高级特征是相同的,所以我们只要微调低级的特征就可以了。

下面介绍CV方向的微调:(为什么基本上深度学习样例都是CV的,没有个NLP的嘛)

  • ConvNet as fixed feature extractor.: 其实这里有两种做法:
  1. 使用最后一个fc layer之前的fc layer获得的特征,学习个线性分类器(比如SVM)
  2. 重新训练最后一个fc layer
  • Fine-tuning the ConvNet
    固定前几层的参数,只对最后几层进行fine-tuning,

对于上面两种方案有一些微调的小技巧,比如先计算出预训练模型的卷积层对所有训练和测试数据的特征向量,然后抛开预训练模型,只训练自己定制的简配版全连接网络。 这个方式的一个好处就是节省计算资源,每次迭代都不会再去跑全部的数据,而只是跑一下简配的全连接

  • Pretrained models

这个其实和第二种是一个意思,不过比较极端,使用整个pre-trained的model作为初始化,然后fine-tuning整个网络而不是某些层,但是这个的计算量是非常大的,就只相当于做了一个初始化。

三. 微调实例

现在我们使用官方训练好的resnet50来对狗的种类识别进行一个简单的微调。
首先,下载官方数据解压到data中

import torch,os,torchvision
import torch.nn as nn
import torch.nn.functional as F
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from torch.utils.data import DataLoader, Dataset
from torchvision import datasets, models, transforms
from PIL import Image
from sklearn.model_selection import StratifiedShuffleSplit

#指定目录位置读取
DATA_ROOT = 'data'
all_labels_df = pd.read_csv(os.path.join(DATA_ROOT,'labels.csv'))
all_labels_df.head()

#定义两个字典,分别以id和名字作为对应
breeds = all_labels_df.breed.unique()
breed2idx = dict((breed,idx) for idx,breed in enumerate(breeds))
idx2breed = dict((idx,breed) for idx,breed in enumerate(breeds))
len(breeds)

#添加到列表中
all_labels_df['label_idx'] = [breed2idx[b] for b in all_labels_df.breed]
all_labels_df.head()

#定义数据集
class DogDataset(Dataset):
    def __init__(self, labels_df, img_path, transform=None):
        self.labels_df = labels_df
        self.img_path = img_path
        self.transform = transform
        
    def __len__(self):
        return self.labels_df.shape[0]
    
    def __getitem__(self, idx):
        image_name = os.path.join(self.img_path, self.labels_df.id[idx]) + '.jpg'
        img = Image.open(image_name)
        label = self.labels_df.label_idx[idx]
        
        if self.transform:
            img = self.transform(img)
        return img, label

#定义超参数
IMG_SIZE = 224 # resnet50的输入是224的所以需要将图片统一大小
BATCH_SIZE= 256 #这个批次大小需要占用4.6-5g的显存,如果不够的化可以改下批次,如果内存超过10G可以改为512
IMG_MEAN = [0.485, 0.456, 0.406]
IMG_STD = [0.229, 0.224, 0.225]
CUDA=torch.cuda.is_available()
DEVICE = torch.device("cuda" if CUDA else "cpu")

#定义训练和验证数据的图片转换规则
train_transforms = transforms.Compose([
    transforms.Resize(IMG_SIZE),
    transforms.RandomResizedCrop(IMG_SIZE),
    transforms.RandomHorizontalFlip(),
    transforms.RandomRotation(30),
    transforms.ToTensor(),
    transforms.Normalize(IMG_MEAN, IMG_STD)
])

val_transforms = transforms.Compose([
    transforms.Resize(IMG_SIZE),
    transforms.CenterCrop(IMG_SIZE),
    transforms.ToTensor(),
    transforms.Normalize(IMG_MEAN, IMG_STD)
])

#分割出10%作为验证集
dataset_names = ['train', 'valid']
stratified_split = StratifiedShuffleSplit(n_splits=1, test_size=0.1, random_state=0)
train_split_idx, val_split_idx = next(iter(stratified_split.split(all_labels_df.id, all_labels_df.breed)))
train_df = all_labels_df.iloc[train_split_idx].reset_index()
val_df = all_labels_df.iloc[val_split_idx].reset_index()
print(len(train_df))
print(len(val_df))

#dataloader加载数据
image_transforms = {'train':train_transforms, 'valid':val_transforms}

train_dataset = DogDataset(train_df, os.path.join(DATA_ROOT,'train'), transform=image_transforms['train'])
val_dataset = DogDataset(val_df, os.path.join(DATA_ROOT,'train'), transform=image_transforms['valid'])
image_dataset = {'train':train_dataset, 'valid':val_dataset}

image_dataloader = {x:DataLoader(image_dataset[x],batch_size=BATCH_SIZE,shuffle=True,num_workers=0) for x in dataset_names}
dataset_sizes = {x:len(image_dataset[x]) for x in dataset_names}

#开始配置网络,ImageNet是识别1000个物体,而狗的分类一共只有120类,所以要对模型的最后一层全连接层进行微调,将输出1000改为120  !!!!微调核心
model_ft = models.resnet50(pretrained=True) # 这里自动下载官方的预训练模型,并且
# 将所有的参数层进行冻结
for param in model_ft.parameters():
    param.requires_grad = False
# 这里打印下全连接层的信息
print(model_ft.fc)
num_fc_ftr = model_ft.fc.in_features #获取到fc层的输入
model_ft.fc = nn.Linear(num_fc_ftr, len(breeds)) # 定义一个新的FC层
model_ft=model_ft.to(DEVICE)# 放到设备中
print(model_ft) # 最后再打印一下新的模型

#设置训练参数
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam([
    {'params':model_ft.fc.parameters()}
], lr=0.001)#指定 新加的fc层的学习率

#定义训练函数
def train(model,device, train_loader, epoch):
    model.train()
    for batch_idx, data in enumerate(train_loader):
        x,y= data
        x=x.to(device)
        y=y.to(device)
        optimizer.zero_grad()
        y_hat= model(x)
        loss = criterion(y_hat, y)
        loss.backward()
        optimizer.step()
    print ('Train Epoch: {}\t Loss: {:.6f}'.format(epoch,loss.item()))

#定义测试函数
def test(model, device, test_loader):
    model.eval()
    test_loss = 0
    correct = 0
    with torch.no_grad():
        for i,data in enumerate(test_loader):          
            x,y= data
            x=x.to(device)
            y=y.to(device)
            optimizer.zero_grad()
            y_hat = model(x)
            test_loss += criterion(y_hat, y).item() # sum up batch loss
            pred = y_hat.max(1, keepdim=True)[1] # get the index of the max log-probability
            correct += pred.eq(y.view_as(pred)).sum().item()
    test_loss /= len(test_loader.dataset)
    print('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
        test_loss, correct, len(val_dataset),
        100. * correct / len(val_dataset)))
   
#训练
for epoch in range(1, 10):
    %time train(model=model_ft,device=DEVICE, train_loader=image_dataloader["train"],epoch=epoch)
    test(model=model_ft, device=DEVICE, test_loader=image_dataloader["valid"])
四. 固定层的向量导出
#采用hook处理
in_list= [] # 这里存放所有的输出
def hook(module, input, output):
    #input是一个tuple代表顺序代表每一个输入项,我们这里只有一项,所以直接获取
    #需要全部的参数信息可以使用这个打印
    #for val in input:
    #    print("input val:",val)
    for i in range(input[0].size(0)):
        in_list.append(input[0][i].cpu().numpy())

model_ft.avgpool.register_forward_hook(hook)
%%time
with torch.no_grad():
    for batch_idx, data in enumerate(image_dataloader["train"]):
        x,y= data
        x=x.to(DEVICE)
        y=y.to(DEVICE)
        y_hat = model_ft(x)

features=np.array(in_list)
np.save("features",features)