概述
在深度神经网络算法的应用过程中,如果我们面对的是数据规模较大的问题,那么在搭建好深度神经网络模型后,我们势必要花费大量的算力和时间去训练模型和优化参数,最后耗费了这么多资源得到的模型只能解决这一个问题,性价比非常低。
如果我们用这么多资源训练的模型能够解决同一类问题,那么模型的性价比会提高很多,这就促使使用迁移模型解决同一类问题的方法出现。因为该方法的出现,我们通过对一个训练好的模型进行细微调整,就能将其应用到相似的问题中,最后还能取得很好的效果;另外,对于原始数据较少的问题,我们也能够通过采用迁移模型进行有效解决,所以,如果能够选取合适的迁移学习方法,则会对解决我们所面临的问题有很大帮助。
假如我们现在需要解决一个计算机视觉的图片分类问题,需要通过搭建一个模型对猫狗图片进行分类,并且提供了大量的猫和狗的图片数据集。
假如我们选择使用卷积神经网络模型来解决这个图片分类问题,则首先要搭建模型,然后不断对模型进行训练,使其预测猫和狗的图片的准确性达到要求的阈值,在这个过程中会消耗大量的时间在参数优化和模型训练上。
不久之后我们又面临另一个图片分类问题,这次需要搭建模型对猫和狗的图片进行分类,同样提供了大量的图片数据集,如果已经掌握了迁移学习方法,就不必再重新搭建一套全新的模型,然后耗费大量的时间进行训练了,可以直接使用之前已经得到的模型和模型的参数并稍加改动来满足新的需求。不过,对迁移的模型需要进行重新训练,这是因为最后分类的对象发生了变化,但是重新训练的时间和搭建全新的模型进行训练的时间相对很少,如果调整的仅仅是迁移模型的一小部分,那么重新训练所耗费的时间会更少。通过迁移学习可以节省大量的时间和精力,而且最终得到的结果不会太差,这就是迁移学习的优势和特点。
本次采用的数据集是来自Kaggle网站上的"Dog vs. Cats"竞赛项目,可以通过网络免费下载这些数据集
在实践中,我们不会直接使用测试数据集对搭建的模型进行训练和优化,而是在训练数据集中划出一部分作为验证集,来评估在每个批次的训练后模型的泛化能力。
这样做的原因是如果我们使用测试数据集进行模型训练和优化,那么模型最终会对测试数据集产生拟合倾向,换而言之,你用两个二元一次方程去求出来的解,将解代回原方程的话是不是必然成立的。再往回带解是不是就没有意义了
所以,为了防止这种情况的出现,我们会把测试数据集从模型的训练和优化过程中隔离出来,只在每轮训练结束后使用。如果模型对验证数据集和测试数据集的预测同时具备高准确率和低损失值,就基本说明模型的参数优化是成功的。
数据预览
在划分好数据集之后,我们就可以进行数据预览了。我们通过数据预览可以掌握数据的基本信息,从而更好地决定如何使用这些数据
开始部分的代码如下:
from torchvision import models
import torch
from torch.autograd import Variable
from torchvision import datasets, transforms
import os
import matplotlib.pyplot as plt
import time
在以上代码中导入了必要的包,且新增了os包和time包
os包集成了一些对文件路径和目录进行操作的类
time包主要是一些和时间相关的方法。
在获取全部数据集之后,我们就可以的对这些文件进行一些简单的分类了:
① 新建一个名为DogVSCats的文件夹
②在该文件夹下面新建一个名为train和一个名为valid的子文件夹,
③在子文件夹下面再分别新建一个名为cat的文件夹和一个名为dog的文件夹,
④最后将数据集中对应部分的数据放到对应名字的文件夹中。
之后就可以进行数据的载入了,对数据进行载入的代码如下:
data_dirs = "DogsVSCats"
"""将数据集的原始图片的大小统一缩放至64×64,并转化为PyTorch可识别和计算的Tensor类型"""
data_transform = {x: transforms.Compose([transforms.Scale([64, 64]), transforms.ToTensor(), transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5])]) # 传入的零九秒里面,size
# 只能是一个整型数据,size是指缩放后图片最小边的边长。 举个例子,如果原图的height>width,那么改变大小后的图片大小是(size*height/width, size)。
for x in ["train", "valid"]}
image_datasets = {x: datasets.ImageFolder(root=os.path.join(data_dirs, x), transform=data_transform[x])
for x in ["train", "valid"]}
dataloader = {x: torch.utils.data.DataLoader(dataset=image_datasets[x],
batch_size=1,
shuffle=True)
for x in ["train", "valid"]
}
下面获取一个批次的数据并进行数据预览和分析,代码如下:
# 通过next和iter迭代操作获取一个批次的装载数据
X_example, y_example = next(iter(dataloader["train"]))
print(u"X_example个数{}".format(len(X_example)))
print(u"y_example个数{}".format(len(y_example)))
因为受到我们之前定义的batch_size值的影响,这一批次的数据只有16张图片,所以X_example和y_example的长度也全部是16,可以通过打印这两个变量来确认。
输入结果如下:
其中,X_example是维度为(16, 3, 64, 64)的Tensor数据类型的变量
16代表在这个批次中有16张图片;
3代表色彩通道数(RGB通道);
64代表图片的宽度值和高度值
y_example也是Tensor数据类型的变量,不过其中的元素全部是0和1。为什么是0和1呢?这是因为在进行数据装载时已经对dog文件夹和cat文件夹下的内容进行了独热编码,所以这时的0和1不仅是每张图片的标签,还分别对应猫的图片和狗的图片。我们可以做一个简单的打印输出,来验证这个独热编码的对应关系。
代码如下:
index_classes = image_datasets["train"].class_to_idx
print(index_classes)
输出的结果如下:
这样就很明显了,猫的图片标签和狗的图片标签被独热编码后分别被数字化了,相较于使用文字作为图片的标签而言,使用0和1也可以让之后的计算方便许多.不过,为了增加之后绘制的图形标签的可识别性,我们还需要通过image_datasets[“train”].classes将原始标签的结果存储在名为example_classes的变量中。
代码如下:
example_clasees = image_datasets["train"].classes
print(example_clasees)
输出内容如下:
我们使用Matplotlib对一个批次的图片进行绘制,具体的代码如下:
img = torchvision.utils.make_grid(X_example)
#因为在plt.imshow在实现的时候输入的是(imagesize, imagesize, channels),而imshow中参数的格式为(channels, imagesize, imagesize),所以需要用numpy().transpose(1,2,0)进行格式转换
img = img.numpy().transpose([1, 2, 0])
print([example_clasees[i] for i in y_example])
plt.imshow(img)
plt.show()
打印输出的该批次的所有图片的标签结果如下:
标签对应的图片结果如下:
模型搭建和参数优化
我们首先需要搭建一个卷积神经网络模型,考虑到训练时间的成本,我们基于VGG16架构来搭建一个简化版的VGGNet模型,这个简化版模型要求输入的图片大小全部缩放到64×64,而在标准的VGG16架构模型中输入的图片大小应该是224×224;同时简化版删除了VGG16最后的三个卷积层和池化层,也改变了全连接层中的连接参数,这一系类的改变都是为了减少整个模型参与训练的参数数量。
简化版模型的搭建代码如下:
# 搭建模型
class Models(torch.nn.Module):
def __init__(self):
super().__init__()
self.Conv = torch.nn.Sequential(
torch.nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1),
torch.nn.ReLU(),
torch.nn.Conv2d(64, 64, kernel_size=3, padding=1),
torch.nn.ReLU(),
torch.nn.MaxPool2d(kernel_size=2, stride=2),
torch.nn.Conv2d(64, 128, kernel_size=3, stride=1, padding=1),
torch.nn.ReLU(),
torch.nn.Conv2d(128, 128, kernel_size=3, stride=1, padding=1),
torch.nn.ReLU(),
torch.nn.MaxPool2d(kernel_size=2, stride=2),
torch.nn.Conv2d(128, 256, kernel_size=3, stride=1, padding=1),
torch.nn.ReLU(),
torch.nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1),
torch.nn.ReLU(),
torch.nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1),
torch.nn.ReLU(),
torch.nn.MaxPool2d(kernel_size=2, stride=2),
torch.nn.Conv2d(256, 512, kernel_size=3, stride=1, padding=1),
torch.nn.ReLU(),
torch.nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1),
torch.nn.ReLU(),
torch.nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1),
torch.nn.ReLU(),
torch.nn.MaxPool2d(kernel_size=2, stride=2)
)
self.Classes = torch.nn.Sequential(
torch.nn.Linear(4 * 4 * 512, 1024),
torch.nn.ReLU(),
torch.nn.Dropout(p=0.5),
torch.nn.Linear(1024, 1024),
torch.nn.ReLU(),
torch.nn.Dropout(p=0.5),
#最后输出两种类别{0:猫, 1:狗}
torch.nn.Linear(1024, 2)
)
def forward(self, input):
x = self.Conv(input)
x = x.view(-1, 4 * 4 * 512)
x = self.Classes(x)
return x
在搭建好模型后,可以通过print对搭建的模型进行打印输出来显示模型中的细节。
打印输入的代码如下:
model = Models()
print(model)
然后,定义好模型的损失函数和对参数进行优化的优化函数
代码如下:
# 定义好模型的损失函数和对参数进行优化的优化函数
model = Models()
loss_f = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-5)
epoch_n = 5
time_open = time.time()
for epoch in range(epoch_n):
print("Epoch {}/{}".format(epoch, epoch_n-1))
print("-"*10)
for phase in ["train", "valid"]:
if phase == "train":
print("Trainning...")
model.train(True)
else:
print("Validing...")
model.train(False)
running_loss = 0.0
running_corrects = 0
for batch, data in enumerate(dataloader[phase], 1):
X, y = data
X, y = Variable(X), Variable(y)
y_pred = model(X)
_, pred = torch.max(y_pred.data, 1)
optimizer.zero_grad()
loss = loss_f(y_pred, y)
if phase == "train":
loss.backward()
optimizer.step()
running_loss += loss.item()
running_corrects += torch.sum(pred == y.data)
if batch%500 ==0 and phase == "train":
print("Batch{}, Train Loss:{:.4f}, Train ACC:{:.4f}".format(batch, running_loss/batch,
100*running_corrects/(16*batch)))
epoch_loss = running_loss*16/len(image_datasets[phase])
epoch_acc = 100*running_corrects/len(image_datasets[phase])
print("{} Loss:{:.4f} Acc:{:.4f}%".format(phase, epoch_loss, epoch_acc))
time_end = time.time() - time_open
print(time_end)
在代码中优化函数使用的是Adam,损失函数使用的是交叉熵。
GPUs计算
上述运算全程使用了计算机的CPU进行计算,所以整个过程所耗费的时长是巨大的(一台正常的标配学生电脑大约8个小时左右)
下面我们对原始代码进行适当调整,将在模型训练的过程中需要计算的参数全部迁移至GPU上。在此之前,我们需要先确认GPUs硬件是否可用,具体的代码如下:
print(torch.cuda.is_available())
如果返回值是True,说明我们的GPUs已经具备了被使用的全部条件,若是False,则说明显卡暂时不支持,如果是驱动存在问题,则最简单的方法是将显卡驱动升级到最近版本。
GPUs配置可参考下面的blog:
win10 英伟达显卡MAX150 安装GPU版pytorch(一)选择合适的CUDA、CUDNN和torch
如果torch安装时conda命令安装报错的话可尝试pip命令
而且若你的torch不是GPUs版的话需要先卸载,再按上述链接中描述重装
在完成对模型训练过程中参数的迁移之后,新的训练代码如下:
# 定义好模型的损失函数和对参数进行优化的优化函数
model = Models().cuda()
loss_f = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-5)
epoch_n = 5
time_open = time.time()
for epoch in range(epoch_n):
print("Epoch {}/{}".format(epoch, epoch_n-1))
print("-"*10)
for phase in ["train", "valid"]:
if phase == "train":
print("Trainning...")
model.train(True)
else:
print("Validing...")
model.train(False)
running_loss = 0.0
running_corrects = 0
for batch, data in enumerate(dataloader[phase], 1):
X, y = data
X, y = Variable(X.cuda()), Variable(y.cuda())
y_pred = model(X)
_, pred = torch.max(y_pred.data, 1)
optimizer.zero_grad()
loss = loss_f(y_pred, y)
if phase == "train":
loss.backward()
optimizer.step()
running_loss += loss.item()
running_corrects += torch.sum(pred == y.data)
if batch%500 ==0 and phase == "train":
print("Batch{}, Train Loss:{:.4f}, Train ACC:{:.4f}".format(batch, running_loss/batch,
100*running_corrects/(16*batch)))
epoch_loss = running_loss*16/len(image_datasets[phase])
epoch_acc = 100*running_corrects/len(image_datasets[phase])
print("{} Loss:{:.4f} Acc:{:.4f}%".format(phase, epoch_loss, epoch_acc))
time_end = time.time() - time_open
print(time_end)
改动处仅为以下两行:
model = Models().cuda()
X, y = Variable(X.cuda()), Variable(y.cuda())
修改之后,用GPUs运行时,训练所耗时间大幅下降,且准确率也有提升,明显比使用CPU进行参数计算在效率上高出不少。
迁移VGG16
迁移VGG16的具体实施过程,首先需要下载已经具备最优参数的模板,这需要对我们之前使用的model = Models()代码部分进行替换,因为我们不需要再自己搭建和定义训练的模型了,而是通过代码自动下载模型并直接调用。
一、指定进行下载的模型是VGG16,并通过设置pretrained = True,来实现下载的模型附带了已经优化好的模型参数。
具体代码如下:
model = models.vgg16(pretrained=True)
在以上代码中,我们 这样,迁移学习的第一步就完成了,如果想要查看迁移模型的细节,就可以通过print将其打印输出。
输出的代码如下:
print(model)
二、对当前迁移过来的模型进行调整
尽管迁移学习要求我们需要解决的问题之间最好具有很强的相似性,但是每个问题对最后输出的结果会有不一样的要求
承担整个模型输出分类工作的是卷积神经网络模型中的全连接层,所以在迁移学习的过程中调整最多的也是全连接层部分。
其基本思路是:
冻结卷积神经网络中全连接层之前的全部网络层次,让这些被冻结的网络层次中的参数在模型的训练过程中不进行梯度更新,能够被优化的参数仅仅是没有被冻结的全连接层的全部参数
首先,迁移过来的VGG16架构模型在最后输出的结果是1000个,但在我们的问题中只需输出两个结果,所以全连接层必须进行调整。
具体代码如下:
for parma in model.parameters(): # 对原模型中的参数进行遍历操作,将参数中的parma.requires_grad全部设置为False,
parma.requires_grad = False # 这样对应的参数将不计算梯度,从而达到冻结梯度的效果
model.classifier = torch.nn.Sequential(torch.nn.Linear(25088, 4096),
torch.nn.ReLU(),
torch.nn.Dropout(p=0.5),
torch.nn.Linear(4096, 4096),
torch.nn.ReLU(),
torch.nn.Dropout(p=0.5),
torch.nn.Linear(4096, 2))
model = model.cuda() # gpu运行
cost = torch.nn.CrossEntropyLoss() # 使用交叉熵计算loss的值
optimizer = torch.optim.Adam(model.classifier.parameters(), lr=1e-5)
首先,对原模型中的参数进行遍历操作,将参数中的parma.requires_grad全部设置为False,这样对应的参数将不计算梯度,当然也不会进行梯度更新了,这便是冻结操作。
然后,定义新的全连接层结构并重新赋值给model.classifier
在完成了新的全连接层定义之后,全连接层中的parma.requires_grad参数会被默认重置为True,所以不需要再次遍历参数来进行解冻操作。
损失函数的loss值仍然使用交叉熵进行计算,但是在优化函数中负责优化的参数变成了全连接层中的所有参数,即只对model.classifier.parameters这部分参数进行优化。
我们可以通过打印输出对比其与模型没有调整之前有什么不同
使用同样的代码如下:
print(model)
原模型:
调整之后的模型:
可以看出,最大的不同就是模型的最后一部分全连接层发生了变化。
迁移VGG16结构的卷积神经网络模型进行迁移学习的完整代码如下:
from torchvision import models
import torch
from torch.autograd import Variable
from torchvision import datasets, transforms
import os
import matplotlib.pyplot as plt
import time
data_dirs = "DogsVSCats"
"""将数据集的原始图片的大小统一缩放至64×64,并转化为PyTorch可识别和计算的Tensor类型"""
data_transform = {x: transforms.Compose([transforms.Scale([64, 64]), transforms.ToTensor(), transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5])]) # 传入的零九秒里面,size
# 只能是一个整型数据,size是指缩放后图片最小边的边长。 举个例子,如果原图的height>width,那么改变大小后的图片大小是(size*height/width, size)。
for x in ["train", "valid"]}
image_datasets = {x: datasets.ImageFolder(root=os.path.join(data_dirs, x), transform=data_transform[x])
for x in ["train", "valid"]}
dataloader = {x: torch.utils.data.DataLoader(dataset=image_datasets[x],
batch_size=1,
shuffle=True)
for x in ["train", "valid"]
}
X_example, y_example = next(iter(dataloader["train"])) # 通过next和iter迭代操作获取一个批次的装载数据
index_classes = image_datasets["train"].class_to_idx
example_clasees = image_datasets["train"].classes
model = models.vgg16(pretrained=True)
print(model)
for parma in model.parameters(): # 对原模型中的参数进行遍历操作,将参数中的parma.requires_grad全部设置为False,
parma.requires_grad = False # 这样对应的参数将不计算梯度,从而达到冻结梯度的效果
model.classifier = torch.nn.Sequential(torch.nn.Linear(25088, 4096),
torch.nn.ReLU(),
torch.nn.Dropout(p=0.5),
torch.nn.Linear(4096, 4096),
torch.nn.ReLU(),
torch.nn.Dropout(p=0.5),
torch.nn.Linear(4096, 2))
print(model)
model = model.cuda() # gpu运行
cost = torch.nn.CrossEntropyLoss() # 使用交叉熵计算loss的值
optimizer = torch.optim.Adam(model.parameters())
loss_f = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.classifier.parameters(), lr=1e-5)
epoch_n = 5
time_open = time.time()
for epoch in range(epoch_n):
print("Epoch {}/{}".format(epoch, epoch_n-1))
print("-"*10)
for phase in ["train", "valid"]:
if phase == "train":
print("Trainning...")
model.train(True)
else:
print("Validing...")
model.train(False)
running_loss = 0.0
running_corrects = 0
for batch, data in enumerate(dataloader[phase], 1):
X, y = data
X, y = Variable(X.cuda()), Variable(y.cuda())
y_pred = model(X)
_, pred = torch.max(y_pred.data, 1)
optimizer.zero_grad()
loss = loss_f(y_pred, y)
if phase == "train":
loss.backward()
optimizer.step()
running_loss += loss.item()
running_corrects += torch.sum(pred == y.data)
if batch%500 ==0 and phase == "trainw":
print("Batch{}, Train Loss:{:.4f}, Train ACC:{:.4f}".format(batch, running_loss/batch,
100*running_corrects/(16*batch)))
epoch_loss = running_loss*16/len(image_datasets[phase])
epoch_acc = 100*running_corrects/len(image_datasets[phase])
print("{} Loss:{:.4f} Acc:{:.4f}%".format(phase, epoch_loss, epoch_acc))
time_end = time.time() - time_open
print(time_end)
因为VGG16模型地参数来比较大,可能会报如下错误:
这可能是因为学生电脑的gpu显存一般来说都不大,可以百度搜索白嫖GPU,获取云端的免费gpu资源来帮助运行程序。
推荐下面blog,有阿里云天池和百度谷歌等
白嫖GPU,我们是认真的!
还有一种方法就是将由GPUs计算重新换为CPU计算,不过这种方法程序运行所耗费的时间将会是巨大的
迁移ResNet50
大致同上迁移VGG16,进行模型迁移的代码改为:
model = models.resnet50(pretrained=True)
和迁移VGG16模型类似,在代码中使用resnet50对vgg16进行替换就完成了对应模型的迁移。
同之前迁移VGG16模型一样,我们需要对ResNet50的全连接层部分进行调整.
代码调整如下:
for parma in model.parameters(): # 对原模型中的参数进行遍历操作,将参数中的parma.requires_grad全部设置为False,
parma.requires_grad = False # 这样对应的参数将不计算梯度,从而达到冻结梯度的效果
model.fc=torch.nn.Linear(2048, 2)
同样,仅仅是最后一部分全连接层有差异,如下是对ResNet50进行迁移学习的完整代码实现:
from torchvision import models
import torch
from torch.autograd import Variable
from torchvision import datasets, transforms
import os
import matplotlib.pyplot as plt
import time
data_dirs = "DogsVSCats"
"""将数据集的原始图片的大小统一缩放至64×64,并转化为PyTorch可识别和计算的Tensor类型"""
data_transform = {x: transforms.Compose([transforms.Scale([64, 64]), transforms.ToTensor(), transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5])]) # 传入的零九秒里面,size
# 只能是一个整型数据,size是指缩放后图片最小边的边长。 举个例子,如果原图的height>width,那么改变大小后的图片大小是(size*height/width, size)。
for x in ["train", "valid"]}
image_datasets = {x: datasets.ImageFolder(root=os.path.join(data_dirs, x), transform=data_transform[x])
for x in ["train", "valid"]}
dataloader = {x: torch.utils.data.DataLoader(dataset=image_datasets[x],
batch_size=1,
shuffle=True)
for x in ["train", "valid"]
}
X_example, y_example = next(iter(dataloader["train"])) # 通过next和iter迭代操作获取一个批次的装载数据
index_classes = image_datasets["train"].class_to_idx
example_clasees = image_datasets["train"].classes
model = models.resnet50(pretrained=True)
print(model)
for parma in model.parameters(): # 对原模型中的参数进行遍历操作,将参数中的parma.requires_grad全部设置为False,
parma.requires_grad = False # 这样对应的参数将不计算梯度,从而达到冻结梯度的效果
model.fc=torch.nn.Linear(2048, 2)
print(model)
model = model.cuda() # gpu运行
cost = torch.nn.CrossEntropyLoss() # 使用交叉熵计算loss的值
optimizer = torch.optim.Adam(model.parameters())
loss_f = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.fc.parameters(), lr=1e-5)
epoch_n = 5
time_open = time.time()
for epoch in range(epoch_n):
print("Epoch {}/{}".format(epoch, epoch_n-1))
print("-"*10)
for phase in ["train", "valid"]:
if phase == "train":
print("Trainning...")
model.train(True)
else:
print("Validing...")
model.train(False)
running_loss = 0.0
running_corrects = 0
for batch, data in enumerate(dataloader[phase], 1):
X, y = data
X, y = Variable(X.cuda()), Variable(y.cuda())
y_pred = model(X)
_, pred = torch.max(y_pred.data, 1)
optimizer.zero_grad()
loss = loss_f(y_pred, y)
if phase == "train":
loss.backward()
optimizer.step()
running_loss += loss.item()
running_corrects += torch.sum(pred == y.data)
if batch%500 ==0 and phase == "trainw":
print("Batch{}, Train Loss:{:.4f}, Train ACC:{:.4f}".format(batch, running_loss/batch,
100*running_corrects/(16*batch)))
epoch_loss = running_loss*16/len(image_datasets[phase])
epoch_acc = 100*running_corrects/len(image_datasets[phase])
print("{} Loss:{:.4f} Acc:{:.4f}%".format(phase, epoch_loss, epoch_acc))
time_end = time.time() - time_open
print(time_end)
GPUs在深度学习的计算优化过程中效率明显高于CPU;
迁移学习非常强大,能快速解决同类问题,对于类似的问题不用再从头到尾对模型的全部参数进行优化,我门对于复杂模型的参数优化可能需要数周,采用迁移学习的思路能大大节约时间成本。
当然,如果模型的训练结果很不理想,则还可以训练更多的模型层次,优化更多的模型参数,而不是盲目地从头训练。