赞
踩
- import os
- import sys
- import json
- import torch
- import torch.nn as nn
- from torchvision import transforms, datasets
- import torch.optim as optim
- from tqdm import tqdm
- #from classic_models.alexnet import AlexNet
- from classic_models.googlenet_v1 import GoogLeNet
-
- def main():
- # 判断可用设备
- device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
- print("using {} device.".format(device))
-
- # 注意改成自己的数据集路径
- data_path = "G:\\flower"
- assert os.path.exists(data_path), "{} path does not exist.".format(data_path)
-
- # 数据预处理与增强
- """
- ToTensor()能够把灰度范围从0-255变换到0-1之间的张量.
- transform.Normalize()则把0-1变换到(-1,1). 具体地说, 对每个通道而言, Normalize执行以下操作: image=(image-mean)/std
- 其中mean和std分别通过(0.5,0.5,0.5)和(0.5,0.5,0.5)进行指定。原来的0-1最小值0则变成(0-0.5)/0.5=-1; 而最大值1则变成(1-0.5)/0.5=1.
- 也就是一个均值为0, 方差为1的正态分布. 这样的数据输入格式可以使神经网络更快收敛。
- """
- data_transform = {
- "train": transforms.Compose([transforms.Resize(224), # 将图片的短边缩放到224,图片的长边和短边的比值不变,即不能保证每张图片都是224*224大小,那么下一步的裁剪就有必要了
- transforms.CenterCrop(224), # 由中心向两边进行裁剪,裁剪的尺寸为224*224
- transforms.ToTensor(), # 可以将PIL和numpy格式的数据从[0,255]范围转换到[0,1] 。另外原始数据的shape是(H x W x C),这步后shape会变为(C x H x W)
- transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))]), # Normalize(mean, std, inplace=False),三通道中Normalize里面一般是Normalize((0.5,0.5,0.5),(0.5,0.5,0.5)),将上一步的数据范围由[0,1]转换为[-1,1]
-
- "val": transforms.Compose([transforms.Resize((224, 224)), # val不需要任何数据增强
- transforms.ToTensor(),
- transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])}
-
-
- # 使用ImageFlolder加载数据集中的图像,并使用指定的预处理操作来处理图像, ImageFlolder会同时返回图像和对应的标签。 (image path, class_index) tuples
- train_dataset = datasets.ImageFolder(root=os.path.join(data_path, "train"), transform=data_transform["train"]) # root:图片存储的根目录,即各类别文件夹所在目录的上一级目录。
- validate_dataset = datasets.ImageFolder(root=os.path.join(data_path, "val"), transform=data_transform["val"]) # transform:对图片进行预处理的操作(函数)。在data_transform中已经定义好
- train_num = len(train_dataset) # 计算train_dataset里面的图片个数
- val_num = len(validate_dataset) # 计算validate_dataset里面的图片个数
-
- # 使用class_to_idx给类别一个index,作为训练时的标签: {'daisy':0, 'dandelion':1, 'roses':2, 'sunflower':3, 'tulips':4}
- flower_list = train_dataset.class_to_idx # class_to_idx是train_dataset里面的一个函数,返回一个字典,即flower_list是一个字典
-
- # 创建一个字典,存储index和类别的对应关系,在模型推理阶段会用到。
- cla_dict = dict((val, key) for key, val in flower_list.items()) # items()方法将字典里对应的一对键和值以元组的形式(键, 值),存储为所生成序列里的单个元素
-
- # 将字典写成一个json文件
- json_str = json.dumps(cla_dict, indent=4) # json.dumps()是把python对象转换成json对象的一个过程,生成的是字符串。
- with open(os.path.join(data_path, 'class_indices.json') , 'w') as json_file:
- json_file.write(json_str)
-
- batch_size = 32 # batch_size大小,是超参,可调,如果模型跑不起来,尝试调小batch_size
-
- # 使用 DataLoader 将 ImageFloder 加载的数据集处理成批量(batch)加载模式
- train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=batch_size, shuffle=True )
- validate_loader = torch.utils.data.DataLoader(validate_dataset, batch_size=4, shuffle=False ) # 注意,验证集不需要shuffle
- print("using {} images for training, {} images for validation.".format(train_num, val_num))
-
- # 实例化模型,并送进设备
- net = GoogLeNet(num_classes = 5) # 使用GoogleNet来定义网络模型,分类数为5
- #net = AlexNet(num_classes=5 )
- net.to(device)
-
- # 指定损失函数用于计算损失;指定优化器用于更新模型参数;指定训练迭代的轮数,训练权重的存储地址
- loss_function = nn.CrossEntropyLoss() # 交叉熵函数
- optimizer = optim.Adam(net.parameters(), lr=0.0002)
- epochs = 1
- save_path = os.path.abspath(os.path.join(os.getcwd(), './results/weights/alexnet')) # os.getcwd()返回当前的文件目录,也就是后面的文件目录
- if not os.path.exists(save_path):
- os.makedirs(save_path) # 创建名为save_path的目录
-
- best_acc = 0.0 # 初始化验证集上最好的准确率,以便后面用该指标筛选模型最优参数。
- for epoch in range(epochs):
- ############################################################## train ######################################################
- net.train()
- acc_num = torch.zeros(1).to(device) # 初始化,用于计算训练过程中预测正确的数量
- sample_num = 0 # 初始化,用于记录当前迭代中,已经计算了多少个样本
- # tqdm是一个进度条显示器,可以在终端打印出现在的训练进度
- # train_loader:是需要迭代的对象,通常为列表或者生成器,其中包含训练数据。进度条会遍历该对象,并相应地更新进度。
- # file=sys.stdout:这个参数指定了进度条应该写入其输出的位置。在这种情况下,它被设置为sys.stdout,表示标准输出流(通常是控制台)。因此,进度条将显示在控制台中。
- # ncols=100:这个参数设置进度条的宽度,以字符为单位。在这里,它被设置为100个字符
- train_bar = tqdm(train_loader, file=sys.stdout, ncols=100)
- for data in train_bar :
- images, labels = data
- sample_num += images.shape[0] #[32, 3, 224, 224]
- optimizer.zero_grad() # 梯度初始化为零,把loss关于weight的导数变成0,避免梯度的叠加效应
- outputs = net(images.to(device)) # output_shape: [batch_size, num_classes]
- pred_class = torch.max(outputs, dim=1)[1] # torch.max 返回值是一个tuple,第一个元素是max值,第二个元素是max值的索引。
- acc_num += torch.eq(pred_class, labels.to(device)).sum() # 是一个比较操作,它会将预测的类别(pred_class)和标签(labels)进行逐元素的比较,返回一个布尔类型的张量,表示对应位置上两个值是否相等。
- # sum() 是对布尔类型的张量进行求和操作,将所有为 True 的元素相加,得到一个标量值,表示预测正确的样本数量。
- loss = loss_function(outputs, labels.to(device)) # 求损失
- loss.backward() # 自动求导
- optimizer.step() # 梯度下降
-
- # print statistics
- train_acc = acc_num.item() / sample_num
- # .desc是进度条tqdm中的成员变量,作用是描述信息
- train_bar.desc = "train epoch[{}/{}] loss:{:.3f}".format(epoch + 1, epochs, loss)
-
- # validate
- net.eval() #不启用 BatchNormalization 和 Dropout。此时pytorch会自动把BN和DropOut固定住,不会取平均,而是用训练好的值。不然的话,一旦test的batch_size过小,很容易就会因BN层导致模型performance损失较大;
- acc_num = 0.0 # accumulate accurate number per epoch
- with torch.no_grad():
- for val_data in validate_loader:
- val_images, val_labels = val_data
- outputs = net(val_images.to(device))
- predict_y = torch.max(outputs, dim=1)[1]
- acc_num += torch.eq(predict_y, val_labels.to(device)).sum().item()
-
- val_accurate = acc_num / val_num
- print('[epoch %d] train_loss: %.3f train_acc: %.3f val_accuracy: %.3f' % (epoch + 1, loss, train_acc, val_accurate))
- # 判断当前验证集的准确率是否是最大的,如果是,则更新之前保存的权重
- if val_accurate > best_acc:
- best_acc = val_accurate
- torch.save(net.state_dict(), os.path.join(save_path, "AlexNet.pth") ) # state_dict()返回一个包含了模型所有参数(权重和偏置)的字典。这个字典中的键是参数的名称,而对应的值则是该参数的张量。
-
- # 每次迭代后清空这些指标,重新计算
- train_acc = 0.0
- val_accurate = 0.0
-
- print('Finished Training')
-
-
- # if __name__ == '__main__':
- # main()
- main()
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。