# 宝可梦分类器(实战cnn神经网络) **Repository Path**: penguink3/baokemeng-classifier ## Basic Information - **Project Name**: 宝可梦分类器(实战cnn神经网络) - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2021-11-24 - **Last Updated**: 2022-04-26 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 1. 宝可梦精灵数据集 https://pan.baidu.com/s/1V_ZJ7ufjUUFZwD2NHSNMFw 提取码:dsxl ## 1.1. 划分用途 - 分类问题:5种 - 皮卡丘 234 - 超梦 239 - 杰尼龟 223 - 小火龙 238 - 妙蛙种子 234 - 训练集、检验集、测试集 - train:---60%---- val:--20%-- test:-20%-- - 实战工作四个步骤 - laod data(包括数据预处理) - build model - train and test - transer learning ## 1.2. 宝可梦数据集类 - 读取数据集我们可以使用继承Dataset父类,实现Dataset需要两个接口 - __init__:构造函数,划分tarin和test然后读取数据 - __len__:返回数据集长度 => len(data)的时候调用 - __getitem__:通过索引返回数据 => data[i]调用 - 数据预处理 - Image size:我们神经网络一般只能接受一个规整的(正方形)图片,如224*224或64*64 - 数据加强:5*230的数据有点少,我们可以通过rotate,crop等增加数据集数量,防止网络过拟合 - normalize:把存图片的矩阵数据,用(mean,std)规范化到范围之内 - toTensor:不是tensor训练个屁s ### 1.2.1. 构造函数读取图片 - 这里我们要求文件目录结构为: > => root文件夹(数据集根目录)-> |各分目录(目录名是里面各个图片的label) - 为了方便获取数据集,请严格按照此方式放置数据,如下例子 ![](README_IMG/1.png) - 构造函数需要传递参数: - root:根目录地址 - resize:图片重塑大小,因为每个图片可能不一样 - mode:模式,这个数据集是训练的还是测试的? ```py def __init__(self, root, resize, mode): """ :param root: 数据集根目录 :param resize: 重塑图片大小 :param mode: train/val ? """ super(Pokemon, self).__init__() self.root = root self.resize = resize self.name2label = {} # 映射子目录名为label的表 # 遍历根目录下的子目录和文件(sorted人为排序,方便重用,让编码的映射保持固定) for name in sorted(os.listdir(os.path.join(root))): if not os.path.isdir(os.path.join(root, name)): # 如果不是目录是个文件 continue # labels[name] = code label的编码 # 这个len(self.name2label.keys())其实就是i+1作为它的值 self.name2label[name] = len(self.name2label.keys()) print(self.name2label) # csv[image] = label,把图片和它的label存到表里方便使用 self.images, self.labels = self.load_csv('images.csv') # 分割并且获取对应的train、val、test数据集 if mode == 'train': # 60% = 0%->60% self.images = self.images[:int(0.6 * len(self.images))] self.labels = self.labels[:int(0.6 * len(self.labels))] elif mode == 'val': # 20% = 60%->80% self.images = self.images[int(0.6 * len(self.images)):int(0.8 * len(self.images))] self.labels = self.labels[int(0.6 * len(self.labels)):int(0.8 * len(self.labels))] else: # 20% = 80%->100% self.images = self.images[int(0.8 * len(self.images)):] self.labels = self.labels[int(0.8 * len(self.labels)):] ``` - 读取图片并且将对应label存储到csv中 ```py def load_csv(self, filename): """ 将图片和它的label对应并存到csv表 :param filename: 保存csv表格文件名 :return: 返回(image, label)对 """ if not os.path.exists(os.path.join(self.root, filename)): # 数据集根目录如果不存在这个文件 images = [] for name in self.name2label.keys(): # 遍历每一个label的文件夹里的图片 # e.g. 'pokemon\\mewtwo\\00001.png images += glob.glob(os.path.join(self.root, name, '*.png')) images += glob.glob(os.path.join(self.root, name, '*.jpg')) images += glob.glob(os.path.join(self.root, name, '*.jpeg')) # glob.glob 获取指定木下的所有指定文件的文件名 # 三种类型的图片文件 print(len(images), images) # e.g. 1167, 'pokemon\\bulbasaur\\00000000.png' # 随机打乱 random.shuffle(images) # 写入csv文件 with open(os.path.join(self.root, filename), mode='w', newline='') as f: writer = csv.writer(f) for img in images: # e.g. 'pokemon\\bulbasaur\\00000000.png' name = img.split(os.sep)[-2] # 按照目录分隔符号分割字地址 e.g. bulbasaur label = self.name2label[name] # 使用name拿到对应的label编码 # e.g. b'pokemon\\bulbasaur\\00000000.png', 0 writer.writerow([img, label]) # 写一行,这里img路径和label编码默认用","分割 print('writen into csv file:', filename) # read from csv file(读取) images, labels = [], [] with open(os.path.join(self.root, filename)) as f: reader = csv.reader(f) for row in reader: # 读取每一行 # e.g. 'pokemon\\bulbasaur\\00000000.png', 0 img, label = row label = int(label) images.append(img) labels.append(label) assert len(images) == len(labels) # 保证两个长度一致 ``` ### 1.2.2. __getitem__函数 - 用于根据idx取得数据,这个函数使得我们能够用iter迭代器或者dataloader读取数据 ```py def __getitem__(self, idx): """ 根据idx得到(images,labels)对 :param idx: :return: (images,labels) """ # idx~[0~len(images)] # self.images, self.labels # e.g. img:'pokemon\\bulbasaur\\00000000.png',label:0 img, label = self.images[idx], self.labels[idx] tf = transforms.Compose([ lambda x: Image.open(x).convert('RGB'), # string path => 3 channel image data transforms.Resize((int(self.resize * 1.25), int(self.resize * 1.25))), # 重塑为resize*1.25大小 transforms.RandomRotation(15), # 随机旋转 transforms.CenterCrop(self.resize), # 中心裁剪 transforms.ToTensor(), # to tensor对象 transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) # 数值(mean.std)化 ]) img = tf(img) label = torch.tensor(label) return img, label ``` - 当然要使用dataloader还需要定义__len__ ```py def __len__(self): """ len :return: """ return len(self.images) ``` ## 1.3. 看一下数据集获取的都是啥? - 使用的是可视化[visdom工具](https://gitee.com/penguink3/study-notes/blob/master/PyTorch/visdom%E5%8F%AF%E8%A7%86%E5%8C%96%E5%B7%A5%E5%85%B7.md) - 由于有__getitem__函数,我们可以通过迭代器浏览pokemon数据集类对象中的pokemon数据 ```py def main(): viz = visdom.Visdom() db = Pokemon('pokemon', 64, 'train') x, y = next(iter(db)) # 使用迭代器获取数据 # print('sample:', x.shape, y.shape, y) # viz.image(db.denormalize(x), win='sample_x', opts=dict(title='sample_x')) # 使用DataLoader读取我们的pokemon data对象 loader = DataLoader(db, batch_size=32, shuffle=True, num_workers=8) for x, y in loader: # 读取一个batch的图片,每行nrow八个 viz.images(db.denormalize(x), nrow=8, win='batch', opts=dict(title='batch')) viz.text(str(y.numpy()), win='label', opts=dict(title='batch-y')) time.sleep(2) ``` # 2. 自定义网络 ## 2.1. resnet18 - 我们采用resnet18进行实战训练 - 该CNN的实现方法可以参考[PyTorch学习](https://gitee.com/penguink3/study-notes/blob/master/PyTorch/PyTorch%E5%AD%A6%E4%B9%A0.md)第8节 ## 2.2. 使用网络 ### 2.2.1. 流程 - 使用网络的一个规范化流程 ``` 拿到数据集train和test for 每一个回合: 分割train数据集为train和val 使用train训练网络: for 每一个batch: 训练网络 过一段时间查看loss和acc 查看loss和acc 使用val测试网络,并且查看test_acc 使用test测试训练好的网络 ``` ### 2.2.2. 训练网络代码 - train ```py def train(myNet, iterator, device, crition, optimization, losslist, acclist): """ Train :param myNet: :param cifar_train: :param device: :param crition: :param optimization: :param losslist: :param acclist: :return: """ step = 1 myNet.train() acc_avg = [] loss_avg = [] for batchidx, (x, label) in enumerate(iterator): # 获取每一个batch的数据 # label: [b], x: [b,3,32,32] x, label = x.to(device), label.to(device) # 数据转化到cuda上 logit = myNet(x) # 执行一次网络 loss = crition(logit, label) # 计算loss loss_avg.append(loss.item()) acc = binary_acc(torch.argmax(logit, dim=1), label).item() # 计算acc acc_avg.append(acc) # 梯度下降,优化神经网络参数 optimization.zero_grad() # 梯度信息清零 loss.backward() # 梯度传递 optimization.step() # 更新参数 # each step output average acc and average loss if batchidx % step == 0: loss = np.average(loss_avg) acc = np.average(acc_avg) # 抽取第一层短接层的三层kernels的其中一个权值 para = [myNet.conv1[0].weight[0][0][0][0].item(), myNet.conv1[0].weight[0][1][0][0].item(), myNet.conv1[0].weight[0][2][0][0].item()] losslist.append(loss) acclist.append(acc) # print(paralist) print("Iteration:", batchidx, "acc:", acc, "loss:", loss) # 输出一下准确率 ``` - eval ```py def eval(myNet, iterator, device): """ Eval :param myNet: :param iterator: :param device: :return: """ myNet.eval() with torch.no_grad(): # 在不需要计算梯度的情况下执行以下内容 total_acc = 0 total_correct = 0 total_num = 0 for x, label in iterator: # 每次取的测试数据也是batchsize个的,这里不需要获取batchidx所以写法有点不一样 # label: [b], x: [b,3,32,32] x, label = x.to(device), label.to(device) # 数据转化到cuda上 logit = myNet(x) # 执行一次网络 pred = logit.argmax(dim=1) # 10个预测值最大的那个 total_correct += torch.eq(pred, label).float().sum().item() # 正确数量记录 total_num += x.size(0) # 由于cifar_val的len是batch的数量,所以不能直接当作数据总量,我们需要手动计算 total_acc = (total_correct / total_num) print("===> testdata正确率:", total_acc) return total_acc ``` # 3. 测试网络 - 从test数据集中每次抽取一个batch进行显示并且统计正确率 ```py def test(myNet, iterator, device, data): """ 训练好网络后进行最终测试 :return: """ viz = visdom.Visdom() myNet.load_state_dict(torch.load('savefile/ResNet.mdl')) # 加载网络状态 myNet.eval() with torch.no_grad(): # 在不需要计算梯度的情况下执行以下内容 total_acc = 0 total_correct = 0 total_num = 0 for x_, label_ in iterator: # 每次取的测试数据也是batchsize个的,这里不需要获取batchidx所以写法有点不一样 # label: [b], x: [b,3,32,32] x, label = x_.to(device), label_.to(device) # 数据转化到cuda上 logit = myNet(x) # 执行一次网络 pred = logit.argmax(dim=1) # 10个预测值最大的那个 total_correct += torch.eq(pred, label).float().sum().item() # 正确数量记录 total_num += x.size(0) # 由于cifar_val的len是batch的数量,所以不能直接当作数据总量,我们需要手动计算 # 读取一个batch的图片,每行nrow八个 viz.images(data.denormalize(x_), nrow=8, win='batch', opts=dict(title='batch')) viz.text(str(label2name_list(label_.numpy())), win='label', opts=dict(title='batch-label')) viz.text(str(label2name_list(pred.cpu().numpy())), win='output', opts=dict(title='batch-output')) time.sleep(2) total_acc = (total_correct / total_num) print("(******* 最终测试数据正确率:", total_acc,"*********") return total_acc ``` # 4. 迁移学习提高精确度 ## 4.1. 为什么要迁移学习 - 当数据集比较少而且自己建立的CNN会发生过拟合情况,会有训练不充分的结果 - 我们可以利用原有训练好的模型进行迁移训练 ![](img/ ## 4.2. 如何迁移学习 - 我们可以利用pytorch框架自带的resnet18进行迁移学习 ```py # 我们使用公有的resnet18网络进行迁移学习 from torchvision.models import resnet18 ``` - 利用resnet18前17层训练好的先验数据,构造新的迁移学习网络 ```py # 获取迁移学习用的原网络 trained_model = resnet18(pretrained=True) # pretrained=True表示进行过训练的 # 通过原网络搬运我们需要的前17层 myNet = nn.Sequential( *list(trained_model.children())[0:-1], # [b,3,224,224] => [b,512,1,1] Flaten2d_chnl(), # [b,512,1,1] => [b,512] nn.Linear(512, 5) ).to(deviceGpu) ``` - 数据的训练和测试跟之前一样,但是结果不是一个级别的,下面是迁移学习结果,一开始就突破90%了 ![](img/24.png) # 5. 文件结构 - 文件目录 - pokemon文件夹:宝可梦精灵图片数据集存放位置,里面的每一个文件夹名代表这个文件夹中图片的标签 - README_IMG文件夹:(笑)README中的图片存放位置 - savefile文件夹:存放训练过程的loss,acc和网络参数等数据 - utils.py:存放工具类和工具接口,包括打平模块等,自己看下吧 - resnet.py:pytorch框架下自己设计的resnet18神经网络 - train_main.py:使用resnet.py中的resnet训练宝可梦数据集分类器 - test_main.py:使用resnet.py中的resnet测试宝可梦数据集分类器 - transer-learning-train.py:使用torchvision中的resnet18做迁移学习训练宝可梦数据集分类器 - transer-learning-test.py:使用torchvision中的resnet18做迁移学习测试宝可梦数据集分类器 ## 应该说这两个没啥用: - 后面新加的两个文件,测试一下VAE自编码器对pokemon数据集的效果,结果是没什么结果,跑出来很烂 - VAencoder.py:VAE的实现 - VAencoder-train.py:VAE训练 ![](README_IMG/2.png)