• Pytorch目标分类深度学习自定义数据集训练


    目录

    一,Pytorch简介;

    二,环境配置;

    ,自定义数据集;

    ,模型训练;

    ,模型验证;


    一,Pytorch简介;

            PyTorch是一个开源的Python机器学习库,基于Torch,用于自然语言处理等应用程序。PyTorch 基于 Python: PyTorch 以 Python 为中心或“pythonic”,旨在深度集成 Python 代码,而不是作为其他语言编写的库的接口。Python 是数据科学家使用的最流行的语言之一,也是用于构建机器学习模型和 ML 研究的最流行的语言之一。由于其语法类似于 Python 等传统编程语言,PyTorch 比其他深度学习框架更容易学习。

    二,环境配置

           版本:

            系统:window10;

            Python:3.11.5;

            pytorch:2.0.1;

           Python安装:

            Python官网:python.org;

            下载3.11.5版本Python安装版进行安装;

            配置Python环境变量;

            在系统变量path中添加Python的bin路径和Script路径;

            查看Python是否安装成功;

            

            正常如上显示表示安装成功。

            同时查看Python对应的Pip版本;

            Pytorch安装:

            pytorch官网:PyTorch

            

            进入Pytorch官网后点击左上角Get Started查看Pytorch对于的Python版本,GPU版本。默认安装的是CPU版本,本文使用Pip安装Pytorch方式,直接运行Run this Command会报错,安装了几次都不行,所以自己找对应的安装文件进行安装更方便。

            根据Pytorch官网介绍的对应版本找到我们需要的依赖文件。

            网址:download.pytorch.org/whl/torch_stable.html

            

            找到对应安装的版本,cu开头表示是GPU版本和版本号,torch后面对应的是Pytorch版本号,cp对应Python版本;点击下载安装文件;

            下载好以后打开文件所在位置,进入window命令界面,执行命令;

    pip install torch-2.0.1+cu117-cp311-cp311-win_amd64.whl

            英伟达GPU安装:

            选择对应的GPU版本安装,安装完成后验证下是否安装成功,正常显示版本表示安装成功。

    三,自定义数据集;

            从网上下载数据集,按照文件夹分类,首先将数据集制作成包含图片路径,和对应索引的csv文件。

    1. import torch
    2. import os, glob
    3. import random, csv
    4. # 所有自定义数据集的一个母类
    5. from torch.utils.data import Dataset, DataLoader
    6. # 常用的图片变换器
    7. from torchvision import transforms
    8. # 从图片读取出数据
    9. from PIL import Image
    10. # 自定义数据集的类,继承自Dataset
    11. class Pokemon(Dataset):
    12. # 一、初始化函数init
    13. # 第一个参数root:总的图片所在的位置,可以是任意的位置,我们的图片可以放在任意的位置,我们这里就存储在当前目录文件夹下。
    14. # 第二个参数resize:图片输出的size,是由这个参数所进行设定。
    15. # 第三个参数mode:这里我们需要做train、validation以及test,对应这三种数据结构,因此我们用一个list[0,1,2]来代表是哪个模式。
    16. def __init__(self, root, resize, mode):
    17. # 先调用母类的初始化函数:
    18. super(Pokemon, self).__init__()
    19. # 1、首先我们将这个参数保存下来
    20. self.root = root
    21. self.resize = resize
    22. # 2、给每一个分类做一个映射,即当前的皮卡丘、妙蛙种子等这个string类型所对应的label是多少,这个是需要我们人为进行编码的。
    23. self.name2label = {} # 用字典来表示映射关系
    24. # 通过循环方式,将root路径下的文件夹名进行编码
    25. for name in sorted(os.listdir(os.path.join(root))):
    26. # 过滤掉非文件夹:如果不是dir,就过滤掉,此外我们还通过sorted排序的方法,将键值对关系固定下来
    27. if not os.path.isdir(os.path.join(root, name)):
    28. continue
    29. # 文件名做key,当前name2label的长度做value
    30. self.name2label[name] = len(self.name2label.keys())
    31. print(self.name2label)
    32. # image, label
    33. self.load_csv('images.csv')
    34. # 二、创建一个csv,用于保存图片全路径和对应的标签label
    35. # 这个函数接受一个参数filename
    36. # 这个函数中需要将所有图片都load进来
    37. def load_csv(self, filename):
    38. images = []
    39. for name in self.name2label.keys():
    40. # 类别信息我们可以使用路径来判断
    41. # 上面路径的mewtwo就是类别
    42. images += glob.glob(os.path.join(self.root, name, '*.png'))
    43. images += glob.glob(os.path.join(self.root, name, '*.jpg'))
    44. images += glob.glob(os.path.join(self.root, name, '*.jpeg'))
    45. print(len(images), images)
    46. # 将images顺序打乱
    47. random.shuffle(images)
    48. # 打开这个文件
    49. with open(os.path.join(self.root, filename), mode='w', newline='') as f:
    50. # 新建writer,获得csv这个文件对象
    51. writer = csv.writer(f)
    52. for img in images: # 获得每行信息
    53. # 通过分割符,将每行信息的内容分割开,取导数第二个,类型
    54. name = img.split(os.sep)[-2]
    55. # 通过获取的类型名来获取label
    56. label = self.name2label[name]
    57. # 将这个label信息写到csv中
    58. # csv是以逗号作为分割的
    59. writer.writerow([img, label])
    60. print('writen into csv file:', filename)
    61. # 三、完成两个自定义的逻辑
    62. # 1、样本的总体数量(图片总体数量),返回的是一个数字,总体图片大概有1168张,60%用于training,因此返回6-7百张图片
    63. def __len__(self):
    64. pass
    65. # 2、用于返回当前index上面元素的值,这里是返回两个数据:
    66. # 需要返回当前image的data,以及image所对应的label[0,1,2,3,4]
    67. def __getitem__(self, idx):
    68. pass
    69. # 创建一个调试函数:
    70. def main():
    71. db = Pokemon('F:\\train', 224, 'train')
    72. if __name__ == '__main__':
    73. main()

            将图片路径改成自己数据的文件夹路径,运行代码在对应路径下生成.csv格式文件

            类别索引根据文件夹种类顺序生成,要和csv文件中索引对应。数据集制作完成后就可以开始训练了。

            首先定义加载数据集类;

    1. import torch
    2. import os, glob
    3. import random, csv
    4. # 所有自定义数据集的一个母类
    5. from torch.utils.data import Dataset, DataLoader
    6. # 常用的图片变换器
    7. from torchvision import transforms
    8. # 从图片读取出数据
    9. from PIL import Image
    10. # 自定义数据集的类,继承自Dataset
    11. class Pokemon(Dataset):
    12. # 一、初始化函数init
    13. # 第一个参数root:总的图片所在的位置,可以是任意的位置,我们的图片可以放在任意的位置,我们这里就存储在当前目录文件夹下。
    14. # 第二个参数resize:图片输出的size,是由这个参数所进行设定。
    15. # 第三个参数mode:这里我们需要做train、validation以及test,对应这三种数据结构,因此我们用一个list[0,1,2]来代表是哪个模式。
    16. def __init__(self, root, resize, mode):
    17. # 先调用母类的初始化函数:
    18. super(Pokemon, self).__init__()
    19. # 1、首先我们将这个参数保存下来
    20. self.root = root
    21. self.resize = resize
    22. # 2、给每一个分类做一个映射,这个string类型所对应的label是多少,这个是需要我们人为进行编码的。
    23. self.name2label = {} # 用字典来表示映射关系
    24. # 通过循环方式,将root路径下的文件夹名进行编码
    25. for name in sorted(os.listdir(os.path.join(root))):
    26. # 过滤掉非文件夹:如果不是dir,就过滤掉,此外我们还通过sorted排序的方法,将键值对关系固定下来
    27. if not os.path.isdir(os.path.join(root, name)):
    28. continue
    29. # 文件名做key,当前name2label的长度做value
    30. self.name2label[name] = len(self.name2label.keys())
    31. # print(self.name2label)
    32. # 将self.load_csv的返回值images, labels赋予self.images, self.labels
    33. self.images, self.labels = self.load_csv('images.csv')
    34. # 四、不同比例模式下对图片数量进行划分
    35. if mode == 'train': # 取60%做training
    36. # len(self.images)的长度是1167,取60%做为train模式的图片
    37. self.images = self.images[:int(0.6 * len(self.images))]
    38. self.labels = self.labels[:int(0.6 * len(self.labels))]
    39. elif mode == 'val': # 取20%做validation, 60%-80%
    40. self.images = self.images[int(0.6 * len(self.images)):int(0.8 * len(self.images))]
    41. self.labels = self.labels[int(0.6 * len(self.labels)):int(0.8 * len(self.labels))]
    42. else: # mode为test,取80%到最末尾
    43. self.images = self.images[int(0.8 * len(self.images)):]
    44. self.labels = self.labels[int(0.8 * len(self.labels)):]
    45. # 二、创建一个csv,用于保存图片全路径和对应的标签label
    46. # 这个函数接受一个参数filename
    47. # 这个函数中需要将所有图片都load进来
    48. def load_csv(self, filename):
    49. # 需要一个判断,如果文件不存在,就需要创建csv,直接读取创建好的csv文件内容即可:
    50. # 如果不存在,就需要创建csv
    51. if not os.path.exists(os.path.join(self.root, filename)):
    52. images = []
    53. for name in self.name2label.keys():
    54. # 类别信息我们可以使用路径来判断
    55. # 上面路径的mewtwo就是类别
    56. images += glob.glob(os.path.join(self.root, name, '*.png'))
    57. images += glob.glob(os.path.join(self.root, name, '*.jpg'))
    58. images += glob.glob(os.path.join(self.root, name, '*.jpeg'))
    59. print(len(images), images)
    60. # 将images顺序打乱
    61. random.shuffle(images)
    62. # 打开这个文件
    63. with open(os.path.join(self.root, filename), mode='w', newline='') as f:
    64. # 新建writer,写入csv这个文件对象
    65. writer = csv.writer(f)
    66. for img in images:
    67. # 通过分割符,将每行信息的内容分割开,取导数第二个,类型
    68. name = img.split(os.sep)[-2]
    69. # 通过获取的类型名来获取label
    70. label = self.name2label[name]
    71. # 将这个label信息写到csv中
    72. # csv是以逗号作为分割的
    73. writer.writerow([img, label])
    74. print('writen into csv file:', filename)
    75. # 三、读取csv文件过程:
    76. # 这里需要在开头有一个判断,如果csv存在,就不用写入csv了,直接进行读取
    77. # 下次运行的时候只需加载进来即可
    78. images, labels = [], []
    79. with open(os.path.join(self.root, filename)) as f:
    80. # 新建reader,读取csv这个文件对象
    81. reader = csv.reader(f)
    82. for row in reader:
    83. img, label = row
    84. label = int(label) # 将这个label转码为int类型
    85. # 将img每个图片路径,以及label保存在建立好的列表对象中。
    86. images.append(img)
    87. labels.append(label)
    88. assert len(images) == len(labels)
    89. return images, labels
    90. # 完成两个自定义的逻辑:
    91. # 1、样本的总体数量(图片总体数量),返回的是一个数字,总体图片大概有1168张,60%用于training,因此返回6-7百张图片
    92. # 五、完成总体样本数量函数的内容
    93. def __len__(self):
    94. # 这里的样本长度是跟模型类别来决定的,上面已经根据不同模型类型划分了样本数量了。
    95. # 不同模式下,样本长度是不同的。
    96. # 因此这里的总体样本长度,就是不同模式下的样本数量。
    97. return len(self.images)
    98. # 九、解决normalize处理后,visdom无法正常显示的问题
    99. # 这里传入的参数x是normalize过后的
    100. def denormalize(self, x_hat):
    101. mean = [0.485, 0.456, 0.406]
    102. std = [0.229, 0.224, 0.225]
    103. mean = torch.tensor(mean).unsqueeze(1).unsqueeze(1)
    104. std = torch.tensor(std).unsqueeze(1).unsqueeze(1)
    105. print('mean.shape,std.shape:', mean.shape, std.shape)
    106. x = x_hat * std + mean
    107. return x
    108. # 2、用于返回当前index上面元素的值,这里是返回两个数据:
    109. # 需要返回当前image的data,以及image所对应的label[0,1,2,3,4]
    110. # 六、完成index与样本的一一对应
    111. def __getitem__(self, idx):
    112. # idx数值范围是[0-len(images)]
    113. # self.images保存了所有的数据;self.labels保存了所有数据对应的label信息;
    114. # img是一个string类型(还不是具体的图片,只是路径)
    115. # label是一个整数类型
    116. img, label = self.images[idx], self.labels[idx]
    117. # 这里就需要将img所对应的路径读取出图片,并转为tensor类型
    118. # 这里我们可以Compose组合操作步骤
    119. # 八、增加数据预处理的工作,在Compose中增加这些内容,data augmentation数据增强
    120. # 这里我们做放大、旋转、裁切这三个数据增强的操作
    121. tf = transforms.Compose([
    122. # 这里需要将路径变成具体的图片数据类型
    123. # 即:string path => image data
    124. lambda x: Image.open(x).convert('RGB'),
    125. # Resize工作,这里的size是我们实例化时的self.resize的值
    126. # 1、data augmentation放大:在Resize设置的基础上,稍微调大一些size, 调整为1.25倍
    127. transforms.Resize((int(self.resize * 1.25), int(self.resize * 1.25))),
    128. # 2、data augmentation旋转:增加随机旋转,注意:这里旋转角度不能太大,会增加学习的难度。
    129. transforms.RandomRotation(15),
    130. # 3、data augmentation中心裁切:裁切为我们所需要的大小
    131. transforms.CenterCrop(self.resize),
    132. # 将数据变为tensor类型
    133. transforms.ToTensor(),
    134. # 4、normalize处理,希望图片数值范围在0左右分布,而不希望数值只分布在0的右侧或只在左侧
    135. # 其中参数统计的所有image net数据集几百万张图片的mean=[R的mean,G的mean,B的mean]和std=[R的方差,G的方差,B的方差]
    136. # 基本上这个数值是通用的
    137. # 数据通过Normalize处理后,就是在-1到1之间分布了。
    138. transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
    139. ])
    140. img = tf(img)
    141. label = torch.tensor(label)
    142. return img, label
    143. # 创建一个调试函数:
    144. def main():
    145. # 七、验证自定义数据集
    146. # 验证需要一些辅助函数,用visdom做一些可视化。
    147. import visdom
    148. import time
    149. import torchvision # 通过API较为简便的加载自定义数据集,需要引入torchvision
    150. # 创建一个visdom这个对象
    151. viz = visdom.Visdom()
    152. # 十一、通过API较为简便的加载自定义数据集(前提是数据集按照不同类型存储在对应类型命名的文件夹下面,并且这些不同类别的文件夹都存储在统一的一个文件夹下,只有这种固定的二级目录存储形式才能用这个API进行加载。)
    153. tf = transforms.Compose([
    154. transforms.Resize((224, 224)),
    155. transforms.ToTensor()
    156. ])
    157. # 参数1:传入路径
    158. # 参数2:变换器,这个变换器就是进行resize操作
    159. db = torchvision.datasets.ImageFolder(root='F:\\train', transform=tf)
    160. loader = DataLoader(db, batch_size=32, shuffle=True)
    161. print(db.class_to_idx) # 通过这个就能知道不同类别是如何编码的了。
    162. if __name__ == '__main__':
    163. main()

            将上面代码修改即可;

    四,模型训练;

            这里我们需要用到可视化工具来查看我们训练效果。

            安装visdom:

    pip install visdom

            在pycharm命令界面启动visdom:

    python -m visdom.server  
    

            正常启动在浏览器输入localhost:8097打开可视化界面;

            准备工作完成,编写模型训练代码,这么我们直接使用Pytorch自带的神经网络resnet18模型;

    1. import torch
    2. from torch import optim, nn
    3. import visdom
    4. import torchvision
    5. from torch.utils.data import DataLoader
    6. from pokemon import Pokemon
    7. from torchvision.models import resnet18 # 这个resnet18是已经training好的状态
    8. from utils import Flatten # 用于打平,这个是自己来实现的打平层
    9. batchsz = 32
    10. lr = 1e-3
    11. epochs = 40
    12. device = torch.device('cuda')
    13. torch.manual_seed(1234) # 这个是随机数种子,保证每次都能复现出来。
    14. # 这里是需要实例化Pokemon类
    15. # 这里之所以使用224,是因为是ResNet最适合的大小。
    16. train_db = Pokemon('F:\\train', 224, 'train')
    17. val_db = Pokemon('F:\\train', 224, 'val')
    18. test_db = Pokemon('F:\\train', 224, 'test')
    19. # 批量加载数据
    20. # 参数num_workers表示工作线程数:
    21. train_loader = DataLoader(train_db
    22. , batch_size=batchsz
    23. , shuffle=True
    24. , num_workers=4)
    25. val_loader = DataLoader(val_db
    26. , batch_size=batchsz
    27. , num_workers=2)
    28. test_loader = DataLoader(test_db
    29. , batch_size=batchsz
    30. , num_workers=2)
    31. # 需要把train的进度保存下来,需要用到visdom
    32. viz = visdom.Visdom()
    33. # 建立一个测试函数:测试函数针对validation和test功能是一样的
    34. def evalute(model, loader):
    35. # 用于统计总的预测正确的数量
    36. correct = 0
    37. # 总的测试数量
    38. total = len(loader.dataset)
    39. for x, y in loader:
    40. x, y = x.to(device), y.to(device)
    41. with torch.no_grad(): # test和validation是不需要梯度信息的
    42. logits = model(x)
    43. pred = logits.argmax(dim=1) # 最大的值所在的位置
    44. # 总的预测正确的数量,累加操作
    45. correct += torch.eq(pred, y).sum().float().item()
    46. accuracy = correct / total
    47. return accuracy
    48. def main():
    49. # 实例化模型
    50. # 使用已经训练好的resnet18模型,一定要设置这个参数pretrained=True
    51. trained_model = resnet18(pretrained=True)
    52. # 我们要使用训练好的resnet18模型的A部分,即取出前17层:
    53. # Sequential结束的是一个打散的数据,所有我们在list前加一个*,*args:接收若干个位置参数,转换成元组tuple形式。
    54. model = nn.Sequential(*list(trained_model.children())[:-1] # model的前17层(即A部分)返回的结果是:[b,512,1,1]
    55. , Flatten() # 打平操作从[b,512,1,1]=>[b,512]
    56. , nn.Linear(512, 14) # 这层是最后那层,用于从新学习分成14类。(第二个参数为自定义数据集实际训练种类数量,根据自己数据集的种类数据传递实际值)
    57. ).to(device)
    58. # 我们从已经训练好的resnet18开始训练效果会好很多
    59. # # 这里我们测试一下
    60. # x = torch.randn(2,3,224,224)
    61. # print(model(x).shape)#打印结果为:torch.Size([2, 5])
    62. # #这样就实现了transfer learning
    63. # ======================================================
    64. # 创建一个优化器Adam,这个优化器比较好
    65. optimizer = optim.Adam(model.parameters(), lr=lr)
    66. # Loss的计算方法:CrossEntropyLoss;
    67. # 这个Loss所接受的参数是logits,logits是不需要经过一个softmax的,只需要得到logits即可。
    68. criteon = nn.CrossEntropyLoss()
    69. # 用于保存模型的训练状态
    70. best_acc, best_epoch = 0, 0
    71. # step每次都是从0开始的,因此这里我们创建一个全局step
    72. global_step = 0
    73. # 用visdom工具保存下accuracy和loss
    74. # training和loss的曲线
    75. # x=0,y=-1是初始状态
    76. viz.line([0], [-1], win='loss', opts=dict(title='loss(损失值)'))
    77. # training和validation accuracy的曲线
    78. viz.line([0], [-1], win='val_acc', opts=dict(title='val_acc(准确率)'))
    79. # training逻辑
    80. for epoch in range(epochs):
    81. for step, (x, y) in enumerate(train_loader):
    82. # x:[b,3,224,224]; y:[b]
    83. x, y = x.to(device), y.to(device) # x和y都转移到cuda上面
    84. # 执行forward函数
    85. logits = model(x) # 学出的预测结果
    86. # 在pytorch中crossEntropyLoss中,传入的真实值y不需要进行one-hot操作,不需要做one-hot编码,会在内部做one-hot。
    87. # 所以我们直接传入y就可以了。
    88. loss = criteon(logits, y) # 预测结果与真实值进行交叉熵计算
    89. # 前向传播和迭代过程
    90. # 优化器
    91. optimizer.zero_grad()
    92. loss.backward()
    93. optimizer.step()
    94. # 用visdom工具保存下accuracy和loss
    95. # 每一个step我都要记录下来
    96. # validation和loss的曲线
    97. # x=loss.item()loss是一个tensor,因此需要通过item转为具体数值,y=-1是初始状态
    98. # 参数update为append,表示添加到曲线的末尾。
    99. viz.line([loss.item()], [global_step], win='loss', update='append')
    100. global_step += 1
    101. # 这里我们每完成两个epoch就做一组validation
    102. if epoch % 1 == 0:
    103. # 我们根据validation accuracy来选择要不要保存这个模型的训练状态。
    104. val_acc = evalute(model, val_loader)
    105. # 如果当前accuracy大于best_acc,就保存当前的状态:
    106. if val_acc > best_acc:
    107. best_epoch = epoch
    108. best_acc = val_acc
    109. # 保存当前模型的状态:
    110. # 参数一:模型状态值
    111. # 参数二:模型状态保存的文件名,文件名后缀随意
    112. torch.save(model, 'best-pro.pth')
    113. # validation和 accuracy的曲线
    114. # 这里val_acc是数值型,所以不需要转换。
    115. viz.line([val_acc], [global_step], win='val_acc', update='append')
    116. print('best acc:', best_acc, 'best epoch:', best_epoch)
    117. # 从最好的状态加载模型:
    118. # model.load_state_dict(torch.load('best-pro.ptl'))
    119. # print('loaded from check point!')
    120. #
    121. # # 上面加载了最好的模型状态,这里使用的模型也是最好的状态时的模型
    122. # test_acc = evalute(model, test_loader)
    123. # print('test_acc:', test_acc)
    124. if __name__ == '__main__':
    125. main()

    这里我们用到了一个util:

    1. from matplotlib import pyplot as plt
    2. import torch
    3. from torch import nn
    4. # 该函数是一个标准的打平层
    5. class Flatten(nn.Module):
    6. # 该文件utils包含一些辅助函数。
    7. def __init__(self):
    8. super(Flatten, self).__init__()
    9. def forward(self, x):
    10. shape = torch.prod(torch.tensor(x.shape[1:])).item()
    11. return x.view(-1, shape)
    12. # 该函数是将img打印到matplotlib上
    13. def plot_image(img, label, name):
    14. fig = plt.figure()
    15. for i in range(6):
    16. plt.subplot(2, 3, i + 1)
    17. plt.tight_layout()
    18. plt.imshow(img[i][0] * 0.3081 + 0.1307, cmap='gray', interpolation='none')
    19. plt.title("{}: {}".format(name, label[i].item()))
    20. plt.xticks([])
    21. plt.yticks([])
    22. plt.show()

    运行函数打开可视化界面,查看训练情况;

            刚开始训练的情况,使用数据量大概1.6w张最终结果大概是准确率96%。已经非常好了。

    五,模型验证;

    1. import numpy as np
    2. import torch
    3. import torch.nn.functional as F
    4. import torchvision.transforms as transforms
    5. from PIL import Image
    6. device = torch.device('cuda')
    7. def main():
    8. labels = ['兔子', '吊兰', '文竹', '月季', '枸骨', '狗', '狮子', '猫', '绿萝', '老虎', '菊花', '蛇', '迎春花', '龟背竹']
    9. image_path = "C:/Users/LENOVO/Desktop/dog.png"
    10. image = Image.open(image_path)
    11. image = image.resize((256, 256), Image.BILINEAR).convert("RGB")
    12. image = np.array(image)
    13. to_tensor = transforms.Compose([
    14. transforms.ToTensor(),
    15. transforms.Normalize((0.485, 0.456, 0.406), (0.229, 0.224, 0.225))])
    16. image = to_tensor(image)
    17. image = torch.unsqueeze(image, 0)
    18. image = image.cuda()
    19. model = torch.load("刚才训练好的模型")
    20. model.eval()
    21. model.to(device)
    22. output = model(image)
    23. output1 = F.softmax(output, dim=1)
    24. predicted = torch.max(output1, dim=1)[1].cpu().item()
    25. outputs2 = output1.squeeze(0)
    26. confidence = outputs2[predicted].item()
    27. confidence = round(confidence, 3)
    28. print("识别结果: ", labels[predicted], " 准确率为: ", confidence * 100, "%")
    29. if __name__ == '__main__':
    30. main()

            测试图片:

            labels为我们训练的类别数组,和cvs的索引对应。

    多次测试结果全对,准确率不低于95%。

  • 相关阅读:
    《数据仓库入门实践》
    Pandas与数据库交互详解
    Flowable 之任务分配
    【网络】网络层协议:IP(待更新)
    [2022/6/29]考试总结
    【django2.0之Rest_Framework框架一】rest_framework序列器介绍
    Docker快速搭建漏洞靶场指南
    elementui中表格组件的高度修改没效果
    SpringBoot日志文件
    Java面试题-Java核心基础-第八天(异常)
  • 原文地址:https://blog.csdn.net/liukangjie520/article/details/132901511