引言
刚进入人工智能实验室,不知道是在学习机器学习还是深度学习,想来他俩可能是一个东西,查阅之后才知道这是两个领域,或许也有些交叉,毕竟我也刚接触,不甚了解。
在我还是个纯度小白之时,写下这篇文章,希望后来同现在的我一样,刚刚涉足此领域的同学能够在这,跨越时空,在小白与小白的交流中得到些许帮助。
开始
在只会一些语法,其他啥都没有,第一周老师讲了一些机器学习和深度学习的了解性内容,就给了一个实验,让我们一周内弄懂并跑出来,其实老师的代码已经完成了,我们可以直接放进Pycharm里跑出来,但是代码细节并没有讲,俗话说师傅领进门,修行在个人。那就从最基本的开始,把这个代码弄懂,把实验理解。
下面我会先把整个代码贴出来,之后一步一步去分析每个模块,每个函数的作用。
说明
本实验来自此处博客,我们的实验也是基于这个博客的内容学习的。
一、数据集
数据源于kaggle,可在此链接自行下载
二、运行代码
三、解析
从头开始,先来看这段代码(注释进行解释):
# 进行一系列数据增强,然后生成训练(train)、验证(val)、和测试(test)数据集
data_transforms = {
'train': transforms.Compose([
transforms.RandomResizedCrop(input_size),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
]),
'val': transforms.Compose([
transforms.Resize(input_size),
transforms.CenterCrop(input_size),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
]),
'test': transforms.Compose([
transforms.Resize(input_size),
transforms.CenterCrop(input_size),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])])
}
这是一个字典数据类型,其中键分别为:对应数据集里的三个文件夹
键train对应的值是一个操作
参数为一个列表,列表中的元素为四个操作:
在torchvision中,一个图像处理包,可以通过它调用一些图像处理函数,对图像进行处理
:此函数存在于中,一般用函数把多个步骤整合到一起
:将给定图像随机裁剪为不同的大小和宽高比,然后缩放所裁剪得到的图像为制定的大小;(即先随机采集,然后对裁剪得到的图像缩放为同一大小)
例如:
:以给定的概率随机水平旋转给定的PIL的图像,默认为0.5;
例如:
:将给定图像转为Tensor(一个数据类型,类似有深度的矩阵)
例如:
:归一化处理
例如:
函数详细内容请查看此处文章
继续往下看,相应的解释都在注释中
image_datasets = {x: datasets.ImageFolder(os.path.join(data_dir, x), data_transforms[x]) for x in
['train', 'val', 'test']}
dataloaders_dict = {
x: torch.utils.data.DataLoader(image_datasets[x], batch_size=batch_size, shuffle=True, num_workers=0) for x in
['train', 'val', 'test']}
首先是数据导入部分,这里采用官方写好的torchvision.datasets.ImageFolder接口实现数据导入。这个接口需要你提供图像所在的文件夹
x是字典的键,从后面的for迭代的范围中获取,有'train', 'val', 'test'三个值
datasets.ImageFolder(os.path.join(, x), data_transforms[x])高亮部分是数据集的文件夹路径,找到文件后,确定x,在执行第二个参数data_transforms[x],把x进行一些列处理
前面torchvision.datasets.ImageFolder只是返回列表,列表是不能作为模型输入的(我也不知道为什么),因此在PyTorch中需要用另一个类来封装列表,那就是:torch.utils.data.DataLoader
torch.utils.data.DataLoader类可以将列表类型的输入数据封装成Tensor数据格式,以备模型使用。
好,我们继续往下看
# 定义一个查看图片和标签的函数
def imshow(inp, title=None):
# transpose(0,1,2),0是x轴,1是y轴,2是z轴,由(0,1,2)变为(1,2,0)就是x和z轴先交换,x和y轴再交换
inp = inp.numpy().transpose((1, 2, 0))
mean = np.array([0.485, 0.456, 0.406]) # 创建一个数组[0.485, 0.456, 0.406]
std = np.array([0.229, 0.224, 0.225]) # 同样也是创建一个数组
inp = std * inp + mean # 调整图像尺寸大小等
inp = np.clip(inp, 0, 1) # 小于0的都为0,大于1的都为1,之间的不变
plt.imshow(inp) # 设置图像为灰色
if title is not None: # 如果图像有标题则显示标题
plt.title(title) # 设置图像标题
plt.pause(0.001) # 窗口绘制后停留0.001秒
imgs, labels = next(iter(dataloaders_dict['train'])) # 自动往下迭代参数对象
out = torchvision.utils.make_grid(imgs[:8]) # 将8个图拼成一张图片
classes = image_datasets['test'].classes # 每个图像的文件名
# out是一个8个图片拼成的长图,经过imshow()处理后附加标题(图片文件名的前8个字母)输出
# imshow(out, title=[classes[x] for x in labels[:8]])
好,想在继续往下走
下面呢给出了四个训练模型,实战中我们只需要挑其中一个进行训练就好,其他的模型要注释掉,下面代码上四个模型我都会分析
# inception------------------------------------------------------inception模型,有趣的是它可以翻译为盗梦空间
model = models.inception_v3(pretrained=True)
# inception_v3是一个预训练模型, pretrained=True执行后会把模型下载到我们的电脑上
model.aux_logits = False # 是否给模型创建辅助,具体增么个辅助太复杂,请观众老爷们自行谷歌
num_fc_in = model.fc.in_features # 提取fc层固定的参数
# 改变全连接层,2分类问题,out_features = 2
model.fc = nn.Linear(num_fc_in, num_classes) # 修改fc层参数为num_classes = 4(最前面前面定义了)
# alexnet--------------------------------------------------------alexnet模型
model = models.alexnet(pretrained=True) # alexnet是一个预训练模型, pretrained=True执行后会把模型下载到我们的电脑上
num_fc_in = model.classifier[6].in_features # 提取fc层固定的参数
model.fc = torch.nn.Linear(num_fc_in, num_classes) # 修改fc层参数为num_classes = 4(最前面前面定义了)
model.classifier[6] = model.fc
#将图层初始化为model.fc
#相当于model.classifier[6] = torch.nn.Linear(num_fc_in, num_classes)
# 建立VGG16迁移学习模型------------------------------------------------vgg16模型
model = torchvision.models.vgg16(pretrained=True)# vgg16是一个预训练模型, pretrained=True执行后会把模型下载到我们的电脑上
# 先将模型参数改为不可更新
for param in model.parameters():
param.requires_grad = False
# 再更改最后一层的输出,至此网络只能更改该层参数
model.classifier[6] = nn.Linear(4096, num_classes)
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.Dropout(p=0.5),
torch.nn.Linear(4096, num_classes))
# resnet18---------------------------------------------------------------resnet模型(和前几个模型差不多,自己脑部吧)
model = models.resnet18(pretrained=True)
# 全连接层的输入通道in_channels个数
num_fc_in = model.fc.in_features
# 改变全连接层,2分类问题,out_features = 2
model.fc = nn.Linear(num_fc_in, num_classes)
继续,解释都在注释里了
# 定义训练函数
def train_model(model, dataloaders, criterion, optimizer, mundde_epochs=25):
since = time.time() # 返回当前时间的时间戳(1970纪元后经过的浮点秒数)
# state_dict变量存放训练过程中需要学习的权重和偏执系数,state_dict作为python的字典对象将每一层的参数映射成tensor张量,
# 需要注意的是torch.nn.Module模块中的state_dict只包含卷积层和全连接层的参数
best_model_wts = copy.deepcopy(model.state_dict()) # copy是一个复制函数
best_acc = 0.0
# 下面这个迭代就是一个进度条的输出,从0到9显示进度
for epoch in range(num_epochs):
print('Epoch {}/{}'.format(epoch, num_epochs - 1))
print('-' * 10)
# 下面这个迭代,范围就两个'train', 'val',对应不执行不同的训练模式
for phase in ['train', 'val']:
if phase == 'train':
model.train()
else:
model.eval()
running_loss = 0.0
running_corrects = 0.0
for inputs, labels in dataloaders[phase]:
# 下面这行代码的意思是将所有最开始读取数据时的tensor变量copy一份到device所指定的GPU或CPU上去,
# 之后的运算都在GPU或CPU上进行
inputs, labels = inputs.to(device), labels.to(device)
optimizer.zero_grad() # 模型梯度设为0
# 接下来所有的tensor运算产生的新的节点都是不可求导的
with torch.set_grad_enabled(phase == 'train'):
outputs = model(inputs) # output等于把inputs放到指定设备上去运算
loss = criterion(outputs, labels) # loss为outputs和labels的交叉熵损失
# 举例:output = torch.max(input, dim)
# 输入
# input是softmax函数输出的一个tensor
# dim是max函数索引的维度0 / 1,0是每列的最大值,1是每行的最大值
# 输出
# 函数会返回两个tensor,第一个tensor是每行的最大值,softmax的输出中最大的是1,所以第一个tensor是全1的tensor;
# 第二个tensor是每行最大值的索引。
_, preds = torch.max(outputs, 1)
if phase == 'train':
loss.backward() # 反向传播计算得到每个参数的梯度值
optimizer.step() # 通过梯度下降执行一步参数更新
running_loss += loss.item() * inputs.size(0)
running_corrects += (preds == labels).sum().item()
epoch_loss = running_loss / len(dataloaders[phase].dataset)
epoch_acc = running_corrects / len(dataloaders[phase].dataset)
print('{} loss: {:.4f} Acc: {:.4f}'.format(phase, epoch_loss, epoch_acc))
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))
model.load_state_dict(best_model_wts)
return model
继续往下看
# 定义优化器和损失函数
model = model.to(device) # 前面解释过了
optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9)
# optimizer = optim.Adam(model.classifier.parameters(), lr=0.0001)
# sched = optim.lr_scheduler.StepLR(optimizer, step_size=4, gamma=0.1)
criterion = nn.CrossEntropyLoss() # 交叉熵损失函数
引用此文章
(params, lr=, momentum=0, dampening=0, weight_decay=0, nesterov=False)[source]
实现随机梯度下降算法(momentum可选)。
参数:
params (iterable) – 待优化参数的iterable或者是定义了参数组的dict
lr (float) – 学习率
momentum (float, 可选) – 动量因子(默认:0)
weight_decay (float, 可选) – 权重衰减(L2惩罚)(默认:0)
dampening (float, 可选) – 动量的抑制因子(默认:0)
nesterov (bool, 可选) – 使用Nesterov动量(默认:False)
例子:
optimizer = torch.optim.SGD(model.parameters(), lr=0.1, momentum=0.9)
optimizer.zero_grad()
loss_fn(model(input), target).backward()
optimizer.step()
adam算法来源:Adam: A Method for Stochastic Optimization
Adam(Adaptive Moment Estimation)本质上是带有动量项的RMSprop,它利用梯度的一阶矩估计和二阶矩估计动态调整每个参数的学习率。它的优点主要在于经过偏置校正后,每一次迭代学习率都有个确定范围,使得参数比较平稳。
其公式如下:
参数:
params(iterable):可用于迭代优化的参数或者定义参数组的dicts。
lr (float, optional) :学习率(默认: 1e-3)
betas (Tuple[float, float], optional):用于计算梯度的平均和平方的系数(默认: (0.9, 0.999))
eps (float, optional):为了提高数值稳定性而添加到分母的一个项(默认: 1e-8)
weight_decay (float, optional):权重衰减(如L2惩罚)(默认: 0)
step(closure=None)函数:执行单一的优化步骤
closure (callable, optional):用于重新评估模型并返回损失的一个闭包