本文属于跟着b站学习pytorch笔记系列。网上很多大佬 关于renet模型图像识别的分类文章很多,但是图像部分通常就是一笔带过,给个链接。还是看了知乎的文章从头准备了数据集。
该数据集由102类产自英国的花卉组成。每类由40-258张图片组成
Visual Geometry Group - University of Oxford
就是红圈的1,4,5部分。 1 是8000多张图片压缩包,
4,5 可以用wget下载后拷到 工程文件夹
imagelabels.mat
总共有8189列,每列上的数字代表类别号。
setid.mat
-trnid字段:总共有1020列,每10列为一类花卉的图片,每列上的数字代表图片号。
-valid字段:总共有1020列,每10列为一类花卉的图片,每列上的数字代表图片号。
-tstid字段:总共有6149列,每一类花卉的列数不定,每列上的数字代表图片号。
import scipy.io # 用于加载mat文件
import numpy as np
import os
from PIL import Image
import shutil
labels = scipy.io.loadmat('./data/flower_data/imagelabels.mat')
labels = np.array(labels['labels'][0]) - 1
print("labels:", labels)
######## flower dataset: train test valid 数据id标识 ########
setid = scipy.io.loadmat('./data/flower_data/setid.mat')
validation = np.array(setid['valid'][0]) - 1
np.random.shuffle(validation)
train = np.array(setid['trnid'][0]) - 1
np.random.shuffle(train)
test = np.array(setid['tstid'][0]) - 1
np.random.shuffle(test)
######## flower data path 数据保存路径 ########
flower_dir = list()
######## flower data dirs 生成保存数据的绝对路径和名称 ########
for img in os.listdir("/Users/benmu/Downloads/jpg"):
######## flower data ########
flower_dir.append(os.path.join("/Users/benmu/Downloads/jpg", img))
######## flower data dirs sort 数据的绝对路径和名称排序 从小到大 ########
flower_dir.sort()
# print(flower_dir)
des_folder_train = "/Users/benmu/PycharmProjects/pythonProject128/data/flower_data/train"
for tid in train:
######## open image and get label ########
img = Image.open(flower_dir[tid])
# print(flower_dir[tid])
img = img.resize((256, 256), Image.ANTIALIAS)
lable = labels[tid]+1
# print(lable)
path = flower_dir[tid]
print("path:", path)
base_path = os.path.basename(path)
print("base_path:", base_path)
classes = str(lable)
class_path = os.path.join(des_folder_train, classes)
# 判断结果
if not os.path.exists(class_path):
os.makedirs(class_path)
print("class_path:", class_path)
despath = os.path.join(class_path, base_path)
print("despath:", despath)
img.save(despath)
des_folder_validation = "/Users/benmu/PycharmProjects/pythonProject128/data/flower_data/validation"
for tid in validation:
######## open image and get label ########
img = Image.open(flower_dir[tid])
# print(flower_dir[tid])
img = img.resize((256, 256), Image.ANTIALIAS)
lable = labels[tid]+1
# print(lable)
path = flower_dir[tid]
print("path:", path)
base_path = os.path.basename(path)
print("base_path:", base_path)
classes = str(lable)
class_path = os.path.join(des_folder_validation, classes)
# 判断结果
if not os.path.exists(class_path):
os.makedirs(class_path)
print("class_path:", class_path)
despath = os.path.join(class_path, base_path)
print("despath:", despath)
img.save(despath)
des_folder_test = "/Users/benmu/PycharmProjects/pythonProject128/data/flower_data/test"
for tid in test:
######## open image and get label ########
img = Image.open(flower_dir[tid])
# print(flower_dir[tid])
img = img.resize((256, 256), Image.ANTIALIAS)
lable = labels[tid]+1
# print(lable)
path = flower_dir[tid]
print("path:", path)
base_path = os.path.basename(path)
print("base_path:", base_path)
classes = str(lable)
class_path = os.path.join(des_folder_test, classes)
# 判断结果
if not os.path.exists(class_path):
os.makedirs(class_path)
print("class_path:", class_path)
despath = os.path.join(class_path, base_path)
print("despath:", despath)
img.save(despath)
分类号效果:
这里图像统一调整尺寸256x256. 常见的模型需要这个或者224x224的尺寸。也可以 不调整后面的图像增强部分会处理。
train,validation 都是1020,test 6149可以自己调整,我就是把test 改为train.
数据增强:
框架已经实现好了,不需要opencv单独处理 。
data_dir = './data/flower_data/'
train_dir = data_dir + '/train'
valid_dir = data_dir + '/valid'
# 进行数据增强操作
data_transforms = {
'train': transforms.Compose([
transforms.RandomRotation(45), # 随机旋转,-45到45度
transforms.CenterCrop(224), # 从中心开始裁剪,将其剪为224
transforms.RandomHorizontalFlip(p=0.5), # 有p的概率随机水平反转
transforms.RandomVerticalFlip(p=0.5), # 垂直反转
transforms.ColorJitter(brightness=0.2, contrast=0.1, saturation=0.1, hue=0.1), ##亮度,对比度,饱和度,色相
transforms.RandomGrayscale(p=0.025), # 有0.025%的概率变为灰度图像
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), # 均值,方差
]),
'valid': transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
])
}
图像增加就是对训练集上,旋转、裁剪、最后是归一化。valid 不需要旋转 。
batch_size = 8
# 入参:path、增强
image_datasets = {x: datasets.ImageFolder(os.path.join(data_dir, x), data_transforms[x]) for x in ['train', 'valid']}
dataloaders = {x: torch.utils.data.DataLoader(image_datasets[x], batch_size=batch_size, shuffle=True) for x in
['train', 'valid']}
dataset_sizes = {x: len(image_datasets[x]) for x in ['train', 'valid']}
class_names = image_datasets['train'].classes
print(image_datasets)
打印image_datasets包含的信息:
{'train': Dataset ImageFolder
Number of datapoints: 6149
Root location: ./data/flower_data/train
StandardTransform
Transform: Compose(
RandomRotation(degrees=[-45.0, 45.0], interpolation=nearest, expand=False, fill=0)
CenterCrop(size=(224, 224))
RandomHorizontalFlip(p=0.5)
RandomVerticalFlip(p=0.5)
ColorJitter(brightness=[0.8, 1.2], contrast=[0.9, 1.1], saturation=[0.9, 1.1], hue=[-0.1, 0.1])
RandomGrayscale(p=0.025)
ToTensor()
Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
), 'valid': Dataset ImageFolder
Number of datapoints: 1020
Root location: ./data/flower_data/valid
StandardTransform
Transform: Compose(
Resize(size=256, interpolation=bilinear, max_size=None, antialias=None)
CenterCrop(size=(224, 224))
ToTensor()
Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
)}
标签对应的实际名字 cat_to_name.json
展示数据
注意tensor的数据需要转换成numpy的格式,而且还需要还原回标准化的结果
def im_convert(tensor):
image = tensor.to("cpu").clone().detach()
image = image.numpy().squeeze()
##因为tensor是c*h*w,我们需要把他变成h*w*c
image = image.transpose(1, 2, 0)
image = image * np.array((0.229, 0.224, 0.225)) + np.array((0.485, 0.456, 0.406))
##clip函数,将小于0的数字变为0,将大于1的数字变为1
image = image.clip(0, 1)
return image
fig=plt.figure(figsize=(20, 12))
columns = 4
rows = 2
dataiter = iter(dataloaders['valid'])
inputs, classes = dataiter.next()
for idx in range (columns*rows):
ax = fig.add_subplot(rows, columns, idx+1, xticks=[], yticks=[])
ax.set_title(cat_to_name[str(int(class_names[classes[idx]]))])
plt.imshow(im_convert(inputs[idx]))
plt.show()
迁移 学习的目的,使用已有模型的权重跟偏置参数,作为我们的初始化的参数。尽可能吧自己的模型与已有的模型近似。
学习什么?通常两种策略:A 接着训练,B冻住模型层,只更改全连接层。
# 是否用GPU训练
train_on_gpu = torch.cuda.is_available()
if not train_on_gpu:
print('CUDA is not available. Training on CPU ...')
else:
print('CUDA is available! Training on GPU ...')
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
model_ft = models.resnet152()
因为我的mac没有cuda,用不了GPU,输入的图片224x224x3,所以模型:resnet152估计不行。
全连接层out_feature=1000,要修改为自己的输出102
这里只是列举了resnet. vgg ,其他的没贴上
model_name = 'resnet' # 可选的比较多 ['resnet', 'alexnet', 'vgg', 'squeezenet', 'densenet', 'inception']
# 是否用人家训练好的特征来做
feature_extract = True
def set_parameter_requires_grad(model, feature_extracting): # 使用resnet训练好的权重参数,不再训练
if feature_extracting:
for param in model.parameters():
param.requires_grad = False
def initialize_model(model_name, num_classes, feature_extract, use_pretrained=True):
# 选择合适的模型,不同模型的初始化方法稍微有点区别
model_ft = None
input_size = 0
if model_name == "resnet":
""" Resnet152
"""
model_ft = models.resnet50(pretrained=use_pretrained) # 下载resnet模型到本地
set_parameter_requires_grad(model_ft, feature_extract)
num_ftrs = model_ft.fc.in_features
model_ft.fc = nn.Sequential(nn.Linear(num_ftrs, 102), # 全连接层输出改为我们的图像类别102
nn.LogSoftmax(dim=1)) # 在softmax的结果上再做多一次log运算
input_size = 224
elif model_name == "vgg":
""" VGG11_bn
"""
model_ft = models.vgg16(pretrained=use_pretrained)
set_parameter_requires_grad(model_ft, feature_extract)
num_ftrs = model_ft.classifier[6].in_features
model_ft.classifier[6] = nn.Linear(num_ftrs, num_classes)
input_size = 224
else:
print("Invalid model name, exiting...")
exit()
return model_ft, input_size
model_ft, input_size = initialize_model(model_name, 102, feature_extract, use_pretrained=True)
#GPU计算
model_ft = model_ft.to(device)
# 模型保存
filename='checkpoint.pth'
# 是否训练所有层
params_to_update = model_ft.parameters()
print("Params to learn:")
if feature_extract:
params_to_update = []
for name,param in model_ft.named_parameters():
if param.requires_grad == True:
params_to_update.append(param)
print("\t",name)
else:
for name,param in model_ft.named_parameters():
if param.requires_grad == True:
print("\t",name)
输出参数:
Params to learn:
fc.0.weight
fc.0.bias
打印模型,可以看到最后全连接层的输出已经改为102
注意:models.resnet152(pretrained=use_pretrained) #下载resnet模型到本地
这个200多M可能慢一些,可以看一下本地的路径:.cache/torch/hub/checkpoints
# 优化器设置
optimizer_ft = optim.Adam(params_to_update, lr=1e-2)
scheduler = optim.lr_scheduler.StepLR(optimizer_ft, step_size=7, gamma=0.1)#optim.lr_scheduler学习率调整策略,学习率每7个epoch衰减成原来的1/10
#最后一层已经LogSoftmax()了,所以不能nn.CrossEntropyLoss()来计算了,nn.CrossEntropyLoss()相当于logSoftmax()和nn.NLLLoss()整合
criterion = nn.NLLLoss()
这里 老师讲了损失函数为什么不用交叉熵改用NLLLoss
def train_model(model, dataloaders, criterion, optimizer, num_epochs=25, is_inception=False,filename=filename): #is_inception是否使用其他的网络
since = time.time()
best_acc = 0
"""
checkpoint = torch.load(filename)
best_acc = checkpoint['best_acc']
model.load_state_dict(checkpoint['state_dict'])
optimizer.load_state_dict(checkpoint['optimizer'])
model.class_to_idx = checkpoint['mapping']
"""
model.to(device)
val_acc_history = []
train_acc_history = []
train_losses = []
valid_losses = []
LRs = [optimizer.param_groups[0]['lr']]
best_model_wts = copy.deepcopy(model.state_dict())
for epoch in range(num_epochs):
print('Epoch {}/{}'.format(epoch, num_epochs - 1))
print('-' * 10)
# 训练和验证
for phase in ['train', 'valid']:
if phase == 'train':
model.train() # 训练
else:
model.eval() # 验证
running_loss = 0.0
running_corrects = 0
# 把数据都取个遍
for inputs, labels in dataloaders[phase]:
inputs = inputs.to(device)
labels = labels.to(device)
# 清零
optimizer.zero_grad()
# 只有训练的时候计算和更新梯度
with torch.set_grad_enabled(phase == 'train'):
if is_inception and phase == 'train':
outputs, aux_outputs = model(inputs)
loss1 = criterion(outputs, labels)
loss2 = criterion(aux_outputs, labels)
loss = loss1 + 0.4*loss2
else:#resnet执行的是这里
outputs = model(inputs)
loss = criterion(outputs, labels)
_, preds = torch.max(outputs, 1)
# 训练阶段更新权重
if phase == 'train':
loss.backward()
optimizer.step()
# 计算损失
running_loss += loss.item() * inputs.size(0)
running_corrects += torch.sum(preds == labels.data)
epoch_loss = running_loss / len(dataloaders[phase].dataset)
epoch_acc = running_corrects.double() / len(dataloaders[phase].dataset)
time_elapsed = time.time() - since
print('Time elapsed {:.0f}m {:.0f}s'.format(time_elapsed // 60, time_elapsed % 60))
print('{} Loss: {:.4f} Acc: {:.4f}'.format(phase, epoch_loss, epoch_acc))
# 得到最好那次的模型
if phase == 'valid' and epoch_acc > best_acc:
best_acc = epoch_acc
best_model_wts = copy.deepcopy(model.state_dict())
state = {
'state_dict': model.state_dict(),
'best_acc': best_acc,
'optimizer' : optimizer.state_dict(),
}
torch.save(state, filename)
if phase == 'valid':
val_acc_history.append(epoch_acc)
valid_losses.append(epoch_loss)
scheduler.step(epoch_loss)
if phase == 'train':
train_acc_history.append(epoch_acc)
train_losses.append(epoch_loss)
print('Optimizer learning rate : {:.7f}'.format(optimizer.param_groups[0]['lr']))
LRs.append(optimizer.param_groups[0]['lr'])
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, val_acc_history, train_acc_history, valid_losses, train_losses, LRs
开始训练 :
model_ft, val_acc_history, train_acc_history, valid_losses, train_losses, LRs = train_model(model_ft, dataloaders, criterion, optimizer_ft, num_epochs=20, is_inception=(model_name=="inception"))
然后我可怜的电脑 就开始嗡嗡的响。19分钟才跑一次。起码要20次吧,老师建议是50次。那么这种级别的数据,不用GPU基本上低配的电脑就没戏了。
Epoch 0/19
----------
Time elapsed 16m 29s
train Loss: 9.5165 Acc: 0.3410
Time elapsed 18m 49s
valid Loss: 10.0126 Acc: 0.5216
Optimizer learning rate : 0.0010000
明天找个别的windows本子,重新折腾下环境试试。
参考:
基于tensorflow_slim模型调参的flower102鲜花分类过程 - 知乎