现在我们已经准备好了Dataloaders,之后要定义神经网络并训练它。为了定义一个神经网络,最好的方法是定义类来分离和抽象所有类型网络的通用功能,如训练循环,验证,评估,预测,设置不同的超参数等。
我们还需要定义实现特定类型网络的类,例如专门用于迁移学习的类,或为全连接操作的类等等。我们将创建三个主要类:
让我们一步步来创建名叫Network的基类
'''
从创建神经网络的Pytorch核心类nn.Module继承我们的类
'''
class Network(nn.Module):
def __init__(self,device=None):
'''
我们调用父类的构造函数
'''
super().__init__()
'''
如果过用gpu,我们就设置设备属性为‘cuda’,否则设置为‘cpu'
这将帮助我们避免在代码中到处检查是否有CUDA可用
'''
if device is not None:
self.device = device
else:
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
'''
我们创建一个虚构的forward方法。forward方法是Pytorch中的核心方法,它执行网络图并通过网络传递输入,将输入转换后在另一端获得输出
在Pytorch中,我们编写自己的forward方法,该方法在运行时执行在__init__方法中定义的模块
因为我们将在派生类中编写forward方法,所以在基类中它是空的
'''
def forward(self,x):
pass
注意forward方法通过nn.Module的“call”方法调用,因此,我们的类对象可以成为一个“可调用的”对象,当它被调用时,forward方法将被自动调用。
1. 接下来,我们添加了train方法。对于任何神经网络的训练,在训练循环的每次迭代中都需要执行一些常见的任务。这部分代码遍历每个批处理。这里定义了一个single epoch(单遍历整个数据集):
如果你熟悉神经网络的基础知识,那么你肯定知道这些步骤,因为它们对于所有框架和神经网络类型都是通用的。train_方法中的以下代码执行这些步骤。代码是非常好解释的,下面是Pytorch特定函数的简要介绍:
class Network(nn.Module):
...
'''
print_every代表我们要打印多少批之后的损失信息
'''
def train_(self,trainloader,criterion,optimizer,print_every):
'''
下面的train方法(self.train())是基类(nn.Module)中的一个内置Pytorch方法,它在模型对象上设置一个标志,代表正在训练
此标志在几个Pytorch模块使用,这些模块在训练和验证/测试期间表现不同,例如Dropout、batch normalization等
'''
self.train()
t0 = time.time()
batches = 0
running_loss = 0
'''
inputs和labels分别来自trainloader的一批图像及其对应的标签
'''
for inputs, labels in trainloader:
batches += 1
#t1 = time.time()
inputs, labels = inputs.to(self.device), labels.to(self.device)
optimizer.zero_grad()
outputs = self.forward(inputs)
'''
Criterion是基本的损失函数,计算网络输出和实际标签的差异
'''
loss = criterion(outputs, labels)
'''
loss.backward()实现反向传播,计算根据连通张量的完全图计算网络中的梯度
'''
loss.backward()
'''
损失函数执行完后,Optimizer.step实现优化算法的一步,产生新的梯度
'''
optimizer.step()
'''
item()给出一个标量值,它用于返回单个值的张量(在本例中,loss是一个浮点数值)
'''
loss = loss.item()
#print('training this batch took {:.3f} seconds'.format(time.time() - t1))
'''
计算一个epoch完整的loss
'''
running_loss += loss
'''
输出loss信息,如果批的数量已经达到print_every
'''
if batches % print_every == 0:
print(f"{time.asctime()}.."
f"Time Elapsed = {time.time()-t0:.3f}.."
f"Batch {batches+1}/{len(trainloader)}.. "
f"Average Training loss: {running_loss/(batches):.3f}.. "
f"Batch Training loss: {loss:.3f}.. "
)
t0 = time.time()
'''
最后我们返回该epoch的平均loss
'''
return running_loss/len(trainloader)
2. 损失函数
注意,Pytorch附带了许多内置的损失函数,用于分类和回归等常见情况。这里我们将损失函数作为参数传递给train_。在分类中使用的一些常见的损失函数有交叉损失、Negative Likehood Log损失(NLLLoss)和二进制交叉损失。在本教程稍后讨论全连接类时,我们将更多地讨论损失函数。
3. 优化器模块
优化器模块应用梯度下降或其变体,执行梯度和学习率的更新。优化器有几种不同的算法,可以在torch.optim模块中找到。例子包括随机梯度下降(SGD),Adam,AdaDelta等。
验证是将模型应用到验证集进行评估。目的是定期评估我们在训练方面的性能。如果你熟悉机器学习的概念,你很可能知道bias(拟合不足)和variance(过拟合)。如果我们在验证集上的损失显著且始终的高于在训练集上的损失,就是过拟合。这基本上意味着我们的模型在任何其他数据集上都不能泛化得足够好,因为模型与训练集的联系太紧密了。
from collections import defaultdict
def update_classwise_accuracies(preds,labels,class_correct,class_totals):
correct = np.squeeze(preds.eq(labels.data.view_as(preds)))
'''
我们只需遍历batch(shape[0]是batch大小)并更新classwise正确的数量和总的数量
'''
for i in range(labels.shape[0]):
label = labels.data[i].item()
class_correct[label] += correct[i].item()
class_totals[label] += 1
class Network(nn.Module):
...
def validate_(self,validloader):
running_loss = 0.
accuracy = 0
'''
创建两个默认字典来存储每个类classwise正确的预测和总的图片数
'''
class_correct = defaultdict(int)
class_totals = defaultdict(int)
'''
self.eval()是一个将模型放入验证模式的Pytorch方法。它告诉Pytorch,我们只想在网络中执行正向传递,而不需要反向传播。它与我们在训练循环中使用的训练方法相反
'''
self.eval()
'''
无论我们在torch.no_grad()块中放入什么,都会告诉Pytorch不要计算梯度。
我们要确保在计算循环中梯度永远不会被计算。
'''
with torch.no_grad():
for inputs, labels in validloader:
inputs, labels = inputs.to(self.device), labels.to(self.device)
outputs = self.forward(inputs)
loss = self.criterion(outputs, labels)
running_loss += loss.item()
_, preds = torch.max(torch.exp(outputs), 1) # you can safely remove the call to
#torch.exp(as described below)
update_classwise_accuracies(preds,labels,class_correct,class_totals)
'''
用前面讨论过的简单公式来计算精度。
'''
accuracy = (100*np.sum(list(class_correct.values()))/np.sum(list(class_totals.values())))
'''
把模型放回训练模式
'''
self.train()
'''
Running loss是所有批的loss,用它除以trainloader的长度(批次的数量)就得到了整个验证集的平均损失
'''
return (running_loss/len(validloader),accuracy)
评估方法的目的是在完成训练后在测试集上评估模型的性能。假设我们为要传递给这个方法的数据集提供了可用的标签。
代码几乎与validate相同。唯一的区别是我们不需要计算这种情况下的损失,因为我们已经完成了训练。
因为这个方法返回总体精度和类级精度,所以我们需要另一个实用函数get_accuracies。我们还需要class_names来获得类的实际名称。在创建迁移学习模型时(本教程稍后),我们将把类名存储为字典映射ids(numbers)到类名字符串。
from collections import defaultdict
def update_classwise_accuracies(preds,labels,class_correct,class_totals):
correct = np.squeeze(preds.eq(labels.data.view_as(preds)))
for i in range(labels.shape[0]):
label = labels.data[i].item()
class_correct[label] += correct[i].item()
class_totals[label] += 1
def get_accuracies(class_names,class_correct,class_totals):
accuracy = (100*np.sum(list(class_correct.values()))/np.sum(list(class_totals.values())))
'''
我们获得这个类的名称,并通过用这个类的正确预测除以在测试数据集中拥有的这个类的图像的总数来得到这个类的准确性
我们添加了一个额外的条件,即我们至少有一个类的图片,以避免除以0
'''
class_accuracies = [(class_names[i],100.0*(class_correct[i]/class_totals[i]))
for i in class_names.keys() if class_totals[i] > 0]
return accuracy,class_accuracies
class Network(nn.Module):
...
def evaluate(self,testloader):
self.eval()
self.model.to(self.device)
class_correct = defaultdict(int)
class_totals = defaultdict(int)
with torch.no_grad():
for inputs, labels in testloader:
inputs, labels = inputs.to(self.device), labels.to(self.device)
outputs = self.forward(inputs)
ps = torch.exp(outputs)
_, preds = torch.max(ps, 1)
update_classwise_accuracies(preds,labels,class_correct,class_totals)
self.train()
return get_accuracies(self.class_names,class_correct,class_totals)
预测方法用于从训练模型中预测或得出推断,以确定我们没有标签的图像的类别。这是在实际部署模型时将调用的方法。
class Network(nn.Module):
...
'''
因为我们需要概率和(可能的)多个类的排序,我们传递topk参数,它告诉我们的函数有多少排序类和它们的概率
'''
def predict(self,inputs,topk=1):
self.eval()
self.model.to(self.device)
with torch.no_grad():
inputs = inputs.to(self.device)
outputs = self.forward(inputs)
ps = torch.exp(outputs)
p,top = ps.topk(topk, dim=1)
return p,top
Pytorch中张量的topk方法返回k个指标和它们在一个维度上的值(dim=1表示行,也就是水平方向上的值)。因为张量是50倍的类数,这将返回topk类和它们每一行的概率)。
这是我们类在启动训练时调用的主要方法。它实现了epoch循环的主训练循环。
它调用train_方法,定期调用验证来监视性能和过拟合等,追踪到目前为止获得的最佳精度,保存最佳精度模型,将完整模型及其超参数和其他变量保存到磁盘作为checkpoint。如果由于某种原因断电或训练中断,可以恢复checkpoint并在以后继续训练。
让我们一步一步来构建这个方法:
class Network(nn.Module):
...
def fit(self,trainloader,validloader,epochs=2,print_every=10,validate_every=1):
for epoch in range(epochs):
'''
将模型移到设备('gpu'或'cpu')
'''
self.model.to(self.device)
print('epoch {:3d}/{}'.format(epoch+1,epochs))
epoch_train_loss = self.train_(trainloader,self.criterion,
self.optimizer,print_every)
'''
检查是否需要在每个validate_every epochs之后调用validate,调用它并输出验证损失和准确性
'''
if validate_every and (epoch % validate_every == 0):
t2 = time.time()
epoch_validation_loss,epoch_accuracy = self.validate_(validloader)
time_elapsed = time.time() - t2
print(f"{time.asctime()}--Validation time {time_elapsed:.3f} seconds.."
f"Epoch {epoch+1}/{epochs}.. "
f"Epoch Training loss: {epoch_train_loss:.3f}.. "
f"Epoch validation loss: {epoch_validation_loss:.3f}.. "
f"validation accuracy: {epoch_accuracy:.3f}")
self.train()
拟合函数还应该监视到目前为止在所有epoch中获得的最佳精度,并在获得一个比以前更好的新模型时保存最佳精度模型。这确保了即使没有检查点,如果在训练期间验证损失开始下降,我们也应该能够检索出我们的最佳模型。
这是一个常见的场景,因为训练可能需要几个小时才能完成,我们可能不得不离开系统。这样我们可以确保我们总是重新加载最佳精度模型的权重,并使用它们进行推断。
from collections import defaultdict
import math
class Network(nn.Module):
def __init__(self,device=None):
...
'''
初始化best_accuracy为0.
'''
self.best_accuracy = 0.
...
def fit(self,trainloader,validloader,epochs=2,print_every=10,validate_every=1):
for epoch in range(epochs):
self.model.to(self.device)
print('epoch {:3d}/{}'.format(epoch+1,epochs))
epoch_train_loss = self.train_(trainloader,self.criterion,
self.optimizer,print_every)
if validate_every and (epoch % validate_every == 0):
t2 = time.time()
epoch_validation_loss,epoch_accuracy = self.validate_(validloader)
time_elapsed = time.time() - t2
print(f"{time.asctime()}--Validation time {time_elapsed:.3f} seconds.."
f"Epoch {epoch+1}/{epochs}.. "
f"Epoch Training loss: {epoch_train_loss:.3f}.. "
f"Epoch validation loss: {epoch_validation_loss:.3f}.. "
f"validation accuracy: {epoch_accuracy:.3f}")
'''
如果验证返回更好的精度,检查并保存最佳精度模型
'''
if self.best_accuracy == 0. or (epoch_accuracy > self.best_accuracy):
print('updating best accuracy: previous best = {:.3f} new best = {:.3f}'.format(self.best_accuracy,
epoch_accuracy))
self.best_accuracy = epoch_accuracy
'''
Pytorch保存方法通过使用Python的Pickle模块序列化Pytorch张量数据结构来保存。在这里,我们存储由state_dict()方法返回的模型状态字典,该方法 包含模型全图的所有权值(体系结构中的每个张量)
'''
torch.save(self.state_dict(),self.best_accuracy_file)
self.train() # just in case we forgot to put the model back to train mode in validate
print('loading best accuracy model')
'''
当我们完成训练循环时,我们恢复最佳精度的模型。这确保我们使用最佳精度的模型。
This ensures that any evaluation or inference we perform while the model
remains in memory shall be done using the best accuracy model instead of
the one obtained in the last iteration of the training loop.
'''
self.load_state_dict(torch.load(self.best_accuracy_file))
注意self.best_accuracy_file应该是在模型参数初始化期间设置的文件名(请参见下一节)
我们需要设置模型中不同的参数和超参数。包括损失函数(criterion),优化器,dropout概率,学习率和其他参数。我们写四个方法:
class Network(nn.Module):
...
def set_criterion(self,criterion_name):
if criterion_name.lower() == 'nllloss':
self.criterion_name = 'NLLLoss'
self.criterion = nn.NLLLoss()
elif criterion_name.lower() == 'crossentropyloss':
self.criterion_name = 'CrossEntropyLoss'
self.criterion = nn.CrossEntropyLoss()
def set_optimizer(self,params,optimizer_name='adam',lr=0.003):
from torch import optim
if optimizer_name.lower() == 'adam':
print('setting optim Adam')
self.optimizer = optim.Adam(params,lr=lr)
self.optimizer_name = optimizer_name
elif optimizer.lower() == 'sgd':
print('setting optim SGD')
self.optimizer = optim.SGD(params,lr=lr)
elif optimizer.lower() == 'adadelta':
print('setting optim Ada Delta')
self.optimizer = optim.Adadelta(params)
def set_model_params(self,
criterion_name,
optimizer_name,
lr, # learning rate
dropout_p,
model_name,
best_accuracy,
best_accuracy_file,
class_names):
self.set_criterion(criterion_name)
self.set_optimizer(self.parameters(),optimizer_name,lr=lr)
self.lr = lr
self.dropout_p = dropout_p
self.model_name = model_name
self.best_accuracy = best_accuracy
self.best_accuracy_file = best_accuracy_file
self.class_names = class_names
def get_model_params(self):
params = {}
params['device'] = self.device
params['model_name'] = self.model_name
params['optimizer_name'] = self.optimizer_name
params['criterion_name'] = self.criterion_name
params['lr'] = self.lr
params['dropout_p'] = self.dropout_p
params['best_accuracy'] = self.best_accuracy
params['best_accuracy_file'] = self.best_accuracy_file
params['class_names'] = self.class_names
return params
class Network(nn.Module):
...
'''
增加chkpoint_file参数到set_params函数
'''
def set_model_params(self,
criterion_name,
optimizer_name,
lr, # learning rate
dropout_p,
model_name,
best_accuracy,
best_accuracy_file,
chkpoint_file):
self.criterion_name = criterion_name
self.set_criterion(criterion_name)
self.optimizer_name = optimizer_name
self.set_optimizer(self.parameters(),optimizer_name,lr=lr)
self.lr = lr
self.dropout_p = dropout_p
self.model_name = model_name
self.best_accuracy = best_accuracy
print('set_model_params: best accuracy = {:.3f}'.format(self.best_accuracy))
self.best_accuracy_file = best_accuracy_file
self.chkpoint_file = chkpoint_file
def get_model_params(self):
params = {}
params['device'] = self.device
params['model_name'] = self.model_name
params['optimizer_name'] = self.optimizer_name
params['criterion_name'] = self.criterion_name
params['lr'] = self.lr
params['dropout_p'] = self.dropout_p
params['best_accuracy'] = self.best_accuracy
print('get_model_params: best accuracy = {:.3f}'.format(self.best_accuracy))
params['best_accuracy_file'] = self.best_accuracy_file
params['chkpoint_file'] = self.chkpoint_file
print('get_model_params: chkpoint file = {}'.format(self.chkpoint_file))
return params
def save_chkpoint(self):
saved_model = {}
'''
通过get_model_params获取所有参数和类名,并将它们存储到chkpoint文件中
'''
saved_model['params'] = self.get_model_params()
torch.save(saved_model,self.chkpoint_file)
print('checkpoint created successfully in {}'.format(self.chkpoint_file))