数据处理部分
VGG_Net包含两个部分,一个是特征提取部分,一个是分类器部分。
实际应用中,由于样本的缺失,很少有人会从头开始训练整个卷积神经网络(以随机初始化权重的方式)。相反,通常的做法是在一个很大的数据集上对神经网络进行预训练(例如ImageNet,该网络包含1000种类别的100多万张图片),然后将训练好的卷积神经网络用来权重初始化或者作为特征提取器。
这两个迁移学习的主要场景如下:
1. 对卷积神经网络进行微调(Finetuning the convnet):与随机初始化的方式不同,该方法通过一个已经经过预训练的网络来对神经网络的权重进行初始化(例如使用上文中所说到的在Image Net上训练得到的网络)剩下的训练部分与普通的网络训练方式一样。
2. 卷积神经网络作为一个特征提取器(ConvNet as fixed feature extractor): 在这种方式下,神经网络的权重除了全连接层外都不会再发生变化,也就是权重的大小会被固定下来。而最后的全连接层会通过随机的方式对其权重进行初始化,并且在对该网络进行训练的过程中只对当前层进行训练。
Pytorch提供了很好的数据预处理的封装。本文所讨论的是人脸性别属于分类问题,在对分类的数据进行处理的时候,可以使用Pytorch提供的ImageFolder
类来实现数据预处理。
例如,在本次训练中,要训练的数据分为train
和val
两个部分。
文件目录如下所示:
.
├── train
│ ├── man
│ └── woman
└── val
├── man
└── woman
首先需要定义数据集的根目录:
data_dir = '/home/xx/workspace/wiki_crop/gender/'
然后,对于train
和val
这两个分别使用ImageFolder
处理:
image_datasets = {x: datasets.ImageFolder(os.path.join(data_dir, x))
for x in ['train','val']}
这时,ImageFolder
已经完成了照片数据的分类,并将这些图片的分类信息放倒了image_datasets
变量中,
image_datasets
变量的结构如下所示:
可以看到,ImageFolder
类已经将性别man
,woman
做好了分类,并赋值为0和1。并且,训练数据以及测试数据被很好的分开。
有了ImageFolder
获取到的image_datasets
,这里只是找到了数据的路径以及相对应的类别,Pytorch
还提供了DataLoader
类,用于在训练时,实时获取数据对应的训练数据。代码如下:
dataloaders = {x: torch.utils.data.DataLoader(image_datasets[x], batch_size=4,
shuffle=True, num_workers=4)
for x in ['train', 'val']}
DataLoader
的第一个参数为上面获取到的image_datasets
,第二个参数为batch_size
,表示的是批训练时每批样本的数量。参数shuffle
表示的是是否打乱数据的顺序,True
表示打乱。参数num_workers
表示参与计算的CPU
核心数。
数据预处理
Pytorch
提供了一个数据预处理的操作对象。定义如下:
data_transforms = {
'train': transforms.Compose([
# transforms.RandomResizedCrop(224),
transforms.CenterCrop(224),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
]),
'val': transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
]),
}
data_transforms
对象在ImageFolder
进行数据处理的时候作为参数传入,可以将上面数据处理的代码改为如下形式:
image_datasets = {x: datasets.ImageFolder(os.path.join(data_dir, x),
data_transforms[x])
for x in ['train','val']}
建立网络模型
Pytorch
提供了很多训练好的网络模型的实现,这些网络结构都是成熟的算法,可以直接拿来用。
出于算法速度的考虑,本文选取了一种较为快速的模型训练方式,即将卷积神经网络作为特征提取器,将提取出的特征送到全连接层进行训练。
使用到的代码如下所示:
model_conv = torchvision.models.resnet18(pretrained=True)
#冻结参数,不训练
for param in model_conv.parameters():
param.requires_grad = False
# Parameters of newly constructed modules have requires_grad=True by default
num_ftrs = model_conv.fc.in_features
有了模型以后,需要对该模型进行优化。优化的好不好,需要有一个评价指标,如下所示为对于该评价指标的定义:
# 定义交叉熵代价函数
criterion = nn.CrossEntropyLoss()
有了代价函数以后,在优化模型的时候还需要一个优化器。这里选择了随机梯度下降算法,定义如下:
optimizer_conv = optim.SGD(model_conv.fc.parameters(), lr=0.001, momentum=0.9)
在执行梯度优化的时候,需要设置一个学习率,Pytorch
提供了一个学习率自动递减的学习率对象,定义如下:
# Decay LR by a factor of 0.1 every 7 epochs
exp_lr_scheduler = lr_scheduler.StepLR(optimizer_conv, step_size=7, gamma=0.1)
有了这些以后,就可以开始对模型进行训练了。
模型训练
本次教程主要使用了在第一章中提到的模型训练方法:将卷积神经网络作为特征提取器。因此在训练神经网络的时候,只会对当前网络模型的全连接层进行训练(这里的全连阶层是自己定义的)。训练的流程定义如下函数所示,局部重要内容会有汉语标注:
# 训练模型
# 参数说明:
# model:待训练的模型
# criterion:评价函数
# optimizer:优化器
# scheduler:学习率
# num_epochs:表示实现完整训练的次数,一个epoch表示一整個训练周期
def train_model(model, criterion, optimizer, scheduler, num_epochs=25):
# 定义训练开始时间
since = time.time()
#用于保存最优的权重
best_model_wts = copy.deepcopy(model.state_dict())
#最优精度值
best_acc = 0.0
# 对整个数据集进行num_epochs次训练
for epoch in range(num_epochs):
print('Epoch {}/{}'.format(epoch, num_epochs - 1))
print('-' * 10)
# Each epoch has a training and validation phase
# 每轮训练训练包含`train`和`val`的数据
for phase in ['train', 'val']:
if phase == 'train':
# 学习率步进
scheduler.step()
# 设置模型的模式为训练模式(因为在预测模式下,采用了`Dropout`方法的模型会关闭部分神经元)
model.train() # Set model to training mode
else:
# 预测模式
model.eval() # Set model to evaluate mode
running_loss = 0.0
running_corrects = 0
# Iterate over data.
# 遍历数据,这里的`dataloaders`近似于一个迭代器,每一次迭代都生成一批`inputs`和`labels`数据
for inputs, labels in dataloaders[phase]:
inputs = inputs.to(device) # 当前批次的训练输入
labels = labels.to(device) # 当前批次的标签输入
# 将梯度参数归0
optimizer.zero_grad()
# 前向计算
# track history if only in train
with torch.set_grad_enabled(phase == 'train'):
# 相应输入对应的输出
outputs = model(inputs)
# 取输出的最大值作为预测值preds
_, preds = torch.max(outputs, 1)
# 计算预测的输出与实际的标签之间的误差
loss = criterion(outputs, labels)
# backward + optimize only if in training phase
if phase == 'train':
# 对误差进行反向传播
loss.backward()
# 执行优化器对梯度进行优化
optimizer.step()
# statistics
# 计算`running_loss`和`running_corrects`
running_loss += loss.item() * inputs.size(0)
running_corrects += torch.sum(preds == labels.data)
# 当前轮的损失
epoch_loss = running_loss / dataset_sizes[phase]
# 当前轮的精度
epoch_acc = running_corrects.double() / dataset_sizes[phase]
print('{} Loss: {:.4f} Acc: {:.4f}'.format(phase, epoch_loss, epoch_acc))
# deep copy the model
# 对模型进行深度复制
if phase == 'val' and epoch_acc > best_acc:
best_acc = epoch_acc
best_model_wts = copy.deepcopy(model.state_dict())
print()
# 计算训练所需要的总时间
time_elapsed = time.time() - since
print('Training complete in {:.0f}m {:.0f}s'.format(time_elapsed // 60, time_elapsed % 60))
print('Best val Acc: {:4f}'.format(best_acc))
# load best model weights
# 加载模型的最优权重
model.load_state_dict(best_model_wts)
return model
可视化预测结果
定义如下函数,对训练好的模型进行测试:
def visualize_model(model, num_images=6):
was_training = model.training
model.eval()
images_so_far = 0
fig = plt.figure()
with torch.no_grad():
for i, (inputs, labels) in enumerate(dataloaders['val']):
inputs = inputs.to(device)
labels = labels.to(device)
outputs = model(inputs)
_, preds = torch.max(outputs, 1)
for j in range(inputs.size()[0]):
images_so_far += 1
ax = plt.subplot(num_images//2, 2, images_so_far)
ax.axis('off')
ax.set_title('predicted: {}'.format(class_names[preds[j]]))
imshow(inputs.cpu().data[j])
if images_so_far == num_images:
model.train(mode=was_training)
return
model.train(mode=was_training)
使用自定义的网络结构
上面讲了如何使用现有的神经网络模型来实现本文的人脸性别识别任务。那么如果想自己定义一个网络结构来实现这样的任务该怎么做呢?此时我们可以使用自定义的网络结构来实现。下面所示的代码定义了一个性别识别的模型:
import torch
import torch.nn as nn
import torch.nn.functional as F
class GENDER_Net(nn.Module):
def __init__(self):
super(GENDER_Net, self).__init__()
# 第一个卷积层,核的大小为5*5
self.conv1 = nn.Conv2d(3, 6, kernel_size=5)
# 第二个卷积层,核的大小为5*5
self.conv2 = nn.Conv2d(6, 10, kernel_size=5)
# 第一个全连接层,第一个参数输入的维度取决于卷积后图像特征的维度,第二个参数为输出维度可以任意
self.fc1 = nn.Linear(5000, 800)
# 定义一个Dropout失活层,若不手动定义参数,则默认参数为p=0.5
self.fc1_drop = nn.Dropout2d()
# 定义全连接层的输出
self.fc2 = nn.Linear(800, 2)
# 前向传播的过程
def forward(self, x):
x = self.conv1(x)
x = F.max_pool2d(x,2)
x = F.relu(x)
# x = F.relu(F.max_pool2d(self.conv1(x), 2))
x = self.conv2(x)
x = self.conv2_drop(x)
x = F.max_pool2d(x,2)
x = F.relu(x)
# x = F.relu(F.max_pool2d(self.conv2_drop(self.conv2(x)), 2))
x = x.view(-1, 5000)
x = self.fc1(x)
x = F.relu(x)
# x = F.relu(self.fc1(x))
x = F.dropout(x, training=self.training)
x = self.fc2(x)
# softmax输出,网络的预测输出对应于输出值最大时所处的位置
return F.softmax(x)
在使用自定义模型时,直接使用下面代码进行声明:
model = GENDER_Net()
模型的保存与调用
保存
模型训练好以后,需要将其保存到本地,以便下一次直接使用。下面的代码显示了如何保存模型:
torch.save(model,"GenderModel.pkl")
调用
下面的代码演示,如何调用训练好的、保存到本地的模型:
model = torch.load("GenderModel.pkl")