Press "Enter" to skip to content

图像分类学习:X光胸片诊断识别—-迁移学习

引言

 

刚进入人工智能实验室,不知道是在学习机器学习还是深度学习,想来他俩可能是一个东西,查阅之后才知道这是两个领域,或许也有些交叉,毕竟我也刚接触,不甚了解。

 

在我还是个纯度小白之时,写下这篇文章,希望后来同现在的我一样,刚刚涉足此领域的同学能够在这,跨越时空,在小白与小白的交流中得到些许帮助。

 

开始

 

在只会一些python语法,其他啥都没有,第一周老师讲了一些机器学习和深度学习的了解性内容,就给了一个实验,让我们一周内弄懂并跑出来,其实老师的代码已经完成了,我们可以直接放进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、val、test对应数据集里的三个文件夹train、val、test

 

键train对应的值是一个操作transfroms.Compose( [列表] )

 

参数为一个列表,列表中的元素为四个操作:

 

transforms.RandomResizedCrop()
transforms.RandomHorizontalFlip()
transforms.ToTensor
transforms.ToTensortransforms.Normalize()

 

transforms在torchvision中,一个图像处理包,可以通过它调用一些图像处理函数,对图像进行处理

 

transfroms.Compose( [列表] ):此函数存在于torchvision.transforms中,一般用Compose函数把多个步骤整合到一起

 

transforms.RandomResizedCrop(数字):将给定图像随机裁剪为不同的大小和宽高比,然后缩放所裁剪得到的图像为制定的大小;(即先随机采集,然后对裁剪得到的图像缩放为同一大小)

例如:

transforms.RandomHorizontalFlip():以给定的概率随机水平旋转给定的PIL的图像,默认为0.5;

 

例如:

transforms.ToTensor:将给定图像转为Tensor(一个数据类型,类似有深度的矩阵)

例如:

transforms.ToTensortransforms.Normalize():归一化处理

例如:

函数详细内容请查看 此处文章

 

可以看出,这一块的代码就是定义一种操作集合,将一张图片进行剪裁、旋转、转为一种数据、数据归一化

 

继续往下看,相应的解释都在注释中

 

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(data_dir, 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]])

 

输出后,在IDE中是这样的(右上角):

 

 

好,想在继续往下走

 

下面呢给出了四个训练模型,实战中我们只需要挑其中一个进行训练就好,其他的模型要注释掉,下面代码上四个模型我都会分析

 

# 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()  # 交叉熵损失函数

 

引用 此文章

 

class torch.optim.SGD(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()

 

class torch.optim.Adam(params, lr=0.001, betas=(0.9, 0.999), eps=1e-08, weight_decay=0)

 

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):用于重新评估模型并返回损失的一个闭包

Be First to Comment

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注