前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >开源图书《Python完全自学教程》12.6机器学习案例12.6.2猫狗二分类

开源图书《Python完全自学教程》12.6机器学习案例12.6.2猫狗二分类

作者头像
老齐
发布2022-12-09 20:26:58
7320
发布2022-12-09 20:26:58
举报
文章被收录于专栏:老齐教室

12.6.2 猫狗二分类

深度学习是机器学习的一个分支,目前常用的深度学习框架有 TensorFlow、PyTorch和飞桨等(飞桨,即 PaddlePaddle,全中文的官方文档,让学习者不为语言而担忧)。本小节中将以 PyTorch 演示一个经典的案例,让初学 Python 的读者对深度学习有感性地认识。所以,以下代码可不求甚解,只要能认识到所涉及到的基础知识并不陌生即可——除了 PyTorch 部分。

“Dogs vs. Cats”是一个传统的二分类问题,下面示例所用的数据集来自于 kaggle.com ,在项目网页(https://www.kaggle.com/c/dogs-vs-cats-redux-kernels-edition/)上可以看到两个压缩包(登录网站之后可以下载),train.zip 用作训练集(其中一部分作为验证集),test.zip 用作测试集。在训练集中(将所下载的 train.zip ,解压缩之后,放到 ./data/train 目录中),所有图片都是用 cat.<id>.jpgdog.<id>.jpg 格式命名,用图片文件的名称作为每张图片的标签(如图12-6-2所示)。

图12-6-2 训练集中的图片

1. 组织数据

将所下载的压缩包 train.zip 在目录 ./data 中解压缩。并在 ./data/train 中创建两个子目录,即 ./data/train/cats./data/train/dogs 。然后创建子目录 ./data/val ,在其中也创建另两个子目录 ./data/val/dogs./data/val/cats 。将 ./data/train 中的数据称为训练集./data/val 中的称为验证集

将所下载的压缩包 test.zip 也在 ./data 中解压缩,得到子目录 ./data/test ,此目录中的数据称为测试集

经过上述操作之后,得到了如下所示的目录结构:

代码语言:javascript
复制
% tree data -d
data
├── test
├── train
│   ├── cats
│   └── dogs
└── val
    ├── cats
    └── dogs

7 directories

然后写一段代码,按照前述要求,将本来已经存在 ./data/train 中的图片,按照文件名中所标示出的 dogcat 分别移动到 ./data/train/cats./data/train/dogs 目录中。

代码语言:javascript
复制
[11]: import re
      import shutil
      import os

      # 训练集目录
      train_dir = "./data/train"
      train_dogs_dir = f"{train_dir}/dogs"
      train_cats_dir = f"{train_dir}/cats"
      # 验证集目录
      val_dir = "./data/val"
      val_dogs_dir = f"{val_dir}/dogs"
      val_cats_dir = f"{val_dir}/cats"

      files = os.listdir(train_dir) 
      for f in files:
          cat_search = re.search('cat', f)    # (1)
          dog_search = re.search('dog', f)
          if cat_search:
              shutil.move(f'{train_dir}/{f}', train_cats_dir)  # (2)
          if dog_search:
              shutil.move(f"{train_dir}/{f}", train_dogs_dir)

在代码 [11] 中将前面所创建的目录结构分别用变量引用,并且实现了图片移动。注释(1)中使用了标准库中的 re 模块,用正则表达式判断字符串 cat 是否在文件名中。例如(在 Python 交互模式中演示):

代码语言:javascript
复制
>>> import re
>>> bool(re.search('cat', 'cat.5699.jpg'))
True
>>> bool(re.search('cat', 'dog.10149.jpg'))
False

关于模块 re 的更多内容,可以参考官方文档(https://docs.python.org/3/library/re.html)。

注释(2)中使用的 shutil 模块也是 Python 标准库的一员,函数 shutil.move() 能够将文件移动到指定目录中( shutil 模块的官方文档地址:https://docs.python.org/3/library/shutil.html)。

运行代码块 [11] 后,将猫和狗的图片分别放在了两个不同的目录中,在 Jupyter 中可以这样查看( ls 是 Linux 命令):

代码语言:javascript
复制
[12]: print("目录 train_dir 中已经没有图片")
      !ls {train_dir} | head -n 5

      print("目录 train_dogs_dir 中是狗图片(显示5个)")
      !ls {train_dogs_dir} | head -n 5

      print("目录 train_cats_dir 中是猫图片(显示5个)")
      !ls {train_cats_dir} | head -n 5
      
# 输出
      目录 train_dir 中已经没有图片
      cats
      dogs
      目录 train_dogs_dir 中是狗图片(显示5个)
      dog.0.jpg
      dog.1.jpg
      dog.10.jpg
      dog.100.jpg
      dog.1000.jpg
      目录 train_cats_dir 中是猫图片(显示5个)
      cat.0.jpg
      cat.1.jpg
      cat.10.jpg
      cat.100.jpg
      cat.1000.jpg

./data/train/cats./data/train/dogs 两个目录中,各有 12500 张图片,再从每个目录中取一部分(此处取 1000 张)图片分别放到对应的验证集目录 ./data/val/cats./data/val/dogs 中。

代码语言:javascript
复制
[13]: dogs_files = os.listdir(train_dogs_dir)
      cats_files = os.listdir(train_cats_dir)

      for dog in dogs_files:
          val_dog_search = re.search("7\d\d\d", dog)
          if val_dog_search:
              shutil.move(f"{train_dogs_dir}/{dog}", val_dogs_dir)

      for cat in cats_files:
          val_cat_search = re.search("7\d\d\d", cat)
          if val_cat_search:
              shutil.move(f"{train_cats_dir}/{cat}", val_cats_dir)
        
      print("目录 val_dogs_dir 中是狗图片")
      !ls {val_dogs_dir} | head -n 5
      print("目录 val_cats_dir 中是狗图片")
      !ls {val_cats_dir} | head -n 5
# 输出:
      目录 val_dogs_dir 中是狗图片
      dog.7000.jpg
      dog.7001.jpg
      dog.7002.jpg
      dog.7003.jpg
      dog.7004.jpg
      目录 val_cats_dir 中是狗图片
      cat.7000.jpg
      cat.7001.jpg
      cat.7002.jpg
      cat.7003.jpg
      cat.7004.jpg

代码块 [13] 将文件名中 <id> 为 7000 至 7999 的图片移动到相应的验证集目录中。

2. 训练模型

数据已经组织好了,即将使用 PyTorch 创建并训练模型。PyTorch 的官方网站是:https://pytorch.org/ ,它提供了非常友好的 Python 接口,与其他第三方包一样,安装后即可使用。

代码语言:javascript
复制
% pip install torch torchvision

安装完毕。如果在如下所演示的 Jupyter 代码中提示无法找到 torch ,可以关闭并退出当前的 Jupyter Lab 后,从新执行 jupyter-lab ——如果还找不到 torch ,请在网上搜索有关资料,并结合本地环境进行修改(导致“搜索路径”问题的因素较多,比如环境变量设置等,需要读者细心、耐心地解决)。

代码语言:javascript
复制
[14]: import numpy as np

      import torch
      import torch.nn as nn
      import torch.optim as optim
      from torch.optim import lr_scheduler

      import torchvision
      from torchvision import datasets, models, transforms

      import matplotlib.pyplot as plt
      import time
      import os
      import copy
      import math

      torch.__version__
[14]: '1.6.0'

按照代码块 [14] 将有关对象引入,此处所用的 PyTorch 版本是 1.6.0 ,读者所安装的若不低于这个版本,代码一般通用。

再次声明,本节不是 PyTorch 的完整学习资料,所以对代码不会做非常详尽地解释,读者囫囵吞枣也无妨,只需要有初步体验即可。

在深度学习项目中,数据扩充(或称“数据增强”、“数据增广”,data augmentataion)往往是不可避免的,这是由于缺少海量数据,为了保证模型的有效性,本着“一分钱掰成两半花”的精神而进行的。最简单的数据扩充方法包括翻转、旋转、尺度变换等等。另外,由于不同的图片大小各异,也需要将图片尺寸规范到限定的范围。还有就是要张量化,才能用于模型的张量运算(关于“张量”的基本概念,参阅拙作《机器学习数学基础》)。

代码语言:javascript
复制
[15]: data_transforms = {
          'train': transforms.Compose([
                       transforms.RandomRotation(5),
                       transforms.RandomHorizontalFlip(),
                       transforms.RandomResizedCrop(
                           224, 
                           scale=(0.96, 1.0), 
                           ratio=(0.95, 1.05)), 
                       transforms.ToTensor(),
                       transforms.Normalize(
                           [0.485, 0.456, 0.406], 
                           [0.229, 0.224, 0.225])]),
          'val': transforms.Compose([
                       transforms.Resize([224, 224]), 
                       transforms.ToTensor(), 
                       transforms.Normalize(
                           [0.485, 0.456, 0.406], 
                           [0.229, 0.224, 0.225])]),
      }

然后使用 data_transforms 定义训练集和验证集数据,以及必要的常量。

代码语言:javascript
复制
[16]: data_dir = 'data'
      CHECK_POINT_PATH = './data/checkpoint.tar'
      SUBMISSION_FILE = "./data/submission.csv"
      image_datasets = {x: datasets.ImageFolder(
                               os.path.join(data_dir, x), 
                               data_transforms[x]) for x in ['train', 'val']}
      dataloaders = {x: torch.utils.data.DataLoader(
                            image_datasets[x], 
                            batch_size=4, 
                            shuffle=True, 
                            num_workers=4) for x in ['train', 'val']}
      dataset_sizes = {x: len(image_datasets[x]) for x in ['train', 'val']}
      class_names = image_datasets['train'].classes

      device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')

      print(class_names)
      print(f"Train image size: {dataset_sizes['train']}")
      print(f"Validation image size: {dataset_sizes['val']}")
 
 # 输出:
      ['cats', 'dogs']
      Train image size: 23000
      Validation image size: 2000

现在训练集中的图片数量是 23000,验证集有 2000 张图片。在代码块 [16] 的 dataloaders 中设置 batch_size=4 (batch,常译为“批”),下面就显示训练集中由 4 张图片组成的“1批”(随机抽取)。

代码语言:javascript
复制
[17]: def imshow(inp, title=None):
          inp = inp.numpy().transpose((1, 2, 0))
          mean = np.array([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)
          plt.imshow(inp)
          if title is not None:
              plt.title(title)
          plt.pause(0.001)

      inputs, classes = next(iter(dataloaders['train']))
      sample_train_images = torchvision.utils.make_grid(inputs)
      imshow(sample_train_images, title=classes)

输出图像:

这批(batch)图片即对应于张量 tensor([1, 0, 1, 0])

下面编写训练模型的函数。

代码语言:javascript
复制
[18]: def train_model(model, criterion, optimizer, 
                      scheduler, num_epochs=2, checkpoint = None):
          since = time.time()

          if checkpoint is None:
              best_model_wts = copy.deepcopy(model.state_dict())
              best_loss = math.inf
              best_acc = 0.
          else:
              print(f'Val loss: {checkpoint["best_val_loss"]}, 
                      Val accuracy: {checkpoint["best_val_accuracy"]}')
              model.load_state_dict(checkpoint['model_state_dict'])
              best_model_wts = copy.deepcopy(model.state_dict())
              optimizer.load_state_dict(checkpoint['optimizer_state_dict'])
              scheduler.load_state_dict(checkpoint['scheduler_state_dict'])
              best_loss = checkpoint['best_val_loss']
              best_acc = checkpoint['best_val_accuracy']

          for epoch in range(num_epochs):
              print('Epoch {}/{}'.format(epoch, num_epochs - 1))
              print('-' * 10)

              # 每轮(epoch)含一次训练和验证
              for phase in ['train', 'val']:
                  if phase == 'train':
                      scheduler.step()
                      model.train()  
                  else:
                      model.eval()   
                  running_loss = 0.0
                  running_corrects = 0

                  # Iterate over data.
                  for i, (inputs, labels) in enumerate(dataloaders[phase]):
                      inputs = inputs.to(device)
                      labels = labels.to(device)

                      # 梯度归零
                      optimizer.zero_grad()
                
                      if i % 200 == 199:
                          print(f'[{epoch+1}, {i}] loss: 
                                  {running_loss/(i*inputs.size(0)):.3f}')

                      # 前向
                      # 跟踪训练过程
                      with torch.set_grad_enabled(phase == 'train'):
                          outputs = model(inputs)
                          _, preds = torch.max(outputs, 1)
                          loss = criterion(outputs, labels)

                          # 后向/反向,在训练过程中
                          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 / dataset_sizes[phase]
                  epoch_acc = running_corrects.double() 
                              / dataset_sizes[phase]

                  print(f'{phase} Loss: {epoch_loss:.4f} Acc: 
                          {epoch_acc:.4f}')

                  # 深拷贝模型
                  if phase == 'val' and epoch_loss < best_loss:
                      print(f'New best model found!')
                      print(f'New record loss: {epoch_loss}, 
                              previous record loss: {best_loss}')
                      best_loss = epoch_loss
                      best_acc = epoch_acc
                      best_model_wts = copy.deepcopy(model.state_dict())

              print()

          time_elapsed = time.time() - since
          print(f'Training complete in 
                  {time_elapsed // 60:.0f}m {time_elapsed % 60:.0f}s')
          print(f'Best val Acc: {best_acc:.4f} Best val loss: 
                  {best_loss:.4f}')

          # 载入最佳的模型权重
          model.load_state_dict(best_model_wts)
          return model, best_loss, best_acc

代码块 [18] 的函数首先检查 checkpoint 的值,如果为 True 则会加载训练的模型,并在其基础上更新参数,否则会从头开始训练。在本示例中,我们提供了一个预训练的模型——在此基础上继续训练,可以减少训练时间——即代码块 [16] 中的 CHECK_POINT_PATH = './data/checkpoint.tar' (读者可以在本书的源码仓库中获得,代码仓库地址参阅 www.itdiffer.com 中关于本书的在线资料)。

之后载入卷积神经网络模型,它擅长于图像识别。

代码语言:javascript
复制
[19]: model_conv = torchvision.models.resnet50(pretrained=True)

当前的任务是二分类,故还要对此卷积神经网络模型进行个性化设置,如定义损失函数(交叉熵,nn.CrossEntropyLoss() )、优化器算法(随机梯度下降,SGD )、学习率( lr_scheduler.StepLR() )。

代码语言:javascript
复制
[20]: for param in model_conv.parameters():    # (3)
          param.requires_grad = False

      num_ftrs = model_conv.fc.in_features
      model_conv.fc = nn.Linear(num_ftrs, 2)

      model_conv = model_conv.to(device)

      criterion = nn.CrossEntropyLoss()

      optimizer_conv = optim.SGD(model_conv.fc.parameters(), 
                                 lr=0.001, momentum=0.9)

      exp_lr_scheduler = lr_scheduler.StepLR(optimizer_conv, 
                                             step_size=7, gamma=0.1)

在代码块 [20] 的注释(3)中,设置 param.requires_grad = False 旨在仅训练所导入的卷积神经网络 resnet50 模型的最后一层的参数。如果要训练所有层的所有参数,需要将代码做如下修改:

代码语言:javascript
复制
for param in model_conv.parameters():
    param.requires_grad = True
    
model_conv = model_conv.to(device)

optimizer_ft = optim.SGD(model_conv.parameters(), lr=0.001, momentum=0.9)

下面进入实质化的训练过程(训练过程所用的时间会因本地计算机的性能和配置有所不同,但都要耗费一段时间,需要耐心等待)。

代码语言:javascript
复制
[21]: try:
          checkpoint = torch.load(CHECK_POINT_PATH)
          print("checkpoint loaded")
      except:
          checkpoint = None
          print("checkpoint not found")
      model_conv, best_val_loss, best_val_acc = 
                     train_model(model_conv,
                                 criterion,
                                 optimizer_conv,
                                 exp_lr_scheduler,
                                 num_epochs = 3,
                                 checkpoint = checkpoint)
      torch.save({'model_state_dict': model_conv.state_dict(),
                  'optimizer_state_dict': optimizer_conv.state_dict(),
                  'best_val_loss': best_val_loss,
                  'best_val_accuracy': best_val_acc,
                  'scheduler_state_dict' : exp_lr_scheduler.state_dict(),
                  }, CHECK_POINT_PATH)
# 输出
      Val loss: 0.03336585155675109, Val accuracy: 1.0
      Epoch 0/2
      ----------
      [1, 199] loss: 0.240
      ...  # 省略部分显示
      train Loss: 0.2325 Acc: 0.9055
      [1, 199] loss: 0.068
      [1, 399] loss: 0.068
      val Loss: 0.0699 Acc: 0.9770

      Epoch 1/2
      ----------
      [2, 199] loss: 0.196
      ...  # 省略部分显示
      train Loss: 0.2363 Acc: 0.9046
      [2, 199] loss: 0.054
      [2, 399] loss: 0.062
      val Loss: 0.0664 Acc: 0.9770

      Epoch 2/2
      ----------
      [3, 199] loss: 0.255
      ...  # 省略部分显示
      train Loss: 0.2341 Acc: 0.9040
      [3, 199] loss: 0.064
      [3, 399] loss: 0.065
      val Loss: 0.0651 Acc: 0.9760

      Training complete in 349m 52s      # 这是训练所用时间
      Best val Acc: 1.0000 Best val loss: 0.0334

当代码块 [21] 终于运行完毕——一般而言,这是一个漫长的过程——就训练好了一个具有识别猫、狗能力的模型 model_conv

然后编写如下代码,检验模型在验证集上的“识别”结果。

代码语言:javascript
复制
[22]: def visualize_model(model, num_images=2):
          was_training = model.training
          model.eval()
          images_so_far = 0
          fig = plt.figure()

          with torch.no_grad():
              for i, (inputs, labels) in enumerate(dataloaders['val']):
                  inputs = inputs.to(device)
                  labels = labels.to(device)
                  outputs = model(inputs)
                  _, preds = torch.max(outputs, 1)

                  for j in range(inputs.size()[0]):
                      images_so_far += 1
                      ax = plt.subplot(num_images//2, 2, images_so_far)
                      ax.axis('off')
                      ax.set_title(f'predicted: {class_names[preds[j]]}')
                      imshow(inputs.cpu().data[j])

                      if images_so_far == num_images:
                          model.train(mode=was_training)
                          return
              model.train(mode=was_training)
visualize_model(model_conv)

输出图示:

但这不是对模型的真正测试。

3. 测试模型

保存在子目录 ./data/test 里面的图片为“测试集” ,现在就用它们来检验模型的“识别”能力。测试集中的每个图片文件以 <id>.jpg 格式命名,从文件名上不知道它是猫还是狗。

本来,可以用 PyTorch 直接从 .data/test 中读入数据。但是,为了向读者多展示一些 Python 库的应用,此处改用另外的方式。首先,创建一个将图片转换为张量的函数(类似于代码块 [15] 的 data_transforms['val'] )。

代码语言:javascript
复制
[23]: def apply_test_transforms(inp):
          out = transforms.functional.resize(inp, [224,224])
          out = transforms.functional.to_tensor(out)
          out = transforms.functional.normalize(out, 
                                                [0.485, 0.456, 0.406], 
                                                [0.229, 0.224, 0.225])
          return out

Python 中关于图片的库被称为 Python Imageing Library ,简称 PIL ,其中 Pillow 是 PIL 的一个常用分支(同样是一个库),其官方网站是:https://pillow.readthedocs.io/。安装方法如下:

代码语言:javascript
复制
% pip install Pillow

然后用 PIL 从测试集中读取文件(以下代码中显示其中的一张图片)。

代码语言:javascript
复制
[24]: from PIL import Image

      test_data_dir = f'{data_dir}/test'
      test_data_files = os.listdir(test_data_dir)
      im = Image.open(f'{test_data_dir}/{test_data_files[0]}')
      plt.imshow(im)

输出图示:

我们已经看到代码块 [24] 中的 im 所引用的图片是一只猫。下面将 im 传入代码块 [23] 定义的函数进行变换。

代码语言:javascript
复制
[25]: im_as_tensor = apply_test_transforms(im)
      print(im_as_tensor.size())
      minibatch = torch.stack([im_as_tensor])
      print(minibatch.size())
# 输出
      torch.Size([3, 224, 224])
      torch.Size([1, 3, 224, 224])

再将 minibatch 传给模型 model_conv ,让它“辨别”图片是猫还是狗。

代码语言:javascript
复制
[26]: preds = model_conv(minibatch)
      preds
[26]: tensor([[ 2.0083, -1.8386]], grad_fn=<AddmmBackward>)

返回值 preds 是一个张量,按照代码块 [26] 的张量输出结果,可知这张图片是猫(第一个数大于第二个数,则是猫)。如果用更直观地方式表述预测结果,可以:

代码语言:javascript
复制
[27]: soft_max = nn.Softmax(dim=1)
      probs = soft_max(preds)
      probs
[27]: tensor([[0.9791, 0.0209]], grad_fn=<SoftmaxBackward>)

将张量里面的数字转化为百分比,probs 的结果说明模型 model_conv “认为”这张图片是猫的概率为 97.91% 。

4. 参加 kaggle 比赛

本小节的项目来自于 kaggle.com ,这是一个著名的深度学习竞赛网站,如果读者也有打算参加,必须要按照网站要求提交 submission.csv 的文件(“Dogs vs. Cats”的竞赛项目已经结束,读者可以参考下述方法,以备参加其他项目),基本格式为:

代码语言:javascript
复制
id,label
1,0.5
2,0.5

其中 id 是测试集(./data/test )中所有图片的 <id>label 为该图片是狗的概率。为此,编写如下函数:

代码语言:javascript
复制
[28]: def predict_dog(model, tensor):
          batch = torch.stack([tensor])
          softMax = nn.Softmax(dim = 1)
          preds = softMax(model(batch))
          return preds[0, 1].item()
          
      def test_data(fname):
          im = Image.open(f'{test_data_dir}/{fname}')
          return apply_test_transforms(im)
        
      import re
      def extract_file_id(fname):      # 从文件名中提取 id
          print("Extracting id from " + fname)
          return int(re.search('\d+', fname).group())

然后执行模型的测试函数,并生成一个以 <id> 为键(整数类型,便于排序),以“是狗”的概率为值的字典(下面的代码需要要执行一段时间,测试集中共计 12500 张图片)。

代码语言:javascript
复制
[29]: model_conv.eval()
      id_to_dog_prob = {extract_file_id(fname): 
                        predict_dog(model_conv,test_data(fname))
                        for fname in test_data_files}
# 输出
    Extracting id from 9733.jpg
    ...  # 省略余下显示内容

为了最终得到 .csv 文件,再用 Pandas 将字典对象 id_to_dog_prob 转化为 DataFrame 对象,并保存为 .csv 文件。

代码语言:javascript
复制
[30]: import pandas as pd

      ds = pd.Series({id : label 
                      for (id, label) in 
                      zip(id_to_dog_prob.keys(), id_to_dog_prob.values())})
      df = pd.DataFrame(ds, columns = ['label']).sort_index()
      df['id'] = df.index
      df = df[['id', 'label']]
      df.to_csv(SUBMISSION_FILE, index = False)

最后将 ./data/submission.csv 文件提交到 kaggle 网站即可——虽然此比赛项目已经结束,还可以自我辉煌战果:

代码语言:javascript
复制
[31]: df.sample(5)     # 随机选出 5 条记录
[31]:        id    label
      5548 5548 0.999494
      8238 8238 0.998453
      8961 8961 0.999983
      4762 4762 0.003668
      2623 2623 0.000197

自学建议 如果读者有意将来从事机器学习有关的工作,所要学习的知识除了编程语言之外(最常用的编程语言是 Python ,此外还有 R 、Julia 等),还包括12.4节中科学计算的有关内容。除此之外,针对机器学习和深度学习都有一些库或开发框架,使用它们就相当于“站在巨人肩膀上”,或者说找到了“生产力工具”,比如 scikit-learn 、PyTorch、Tensorflow、飞桨(PaddlePaddle)等。 以上所列都是进入机器学习领域的技术准备,除了这些之外,还有一个前置的知识准备:足够的数学知识(参阅拙作《机器学习数学基础》,电子工业出版社)。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-08-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 老齐教室 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 12.6.2 猫狗二分类
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档