【Pytorch】模型部署

文章目录

  • [0. 进行设置](#0. 进行设置)
  • [1. 获取数据](#1. 获取数据)
  • [2. FoodVision Mini模型部署实验概要](#2. FoodVision Mini模型部署实验概要)
  • [3. 创建 EffNetB2 特征提取器](#3. 创建 EffNetB2 特征提取器)
  • [4. 创建 ViT 特征提取器](#4. 创建 ViT 特征提取器)
  • [5. 使用训练好模型进行预测并计时](#5. 使用训练好模型进行预测并计时)
  • [6. 比较模型结果、预测时间和大小](#6. 比较模型结果、预测时间和大小)
  • [7. 通过创建 Gradio 演示让 FoodVision Mini 呈现](#7. 通过创建 Gradio 演示让 FoodVision Mini 呈现)
  • [8. 将Gradio demo变成可部署的应用程序](#8. 将Gradio demo变成可部署的应用程序)
  • [9. 将 FoodVision Mini 应用程序部署到 HuggingFace Spaces](#9. 将 FoodVision Mini 应用程序部署到 HuggingFace Spaces)
  • 补充:创建更大的FoodVision
  • [补充:将 FoodVision Big 模型转变为可部署的应用程序](#补充:将 FoodVision Big 模型转变为可部署的应用程序)

PyTorch 模型向公众开放,把模型作为可用的应用程序部署到互联网上。

机器学习模型部署涉及将模型提供给其他人或其他人。例如,有人可能会将您的模型用作食品识别应用程序的一部分。其他东西可能是使用您的模型的另一个模型或程序,例如使用机器学习模型来检测交易是否欺诈的银行系统。

部署机器学习模型的方法:

Tool/resource Deployment type
Google's ML Kit On-device (Android and iOS)
Apple's Core ML and coremltools Python package On-device (all Apple devices)
Amazon Web Service's (AWS) Sagemaker Cloud
Google Cloud's Vertex AI Cloud
Microsoft's Azure Machine Learning Cloud
Hugging Face Spaces Cloud
API with FastAPI Cloud/self-hosted server
API with TorchServe Cloud/self-hosted server
ONNX (Open Neural Network Exchange) Many/general
Many more...

最好的方法之一就是使用 Gradio 将机器学习模型转变为演示应用程序,然后将其部署在 Hugging Face Spaces 上。

本文的目标是通过具有以下指标的演示 Gradio 应用程序部署 FoodVision 模型,(模型可以看前面博客),最好的两个模型:EffNetB2 和 ViT 特征提取器,通过比较这两个模型,选择一个较好的进行部署。

本文流程:

  1. 进行设置
  2. 获取数据
  3. FoodVision Mini模型部署实验概要
  4. 创建 EffNetB2 特征提取器
  5. 创建 ViT 特征提取器
  6. 使用训练好模型进行预测并计时
  7. 比较模型结果、预测时间和大小
  8. 通过创建 Gradio 演示让 FoodVision Mini 呈现
  9. 将 FoodVision Mini Gradio 演示转变为可部署的应用程序
  10. 将 Gradio 演示部署到 HuggingFace Spaces

0. 进行设置

导入前面博客已经编写过的脚本:data_setup.pyengine.pyhelper_functions.pyutils.py,通过链接添加链接描述,直接在 0. 进行设置 进行copy重复使用即可。

导入需要的库和设置与设备无关:

python 复制代码
# Continue with regular imports
import matplotlib.pyplot as plt
import torch
import torchvision

from torch import nn
from torchvision import transforms

# Try to get torchinfo, install it if it doesn't work
from torchinfo import summary

# Try to import the going_modular directory, download it from GitHub if it doesn't work
from going_modular.going_modular import data_setup, engine
from helper_functions import download_data, set_seeds, plot_loss_curves
python 复制代码
device = "cuda" if torch.cuda.is_available() else "cpu"
device

1. 获取数据

下载的数据集是整个 Food101 数据集的样本(101 个食物类别,每个类别有 1,000 张图像)。选择数据集的20%, 是指随机选择的披萨、牛排和寿司类别中的 20% 的图像。

通过程序直接下载:

python 复制代码
# Download pizza, steak, sushi images from GitHub
data_20_percent_path = download_data(source="https://github.com/mrdbourke/pytorch-deep-learning/raw/main/data/pizza_steak_sushi_20_percent.zip",
                                     destination="pizza_steak_sushi_20_percent")

data_20_percent_path

设置训练和测试路径:

python 复制代码
# Setup directory paths to train and test images
train_dir = data_20_percent_path / "train"
test_dir = data_20_percent_path / "test"

2. FoodVision Mini模型部署实验概要

引入前面部分中表现最佳的模型:

  • EffNetB2 特征提取器(简称 EffNetB2) - 最初创建于 07 年。使用 torchvision.models.efficientnet_b2() 和调整后的 classifier 层。
  • ViT-B/16 特征提取器(简称 ViT) - 最初创建于 08 年。使用 torchvision.models.vit_b_16() 和调整后的 head 层。( ViT-B/16 代表"Vision Transformer Base,patch size 16")

注意:"特征提取器模型"通常从在与自己的问题类似的数据集上进行预训练的模型开始。预训练模型的基础层通常保持冻结(预训练模式/权重保持不变),而一些顶部(或分类器/分类头)层通过根据您自己的数据进行训练来根据您自己的问题进行定制。


3. 创建 EffNetB2 特征提取器

创建流程:

(1)将预训练权重设置为 weights=torchvision.models.EfficientNet_B2_Weights.DEFAULT ,其中" DEFAULT "表示"当前可用的最佳"(或者可以使用 weights="DEFAULT" )。

(2)使用 transforms() 方法从权重获取预训练模型图像转换(我们需要这些,以便我们可以将图像转换为与预训练 EffNetB2 训练时相同的格式)。

(3)通过将权重传递给 torchvision.models.efficientnet_b2 的实例来创建预训练的模型实例。

(4)冻结模型中的基础层。

(5)更新分类器头以适合我们自己的数据。

python 复制代码
# 1. Setup pretrained EffNetB2 weights
effnetb2_weights = torchvision.models.EfficientNet_B2_Weights.DEFAULT

# 2. Get EffNetB2 transforms
effnetb2_transforms = effnetb2_weights.transforms()

# 3. Setup pretrained model
effnetb2 = torchvision.models.efficientnet_b2(weights=effnetb2_weights) # could also use weights="DEFAULT"

# 4. Freeze the base layers in the model (this will freeze all layers to begin with)
for param in effnetb2.parameters():
    param.requires_grad = False

现在要更改分类器头,我们首先使用模型的 classifier 属性检查它:

python 复制代码
# Check out EffNetB2 classifier head
effnetb2.classifier
python 复制代码
Sequential(
  (0): Dropout(p=0.3, inplace=True)
  (1): Linear(in_features=1408, out_features=1000, bias=True)
)

要更改分类器头以满足我们自己的问题,让我们将 out_features 变量替换为我们拥有的相同数量的类(在我们的例子中, out_features=3 ,用于披萨、牛排、寿司分类)。

python 复制代码
# 5. Update the classifier head
effnetb2.classifier = nn.Sequential(
    nn.Dropout(p=0.3, inplace=True), # keep dropout layer same
    nn.Linear(in_features=1408, # keep in_features same 
              out_features=3)) # change out_features to suit our number of classes
  1. 创建一个函数来制作 EffNetB2 特征提取器

create_effnetb2_model() ,它将采用可自定义数量的类和随机种子参数来实现可重复性:

python 复制代码
def create_effnetb2_model(num_classes:int=3, 
                          seed:int=42):
    """Creates an EfficientNetB2 feature extractor model and transforms.

    Args:
        num_classes (int, optional): number of classes in the classifier head. 
            Defaults to 3.
        seed (int, optional): random seed value. Defaults to 42.

    Returns:
        model (torch.nn.Module): EffNetB2 feature extractor model. 
        transforms (torchvision.transforms): EffNetB2 image transforms.
    """
    # 1, 2, 3. Create EffNetB2 pretrained weights, transforms and model
    weights = torchvision.models.EfficientNet_B2_Weights.DEFAULT
    transforms = weights.transforms()
    model = torchvision.models.efficientnet_b2(weights=weights)

    # 4. Freeze all layers in base model
    for param in model.parameters():
        param.requires_grad = False

    # 5. Change classifier head with random seed for reproducibility
    torch.manual_seed(seed)
    model.classifier = nn.Sequential(
        nn.Dropout(p=0.3, inplace=True),
        nn.Linear(in_features=1408, out_features=num_classes),
    )
    
    return model, transforms

测试:

python 复制代码
effnetb2, effnetb2_transforms = create_effnetb2_model(num_classes=3,
                                                      seed=42)

用 torchinfo.summary() 获得摘要:

python 复制代码
from torchinfo import summary

# # Print EffNetB2 model summary (uncomment for full output) 
summary(effnetb2, 
        input_size=(1, 3, 224, 224),
        col_names=["input_size", "output_size", "num_params", "trainable"],
        col_width=20,
        row_settings=["var_names"])
  1. 为 EffNetB2 创建 DataLoader
python 复制代码
# Setup DataLoaders
from going_modular.going_modular import data_setup
train_dataloader_effnetb2, test_dataloader_effnetb2, class_names = data_setup.create_dataloaders(train_dir=train_dir,
                                                                                                 test_dir=test_dir,
                                                                                                 transform=effnetb2_transforms,
                                                                                                 batch_size=32)
  1. 训练EffNetB2特征提取器

我们可以通过创建一个优化器(我们将使用 torch.optim.Adam() 和学习率为 1e-3 )、一个损失函数(我们将使用 torch.nn.CrossEntropyLoss() 来实现)多类分类),然后将这些以及我们的 DataLoader 传递给 engine.train() 函数。

python 复制代码
from going_modular.going_modular import engine

# Setup optimizer
optimizer = torch.optim.Adam(params=effnetb2.parameters(),
                             lr=1e-3)
# Setup loss function
loss_fn = torch.nn.CrossEntropyLoss()

# Set seeds for reproducibility and train the model
set_seeds()
effnetb2_results = engine.train(model=effnetb2,
                                train_dataloader=train_dataloader_effnetb2,
                                test_dataloader=test_dataloader_effnetb2,
                                epochs=10,
                                optimizer=optimizer,
                                loss_fn=loss_fn,
                                device=device)
  1. 检查 EffNetB2 损失曲线
python 复制代码
from helper_functions import plot_loss_curves

plot_loss_curves(effnetb2_results)
  1. 保存EffNetB2特征提取器

为了保存我们的模型,可以使用 utils.save_model() 函数。

python 复制代码
from going_modular.going_modular import utils

# Save the model
utils.save_model(model=effnetb2,
                 target_dir="models",
                 model_name="09_pretrained_effnetb2_feature_extractor_pizza_steak_sushi_20_percent.pth")
  1. 检查EffNetB2特征提取器的大小

我们希望我们的模型能够在计算能力有限的设备上运行(例如在移动设备或网络浏览器中),因此通常尺寸越小越好(只要它在准确性方面仍然表现良好) 。

python 复制代码
from pathlib import Path

# Get the model size in bytes then convert to megabytes
pretrained_effnetb2_model_size = Path("models/09_pretrained_effnetb2_feature_extractor_pizza_steak_sushi_20_percent.pth").stat().st_size // (1024*1024) # division converts bytes to megabytes (roughly) 
print(f"Pretrained EffNetB2 feature extractor model size: {pretrained_effnetb2_model_size} MB")
python 复制代码
Pretrained EffNetB2 feature extractor model size: 29 MB
  1. 收集 EffNetB2 特征提取器统计数据

关于 EffNetB2 特征提取器模型的统计数据,例如测试损失、测试准确性和模型大小,我们如何将它们全部收集在字典中,以便我们可以将它们与即将推出的 ViT 特征提取器进行比较。

可以通过计算 effnetb2.parameters() 中元素(或模式/权重)的数量来做到这一点。我们将使用 torch.numel() ("元素数量"的缩写)方法访问每个参数中的元素数量。

python 复制代码
# Count number of parameters in EffNetB2
effnetb2_total_params = sum(torch.numel(param) for param in effnetb2.parameters())
effnetb2_total_params
python 复制代码
7705221

将所有内容放入字典中,以便稍后进行比较:

python 复制代码
# Create a dictionary with EffNetB2 statistics
effnetb2_stats = {"test_loss": effnetb2_results["test_loss"][-1],
                  "test_acc": effnetb2_results["test_acc"][-1],
                  "number_of_parameters": effnetb2_total_params,
                  "model_size (MB)": pretrained_effnetb2_model_size}
effnetb2_stats
python 复制代码
{'test_loss': 0.28108683228492737,
 'test_acc': 0.9625,
 'number_of_parameters': 7705221,
 'model_size (MB)': 29}

4. 创建 ViT 特征提取器

与 EffNetB2 特征提取器大致相同的方式进行操作,只不过这次使用 torchvision.models.vit_b_16() 而不是 torchvision.models.efficientnet_b2() 。

我们首先创建一个名为 create_vit_model() 的函数,该函数与 create_effnetb2_model() 非常相似,当然返回的是 ViT 特征提取器模型和转换而不是 EffNetB2。

另一个细微的区别是 torchvision.models.vit_b_16() 的输出层称为 heads 而不是 classifier 。

python 复制代码
# Check out ViT heads layer
vit = torchvision.models.vit_b_16()
vit.heads
python 复制代码
Sequential(
  (head): Linear(in_features=768, out_features=1000, bias=True)
)

开始:

python 复制代码
def create_vit_model(num_classes:int=3, 
                     seed:int=42):
    """Creates a ViT-B/16 feature extractor model and transforms.

    Args:
        num_classes (int, optional): number of target classes. Defaults to 3.
        seed (int, optional): random seed value for output layer. Defaults to 42.

    Returns:
        model (torch.nn.Module): ViT-B/16 feature extractor model. 
        transforms (torchvision.transforms): ViT-B/16 image transforms.
    """
    # Create ViT_B_16 pretrained weights, transforms and model
    weights = torchvision.models.ViT_B_16_Weights.DEFAULT
    transforms = weights.transforms()
    model = torchvision.models.vit_b_16(weights=weights)

    # Freeze all layers in model
    for param in model.parameters():
        param.requires_grad = False

    # Change classifier head to suit our needs (this will be trainable)
    torch.manual_seed(seed)
    model.heads = nn.Sequential(nn.Linear(in_features=768, # keep this the same as original model
                                          out_features=num_classes)) # update to reflect target number of classes
    
    return model, transforms

测试:

python 复制代码
# Create ViT model and transforms
vit, vit_transforms = create_vit_model(num_classes=3,
                                       seed=42)

使用 torchinfo.summary() 获得 ViT 模型的摘要:

python 复制代码
from torchinfo import summary

# # Print ViT feature extractor model summary (uncomment for full output)
summary(vit, 
        input_size=(1, 3, 224, 224),
        col_names=["input_size", "output_size", "num_params", "trainable"],
        col_width=20,
        row_settings=["var_names"])

像 EffNetB2 特征提取器模型一样,我们的 ViT 模型的基础层被冻结,输出层根据我们的需求定制!

ViT 模型的参数比 EffNetB2 模型多得多。也许当我们稍后比较我们的模型的速度和性能时,这会发挥作用。

  1. 为 ViT 创建 DataLoader
python 复制代码
# Setup ViT DataLoaders
from going_modular.going_modular import data_setup
train_dataloader_vit, test_dataloader_vit, class_names = data_setup.create_dataloaders(train_dir=train_dir,
                                                                                       test_dir=test_dir,
                                                                                       transform=vit_transforms,
                                                                                       batch_size=32)
  1. 训练ViT特征提取器

使用 engine.train() 函数和 torch.optim.Adam() 以及学习率为 1e-3 作为优化器和 torch.nn.CrossEntropyLoss() 作为我们的损失函数,使用 set_seeds() 函数来尝试使我们的结果尽可能具有可重复性。

python 复制代码
from going_modular.going_modular import engine

# Setup optimizer
optimizer = torch.optim.Adam(params=vit.parameters(),
                             lr=1e-3)
# Setup loss function
loss_fn = torch.nn.CrossEntropyLoss()

# Train ViT model with seeds set for reproducibility
set_seeds()
vit_results = engine.train(model=vit,
                           train_dataloader=train_dataloader_vit,
                           test_dataloader=test_dataloader_vit,
                           epochs=10,
                           optimizer=optimizer,
                           loss_fn=loss_fn,
                           device=device)
  1. 检查ViT损失曲线
python 复制代码
from helper_functions import plot_loss_curves

plot_loss_curves(vit_results)
  1. 保存ViT特征提取器
python 复制代码
# Save the model
from going_modular.going_modular import utils

utils.save_model(model=vit,
                 target_dir="models",
                 model_name="09_pretrained_vit_feature_extractor_pizza_steak_sushi_20_percent.pth")
  1. 检查ViT特征提取器的大小
python 复制代码
from pathlib import Path

# Get the model size in bytes then convert to megabytes
pretrained_vit_model_size = Path("models/09_pretrained_vit_feature_extractor_pizza_steak_sushi_20_percent.pth").stat().st_size // (1024*1024) # division converts bytes to megabytes (roughly) 
print(f"Pretrained ViT feature extractor model size: {pretrained_vit_model_size} MB")
python 复制代码
Pretrained ViT feature extractor model size: 327 MB
  1. 收集 ViT 特征提取器统计数据

计算它的参数总数:

python 复制代码
# Count number of parameters in ViT
vit_total_params = sum(torch.numel(param) for param in vit.parameters())
vit_total_params
python 复制代码
85800963

注意:参数(或权重/模式)数量越多,通常意味着模型具有更高的学习能力,但它是否真正使用了这种额外的能力则是另一回事。有鉴于此,我们的 EffNetB2 模型有 7,705,221 个参数,而我们的 ViT 模型有 85,800,963 个参数(多出 11.1 倍),因此我们可以假设,如果提供更多数据(更多学习机会),我们的 ViT 模型具有更强的学习能力。然而,更大的学习能力通常伴随着更大的模型文件大小和更长的执行推理时间。

创建一个包含 ViT 模型的一些重要特征的字典:

python 复制代码
# Create ViT statistics dictionary
vit_stats = {"test_loss": vit_results["test_loss"][-1],
             "test_acc": vit_results["test_acc"][-1],
             "number_of_parameters": vit_total_params,
             "model_size (MB)": pretrained_vit_model_size}

vit_stats
python 复制代码
{'test_loss': 0.06443451717495918,
 'test_acc': 0.984659090909091,
 'number_of_parameters': 85800963,
 'model_size (MB)': 327}

5. 使用训练好模型进行预测并计时

使用 Python 的 pathlib.Path("target_dir").glob("/.jpg")) 来查找目标目录中扩展名为 .jpg 的所有文件路径(我们的所有测试图像):

python 复制代码
from pathlib import Path

# Get all test data paths
print(f"[INFO] Finding all filepaths ending with '.jpg' in directory: {test_dir}")
test_data_paths = list(Path(test_dir).glob("*/*.jpg"))
test_data_paths[:5]
  1. 创建一个函数来对测试数据集进行预测

已经有了测试图像路径的列表,开始处理 pred_and_store() 函数:

(1)创建一个函数,该函数采用路径列表、经过训练的 PyTorch 模型、一系列转换(以准备图像)、目标类名称列表和目标设备。

(2)创建一个空列表来存储预测字典(我们希望函数返回一个字典列表,每个预测一个)。

(3)循环遍历目标输入路径(步骤 4-14 将在循环内发生)。

(4)为循环中的每次迭代创建一个空字典以存储每个样本的预测值。

(6)获取样本路径和真实类名称(我们可以通过从路径推断类来完成此操作)。

(7)使用 Python 的 timeit.default_timer() 启动预测计时器。

(8)使用 PIL.Image.open(path) 打开图像。

(9)转换图像,使其能够与目标模型一起使用,并添加批量尺寸并将图像发送到目标设备。

(10)通过将模型发送到目标设备并打开 eval() 模式来准备模型进行推理。

(11)打开 torch.inference_mode() 并将目标变换图像传递给模型,并使用 torch.softmax() 计算预测概率,使用 torch.argmax() 计算目标标签。

(12)将预测概率和预测类别添加到步骤 4 中创建的预测字典中。还要确保预测概率位于 CPU 上,以便可以与 NumPy 和 pandas 等非 GPU 库一起使用,以供以后检查。

(13)结束步骤 6 中启动的预测计时器,并将时间添加到步骤 4 中创建的预测字典中。

(14)查看预测类别是否与步骤 5 中的真实类别匹配,并将结果添加到步骤 4 中创建的预测字典中。

(15)将更新后的预测字典附加到步骤 2 中创建的空预测列表。

(16)返回预测字典列表。

python 复制代码
import pathlib
import torch

from PIL import Image
from timeit import default_timer as timer 
from tqdm.auto import tqdm
from typing import List, Dict

# 1. Create a function to return a list of dictionaries with sample, truth label, prediction, prediction probability and prediction time
def pred_and_store(paths: List[pathlib.Path], 
                   model: torch.nn.Module,
                   transform: torchvision.transforms, 
                   class_names: List[str], 
                   device: str = "cuda" if torch.cuda.is_available() else "cpu") -> List[Dict]:
    
    # 2. Create an empty list to store prediction dictionaires
    pred_list = []
    
    # 3. Loop through target paths
    for path in tqdm(paths):
        
        # 4. Create empty dictionary to store prediction information for each sample
        pred_dict = {}

        # 5. Get the sample path and ground truth class name
        pred_dict["image_path"] = path
        class_name = path.parent.stem
        pred_dict["class_name"] = class_name
        
        # 6. Start the prediction timer
        start_time = timer()
        
        # 7. Open image path
        img = Image.open(path)
        
        # 8. Transform the image, add batch dimension and put image on target device
        transformed_image = transform(img).unsqueeze(0).to(device) 
        
        # 9. Prepare model for inference by sending it to target device and turning on eval() mode
        model.to(device)
        model.eval()
        
        # 10. Get prediction probability, predicition label and prediction class
        with torch.inference_mode():
            pred_logit = model(transformed_image) # perform inference on target sample 
            pred_prob = torch.softmax(pred_logit, dim=1) # turn logits into prediction probabilities
            pred_label = torch.argmax(pred_prob, dim=1) # turn prediction probabilities into prediction label
            pred_class = class_names[pred_label.cpu()] # hardcode prediction class to be on CPU

            # 11. Make sure things in the dictionary are on CPU (required for inspecting predictions later on) 
            pred_dict["pred_prob"] = round(pred_prob.unsqueeze(0).max().cpu().item(), 4)
            pred_dict["pred_class"] = pred_class
            
            # 12. End the timer and calculate time per pred
            end_time = timer()
            pred_dict["time_for_pred"] = round(end_time-start_time, 4)

        # 13. Does the pred match the true label?
        pred_dict["correct"] = class_name == pred_class

        # 14. Add the dictionary to the list of preds
        pred_list.append(pred_dict)
    
    # 15. Return list of prediction dictionaries
    return pred_list
  1. 使用 EffNetB2 进行预测并计时

测试我们的 pred_and_store() 函数:

  • device- 我们将对 device 参数进行硬编码以使用 "cpu" ,因为当我们部署模型时,我们并不总是能够访问 "cuda" (GPU ) 设备。
  • transforms- 我们还要确保将 transform 参数设置为 effnetb2_transforms ,以确保图像以与 effnetb2 模型相同的方式打开和变换受过培训。
python 复制代码
# Make predictions across test dataset with EffNetB2
effnetb2_test_pred_dicts = pred_and_store(paths=test_data_paths,
                                          model=effnetb2,
                                          transform=effnetb2_transforms,
                                          class_names=class_names,
                                          device="cpu") # make predictions on CPU 

检查预测数据:

python 复制代码
# Inspect the first 2 prediction dictionaries
effnetb2_test_pred_dicts[:2]

将字典列表转换为 pandas DataFrame:

python 复制代码
# Turn the test_pred_dicts into a DataFrame
import pandas as pd
effnetb2_test_pred_df = pd.DataFrame(effnetb2_test_pred_dicts)
effnetb2_test_pred_df.head()

找出我们的 EffNetB2 模型有多少预测出错了:

python 复制代码
# Check number of correct predictions
effnetb2_test_pred_df.correct.value_counts()

总共 150 个预测中有 6 个错误。

平均预测时间:

python 复制代码
# Find the average time per prediction 
effnetb2_average_time_per_pred = round(effnetb2_test_pred_df.time_for_pred.mean(), 4)
print(f"EffNetB2 average time per prediction: {effnetb2_average_time_per_pred} seconds")
python 复制代码
EffNetB2 average time per prediction: 0.1125 seconds

将 EffNetB2 每次预测的平均时间添加到 effnetb2_stats 字典中:

python 复制代码
# Add EffNetB2 average prediction time to stats dictionary 
effnetb2_stats["time_per_pred_cpu"] = effnetb2_average_time_per_pred
effnetb2_stats
python 复制代码
{'test_loss': 0.28108683228492737,
 'test_acc': 0.9625,
 'number_of_parameters': 7705221,
 'model_size (MB)': 29,
 'time_per_pred_cpu': 0.1125}
  1. 使用 ViT 进行预测并计时

和上面相同的操作:

python 复制代码
# Make list of prediction dictionaries with ViT feature extractor model on test images
vit_test_pred_dicts = pred_and_store(paths=test_data_paths,
                                     model=vit,
                                     transform=vit_transforms,
                                     class_names=class_names,
                                     device="cpu")
python 复制代码
# Check the first couple of ViT predictions on the test dataset
vit_test_pred_dicts[:2]
python 复制代码
[{'image_path': PosixPath('data/pizza_steak_sushi_20_percent/test/pizza/2997525.jpg'),
  'class_name': 'pizza',
  'pred_prob': 0.9986,
  'pred_class': 'pizza',
  'time_for_pred': 0.7175,
  'correct': True},
 {'image_path': PosixPath('data/pizza_steak_sushi_20_percent/test/pizza/930553.jpg'),
  'class_name': 'pizza',
  'pred_prob': 0.9982,
  'pred_class': 'pizza',
  'time_for_pred': 0.5342,
  'correct': True}]
python 复制代码
# Turn vit_test_pred_dicts into a DataFrame
import pandas as pd
vit_test_pred_df = pd.DataFrame(vit_test_pred_dicts)
vit_test_pred_df.head()
python 复制代码
# Count the number of correct predictions
vit_test_pred_df.correct.value_counts()
python 复制代码
# Calculate average time per prediction for ViT model
vit_average_time_per_pred = round(vit_test_pred_df.time_for_pred.mean(), 4)
print(f"ViT average time per prediction: {vit_average_time_per_pred} seconds")
python 复制代码
ViT average time per prediction: 0.5078 seconds
python 复制代码
# Add average prediction time for ViT model on CPU
vit_stats["time_per_pred_cpu"] = vit_average_time_per_pred
vit_stats
python 复制代码
{'test_loss': 0.06443451717495918,
 'test_acc': 0.984659090909091,
 'number_of_parameters': 85800963,
 'model_size (MB)': 327,
 'time_per_pred_cpu': 0.5078}

6. 比较模型结果、预测时间和大小

将添加一列来查看模型名称,并将测试精度转换为整数百分比而不是小数:

python 复制代码
# Turn stat dictionaries into DataFrame
df = pd.DataFrame([effnetb2_stats, vit_stats])

# Add column for model names
df["model"] = ["EffNetB2", "ViT"]

# Convert accuracy to percentages
df["test_acc"] = round(df["test_acc"] * 100, 2)

df

将 ViT 模型统计数据除以 EffNetB2 模型统计数据,以找出模型之间的不同比率:

python 复制代码
# Compare ViT to EffNetB2 across different characteristics
pd.DataFrame(data=(df.set_index("model").loc["ViT"] / df.set_index("model").loc["EffNetB2"]), # divide ViT statistics by EffNetB2 statistics
             columns=["ViT to EffNetB2 ratios"]).T

ViT 模型在性能指标(测试损失,越低越好,测试准确度,越高越好)方面似乎优于 EffNetB2 模型,但代价是:11x+ 参数数量。模型尺寸的 11 倍以上。每张图像的预测时间增加 4.5 倍。

  1. 可视化速度与性能的权衡

ViT 模型在测试损失和测试准确性等性能指标方面优于我们的 EffNetB2 模型,但是 EffNetB2 模型执行预测速度更快,并且模型大小小得多。

可以通过使用 matplotlib 创建绘图来做到可视化:

(1)从比较 DataFrame 创建散点图以比较 EffNetB2 和 ViT time_per_pred_cpu 和 test_acc 值。

(2)添加各自数据的标题和标签,并自定义字体大小以实现美观。

(3)使用适当的标签(模型名称)对步骤 1 中散点图上的样本进行注释。

(4)根据模型尺寸创建图例 ( model_size (MB) )。

python 复制代码
# 1. Create a plot from model comparison DataFrame
fig, ax = plt.subplots(figsize=(12, 8))
scatter = ax.scatter(data=df, 
                     x="time_per_pred_cpu", 
                     y="test_acc", 
                     c=["blue", "orange"], # what colours to use?
                     s="model_size (MB)") # size the dots by the model sizes

# 2. Add titles, labels and customize fontsize for aesthetics
ax.set_title("FoodVision Mini Inference Speed vs Performance", fontsize=18)
ax.set_xlabel("Prediction time per image (seconds)", fontsize=14)
ax.set_ylabel("Test accuracy (%)", fontsize=14)
ax.tick_params(axis='both', labelsize=12)
ax.grid(True)

# 3. Annotate with model names
for index, row in df.iterrows():
    ax.annotate(text=row["model"], # note: depending on your version of Matplotlib, you may need to use "s=..." or "text=...", see: https://github.com/faustomorales/keras-ocr/issues/183#issuecomment-977733270 
                xy=(row["time_per_pred_cpu"]+0.0006, row["test_acc"]+0.03),
                size=12)

# 4. Create a legend based on model sizes
handles, labels = scatter.legend_elements(prop="sizes", alpha=0.5)
model_size_legend = ax.legend(handles, 
                              labels, 
                              loc="lower right", 
                              title="Model size (MB)",
                              fontsize=12)

# Save the figure
plt.savefig("images/09-foodvision-mini-inference-speed-vs-performance.jpg")

# Show the figure
plt.show()

(图片和前面的数据不符合是因为换了一个服务器的GUP,都快了不少,很显然,好的服务器对越大的模型越有利,前面的数据就不替换了,本图具体的数据对比参照下面数据)

拥有更大、性能更好的深度模型(例如我们的 ViT 模型)时,通常需要更长的时间来执行推理(更高的延迟)。

强调速度,因此我们将坚持部署 EffNetB2,因为它速度更快且占用空间小得多。


7. 通过创建 Gradio 演示让 FoodVision Mini 呈现

决定部署 EffNetB2 模型,通过使用Gradio 实现。

开始部署,通用别名 gr 导入 Gradio,如果它不存在,我们将安装它:

python 复制代码
# Import/install Gradio 
try:
    import gradio as gr
except: 
    !pip -q install gradio
    import gradio as gr
    
print(f"Gradio version: {gr.__version__}")
python 复制代码
Gradio version: 4.19.2
  1. Gradio overview

inputs -> ML model -> outputs

对于 FoodVision Mini,我们的输入是食物图像,我们的 ML 模型是 EffNetB2,我们的输出是食物类别(披萨、牛排或寿司)。

images of food -> EffNetB2 -> outputs

Gradio 通过创建从输入到输出的接口 ( gradio.Interface() ) 来模拟此案例:gradio.Interface(fn, inputs, outputs) fn 是一个 Python 函数,用于将 inputs 映射到 outputs 。

Gradio 提供了一个非常有用的 Interface 类,可以轻松创建输入 -> 模型/函数 -> 输出工作流程,其中输入和输出几乎可以是您想要的任何内容。例如,可以输入推文(文本)来查看它们是否与机器学习有关,或者输入文本提示来生成图像。

注意:Gradio 有大量可能的 inputs 和 outputs 选项,称为"组件",从图像到文本到数字到音频到视频等等。

  1. 创建一个函数来映射我们的输入和输出

创建一个函数,它将图像作为输入,对其进行预处理(转换),使用 EffNetB2 进行预测,然后返回预测(简称 pred 或 pred label)以及预测概率(pred prob)。
input: image -> transform -> predict with EffNetB2 -> output: pred, pred prob, time taken

这个函数就是Gradio Interface 的 fn 参数。

确保我们的 EffNetB2 模型位于 CPU 上(因为坚持仅使用 CPU 进行预测,但是如果可以访问 GPU,则可以更改此设置)。

python 复制代码
# Put EffNetB2 on CPU
effnetb2.to("cpu") 

# Check the device
next(iter(effnetb2.parameters())).device

创建一个名为 predict() 的函数来复制上面的工作流程:

python 复制代码
from typing import Tuple, Dict

def predict(img) -> Tuple[Dict, float]:
    """Transforms and performs a prediction on img and returns prediction and time taken.
    """
    # Start the timer
    start_time = timer()
    
    # Transform the target image and add a batch dimension
    img = effnetb2_transforms(img).unsqueeze(0)
    
    # Put model into evaluation mode and turn on inference mode
    effnetb2.eval()
    with torch.inference_mode():
        # Pass the transformed image through the model and turn the prediction logits into prediction probabilities
        pred_probs = torch.softmax(effnetb2(img), dim=1)
    
    # Create a prediction label and prediction probability dictionary for each prediction class (this is the required format for Gradio's output parameter)
    pred_labels_and_probs = {class_names[i]: float(pred_probs[0][i]) for i in range(len(class_names))}
    
    # Calculate the prediction time
    pred_time = round(timer() - start_time, 5)
    
    # Return the prediction dictionary and prediction time 
    return pred_labels_and_probs, pred_time

测试一下:

python 复制代码
import random
from PIL import Image

# Get a list of all test image filepaths
test_data_paths = list(Path(test_dir).glob("*/*.jpg"))

# Randomly select a test image path
random_image_path = random.sample(test_data_paths, k=1)[0]

# Open the target image
image = Image.open(random_image_path)
print(f"[INFO] Predicting on image at path: {random_image_path}\n")

# Predict on the target image and print out the outputs
pred_dict, pred_time = predict(img=image)
print(f"Prediction label and probability dictionary: \n{pred_dict}")
print(f"Prediction time: {pred_time} seconds")
python 复制代码
[INFO] Predicting on image at path: data/pizza_steak_sushi_20_percent/test/pizza/148765.jpg

Prediction label and probability dictionary: 
{'pizza': 0.9311544895172119, 'steak': 0.011404184624552727, 'sushi': 0.05744136497378349}
Prediction time: 0.08999 seconds
  1. 创建示例图像列表

predict() 函数能够从输入 -> 转换 -> ML 模型 -> 输出

Gradio 的 Interface 类采用 examples 列表作为可选参数 ( gradio.Interface(examples=List[Any]) )examples 参数的格式是列表,所有需要创建一个包含测试图像的随机文件路径的列表。

3个例子:

python 复制代码
# Create a list of example inputs to our Gradio demo
example_list = [[str(filepath)] for filepath in random.sample(test_data_paths, k=3)]
example_list

Gradio 演示将展示这些作为演示的示例输入,以便人们可以尝试并查看它的作用,而无需上传任何自己的数据。

  1. 构建Gradio interface

工作流程:input: image -> transform -> predict with EffNetB2 -> output: pred, pred prob, time taken

可以使用以下参数来处理 gradio.Interface() 类:

  • fn - 将 inputs 映射到 outputs 的 Python 函数,在我们的例子中,我们将使用 predict() 函数。
  • inputs - interface的输入,例如使用 gradio.Image() 或 "image" 的图像。
  • outputs - 一旦 inputs 经过 fn 后我们interface的输出,例如使用 gradio.Label() 的标签(对于我们模型的预测标签)或使用 gradio.Number() 的数字(对于我们模型的预测时间)
  • examples - 用于演示的示例列表。
  • title - 演示的字符串标题
  • description - 演示的字符串描述
  • article - 演示底部的参考注释

创建 gr.Interface() 的演示实例后,我们可以使用 gradio.Interface().launch() 或 demo.launch() 命令将其变为现实。

python 复制代码
import gradio as gr

# Create title, description and article strings
title = "FoodVision Mini 🍕🥩🍣"
description = "An EfficientNetB2 feature extractor computer vision model to classify images of food as pizza, steak or sushi."
article = "Created at [09. PyTorch Model Deployment](https://www.learnpytorch.io/09_pytorch_model_deployment/)."

# Create the Gradio demo
demo = gr.Interface(fn=predict, # mapping function from input to output
                    inputs=gr.Image(type="pil"), # what are the inputs?
                    outputs=[gr.Label(num_top_classes=3, label="Predictions"), # what are the outputs?
                             gr.Number(label="Prediction time (s)")], # our fn has two outputs, therefore we have two outputs
                    examples=example_list, 
                    title=title,
                    description=description,
                    article=article)

# Launch the demo!
demo.launch(debug=False, # print errors locally?
            share=True) # generate a publically shareable URL?
python 复制代码
Running on local URL:  http://127.0.0.1:7860
Running on public URL: https://dffde555b7ae1e09b8.gradio.live

This share link expires in 72 hours. For free permanent hosting and GPU upgrades, run `gradio deploy` from Terminal to deploy to Spaces (https://huggingface.co/spaces)

FoodVision Mini Gradio 演示在 Google Colab 和浏览器中运行(从 Google Colab 运行时的链接仅持续 72 小时)。可以在 Hugging Face Spaces 上观看永久现场演示。

如果您在 launch() 方法中设置参数 share=True ,Gradio 还会为您提供一个可共享的链接,例如 https://123XYZ.gradio.app (此链接仅作为示例,可能已过期) ),有效期为 72 小时。


8. 将Gradio demo变成可部署的应用程序

与别人分享,可以使用提供的 Gradio 链接,但是共享链接只能持续 72 小时,为了使 FoodVision Mini 演示更加持久,我们可以将其打包到应用程序中并将其上传到 Hugging Face Spaces。

  1. 什么是Hugging Face Spaces

Hugging Face Spaces 是一种资源,可托管和共享机器学习应用程序。构建demo是展示和测试所做工作的最佳方式之一。

  1. 部署的 Gradio 应用程序结构

要上传我们的演示 Gradio 应用程序,我们需要将与其相关的所有内容放入一个目录中。

例如,我们的演示可能位于路径 demos/foodvision_mini/ 中,文件结构如下:

python 复制代码
demos/
└── foodvision_mini/
    ├── 09_pretrained_effnetb2_feature_extractor_pizza_steak_sushi_20_percent.pth
    ├── app.py
    ├── examples/
    │   ├── example_1.jpg
    │   ├── example_2.jpg
    │   └── example_3.jpg
    ├── model.py
    └── requirements.txt

文件说明:

  • 09_pretrained_effnetb2_feature_extractor_pizza_steak_sushi_20_percent.pth 是我们经过训练的 PyTorch 模型文件。
  • app.py 包含我们的 Gradio 应用程序(类似于启动该应用程序的代码)。
    • 注意: app.py 是 Hugging Face Spaces 使用的默认文件名,如果在那里部署应用程序,Spaces 将默认查找名为 app.py 的文件来运行。这可以在设置中更改。
  • examples/ 包含与我们的 Gradio 应用程序一起使用的示例图像。
  • model.py 包含模型定义以及与模型关联的任何转换。
  • requirements.txt 包含运行我们的应用程序的依赖项,例如 torch 、 torchvision 和 gradio 。
  1. 创建 demos 文件夹来存储我们的 FoodVision Mini 应用程序文件

首先创建一个 demos/ 目录来存储所有 FoodVision Mini 应用程序文件:可以使用Python的 pathlib.Path("path_to_dir") 来建立目录路径,并使用 pathlib.Path("path_to_dir").mkdir() 来创建它。

python 复制代码
import shutil
from pathlib import Path

# Create FoodVision mini demo path
foodvision_mini_demo_path = Path("demos/foodvision_mini/")

# Remove files that might already exist there and create new directory
if foodvision_mini_demo_path.exists():
    shutil.rmtree(foodvision_mini_demo_path)
    foodvision_mini_demo_path.mkdir(parents=True, # make the parent folders?
                                    exist_ok=True) # create it even if it already exists?
else:
    # If the file doesn't exist, create it anyway
    foodvision_mini_demo_path.mkdir(parents=True, 
                                    exist_ok=True)
    
# Check what's in the folder
!ls demos/foodvision_mini/
  1. 创建示例图像文件夹以与 FoodVision Mini 演示一起使用

测试数据集中的三个示例图像应该足够了,需要如下操作:

  • 在 demos/foodvision_mini 目录中创建 examples/ 目录。
  • 将测试数据集中的三个随机图像复制到 demos/foodvision_mini/examples/ 目录。
python 复制代码
import shutil
from pathlib import Path

# 1. Create an examples directory
foodvision_mini_examples_path = foodvision_mini_demo_path / "examples"
foodvision_mini_examples_path.mkdir(parents=True, exist_ok=True)

# 2. Collect three random test dataset image paths
foodvision_mini_examples = [Path('data/pizza_steak_sushi_20_percent/test/sushi/592799.jpg'),
                            Path('data/pizza_steak_sushi_20_percent/test/steak/3622237.jpg'),
                            Path('data/pizza_steak_sushi_20_percent/test/pizza/2582289.jpg')]

# 3. Copy the three random images to the examples directory
for example in foodvision_mini_examples:
    destination = foodvision_mini_examples_path / example.name
    print(f"[INFO] Copying {example} to {destination}")
    shutil.copy2(src=example, dst=destination)

现在为了验证我们的示例是否存在,让我们用 os.listdir() 列出 demos/foodvision_mini/examples/ 目录的内容,然后将文件路径格式化为列表(因为它是 Gradio 的 gradio.Interface() 参数)。

python 复制代码
import os

# Get example filepaths in a list of lists
example_list = [["examples/" + example] for example in os.listdir(foodvision_mini_examples_path)]
example_list
python 复制代码
[['examples/2582289.jpg'], ['examples/3622237.jpg'], ['examples/592799.jpg']]
  1. 将训练好的 EffNetB2 模型移至 FoodVision Mini 演示目录

我们之前将 FoodVision Mini EffNetB2 特征提取器模型保存在 models/09_pretrained_effnetb2_feature_extractor_pizza_steak_sushi_20_percent.pth 下。

将模型移至 demos/foodvision_mini 目录,而不是加倍保存的模型文件:可以使用Python的 shutil.move() 方法来实现这一点,并传入 src (目标文件的源路径)和 dst (目标文件的目标路径)被移动到)参数。

python 复制代码
import shutil

# Create a source path for our target model
effnetb2_foodvision_mini_model_path = "models/09_pretrained_effnetb2_feature_extractor_pizza_steak_sushi_20_percent.pth"

# Create a destination path for our target model 
effnetb2_foodvision_mini_model_destination = foodvision_mini_demo_path / effnetb2_foodvision_mini_model_path.split("/")[1]

# Try to move the file
try:
    print(f"[INFO] Attempting to move {effnetb2_foodvision_mini_model_path} to {effnetb2_foodvision_mini_model_destination}")
    
    # Move the model
    shutil.move(src=effnetb2_foodvision_mini_model_path, 
                dst=effnetb2_foodvision_mini_model_destination)
    
    print(f"[INFO] Model move complete.")

# If the model has already been moved, check if it exists
except:
    print(f"[INFO] No model found at {effnetb2_foodvision_mini_model_path}, perhaps its already been moved?")
    print(f"[INFO] Model exists at {effnetb2_foodvision_mini_model_destination}: {effnetb2_foodvision_mini_model_destination.exists()}")
  1. 将我们的 EffNetB2 模型转换为 Python 脚本 ( model.py )

当前模型的 state_dict 保存到 demos/foodvision_mini/09_pretrained_effnetb2_feature_extractor_pizza_steak_sushi_20_percent.pth 。

要加载它,我们可以使用 model.load_state_dict() 和 torch.load() 。

创建一个名为 model.py 的脚本,其中包 3. 创建用于制作 EffNetB2 特征提取器的函数中创建的 create_effnetb2_model() 函数。

这样我们就可以在另一个脚本中导入该函数(参阅下面的 app.py ),然后使用它来创建我们的 EffNetB2 model 实例并获取其适当的转换。

可以使用 %%writefile path/to/file 魔术命令将代码单元转换为文件:

python 复制代码
%%writefile demos/foodvision_mini/model.py
import torch
import torchvision

from torch import nn


def create_effnetb2_model(num_classes:int=3, 
                          seed:int=42):
    """Creates an EfficientNetB2 feature extractor model and transforms.

    Args:
        num_classes (int, optional): number of classes in the classifier head. 
            Defaults to 3.
        seed (int, optional): random seed value. Defaults to 42.

    Returns:
        model (torch.nn.Module): EffNetB2 feature extractor model. 
        transforms (torchvision.transforms): EffNetB2 image transforms.
    """
    # Create EffNetB2 pretrained weights, transforms and model
    weights = torchvision.models.EfficientNet_B2_Weights.DEFAULT
    transforms = weights.transforms()
    model = torchvision.models.efficientnet_b2(weights=weights)

    # Freeze all layers in base model
    for param in model.parameters():
        param.requires_grad = False

    # Change classifier head with random seed for reproducibility
    torch.manual_seed(seed)
    model.classifier = nn.Sequential(
        nn.Dropout(p=0.3, inplace=True),
        nn.Linear(in_features=1408, out_features=num_classes),
    )
    
    return model, transforms
  1. 将 FoodVision Mini Gradio 应用程序转换为 Python 脚本 ( app.py )

默认情况下,当创建 HuggingFace Space 时,它​​会查找名为 app.py 的文件来运行和托管(可以在设置中更改此设置)。

app.py 脚本将所有部分放在一起来创建我们的 Gradio 演示,有四个主要部分:

  • 【导入和类名设置】 - 在这里,我们将为演示导入各种依赖项,包括 model.py 中的 create_effnetb2_model() 函数,并为 FoodVision Mini 应用程序设置不同的类名。
  • 【模型和转换准备】 - 在这里,我们将创建一个 EffNetB2 模型实例以及与之相伴的转换,然后加载保存的模型权重/ state_dict 。当我们加载模型时,我们还将在 torch.load() 中设置 map_location=torch.device("cpu") ,这样我们的模型就会加载到 CPU 上,无论它训练的设备如何(我们这样做是因为我们不一定当我们部署时有一个 GPU,如果我们的模型是在 GPU 上训练的,但我们在没有明确说明的情况下尝试将其部署到 CPU,我们会收到错误)。
  • 【预测函数】 - Gradio 的 gradio.Interface() 采用 fn 参数将输入映射到输出,我们的 predict() 函数将与我们在第 7. 节中定义的函数相同:创建一个函数来映射我们的输入和输出,它将接收图像,然后使用加载的变换对其进行预处理,然后再使用加载的模型对其进行预测。
    • 【注意】:我们必须通过 examples 参数动态创建示例列表。我们可以通过使用 [["examples/" + example] for example in os.listdir("examples")] 创建 examples/ 目录中的文件列表来实现此目的。
  • 【Gradio 应用程序】 - 这是我们演示的主要逻辑所在的位置,我们将创建一个名为 demo 的 gradio.Interface() 实例来组合我们的输入, predict() 函数和输出。我们将通过调用 demo.launch() 启动我们的 FoodVision Mini 演示来完成脚本!
python 复制代码
%%writefile demos/foodvision_mini/app.py
### 1. Imports and class names setup ### 
import gradio as gr
import os
import torch

from model import create_effnetb2_model
from timeit import default_timer as timer
from typing import Tuple, Dict

# Setup class names
class_names = ["pizza", "steak", "sushi"]

### 2. Model and transforms preparation ###

# Create EffNetB2 model
effnetb2, effnetb2_transforms = create_effnetb2_model(
    num_classes=3, # len(class_names) would also work
)

# Load saved weights
effnetb2.load_state_dict(
    torch.load(
        f="09_pretrained_effnetb2_feature_extractor_pizza_steak_sushi_20_percent.pth",
        map_location=torch.device("cpu"),  # load to CPU
    )
)

### 3. Predict function ###

# Create predict function
def predict(img) -> Tuple[Dict, float]:
    """Transforms and performs a prediction on img and returns prediction and time taken.
    """
    # Start the timer
    start_time = timer()
    
    # Transform the target image and add a batch dimension
    img = effnetb2_transforms(img).unsqueeze(0)
    
    # Put model into evaluation mode and turn on inference mode
    effnetb2.eval()
    with torch.inference_mode():
        # Pass the transformed image through the model and turn the prediction logits into prediction probabilities
        pred_probs = torch.softmax(effnetb2(img), dim=1)
    
    # Create a prediction label and prediction probability dictionary for each prediction class (this is the required format for Gradio's output parameter)
    pred_labels_and_probs = {class_names[i]: float(pred_probs[0][i]) for i in range(len(class_names))}
    
    # Calculate the prediction time
    pred_time = round(timer() - start_time, 5)
    
    # Return the prediction dictionary and prediction time 
    return pred_labels_and_probs, pred_time

### 4. Gradio app ###

# Create title, description and article strings
title = "FoodVision Mini 🍕🥩🍣"
description = "An EfficientNetB2 feature extractor computer vision model to classify images of food as pizza, steak or sushi."
article = "Created at [09. PyTorch Model Deployment](https://www.learnpytorch.io/09_pytorch_model_deployment/)."

# Create examples list from "examples/" directory
example_list = [["examples/" + example] for example in os.listdir("examples")]

# Create the Gradio demo
demo = gr.Interface(fn=predict, # mapping function from input to output
                    inputs=gr.Image(type="pil"), # what are the inputs?
                    outputs=[gr.Label(num_top_classes=3, label="Predictions"), # what are the outputs?
                             gr.Number(label="Prediction time (s)")], # our fn has two outputs, therefore we have two outputs
                    # Create examples list from "examples/" directory
                    examples=example_list, 
                    title=title,
                    description=description,
                    article=article)

# Launch the demo!
demo.launch()
  1. 创建 FoodVision Mini 的要求文件 ( requirements.txt )

当我们将演示应用程序部署到 Hugging Face Spaces 时,它将搜索此文件并安装我们定义的依赖项,以便我们的应用程序可以运行。

python 复制代码
%%writefile demos/foodvision_mini/requirements.txt
torch==1.12.0
torchvision==0.13.0
gradio==2.0.0

9. 将 FoodVision Mini 应用程序部署到 HuggingFace Spaces

上传到 Hugging Face Space(也称为 Hugging Face 存储库,类似于 git 存储库)有两个主要选项:

在开始之前先注册一个账号:https://huggingface.co/join

  1. 下载 FoodVision Mini 应用程序文件

demos/foodvision_mini 中的demo文件

python 复制代码
!ls demos/foodvision_mini

要开始将文件上传到 Hugging Face,我们现在从 Google Colab(或运行此笔记本的任何地方)下载它们。

通过以下命令将文件压缩到单个 zip 文件夹中:zip -r ../foodvision_mini.zip * -x "*.pyc" "*.ipynb" "*__pycache__*" "*ipynb_checkpoints*"

  • zip 代表"zip",如"请将以下目录中的文件压缩在一起"。
  • -r 代表"递归",如"遍历目标目录中的所有文件"。
  • .../foodvision_mini.zip 是我们希望将文件压缩到的目标目录。
  • * 代表"当前目录下的所有文件"。
  • -x 代表"排除这些文件"。

可以使用 google.colab.files.download("demos/foodvision_mini.zip") 从 Google Colab 下载 zip 文件(我们会将其放在 try 和 except 块中,以防万一我们没有运行Google Colab 内的代码,如果是这样,我们将打印一条消息,说明手动下载文件)。

python 复制代码
# Change into and then zip the foodvision_mini folder but exclude certain files
!cd demos/foodvision_mini && zip -r ../foodvision_mini.zip * -x "*.pyc" "*.ipynb" "*__pycache__*" "*ipynb_checkpoints*"

# Download the zipped FoodVision Mini app (if running in Google Colab)
try:
    from google.colab import files
    files.download("demos/foodvision_mini.zip")
except:
    print("Not running in Google Colab, can't use google.colab.files.download(), please manually download.")

kaggle 可以直接下载即可。

  1. 在本地运行 FoodVision Mini 演示

下载好 foodvision_mini.zip 文件后,则可以通过以下方式在本地测试它:

(1)解压缩文件。

(2)打开终端或命令行提示符。

(3)更改为 foodvision_mini 目录 ( cd foodvision_mini )。

(4)创建环境 ( python3 -m venv env )。

(5)激活环境 ( source env/bin/activate )。

(6)安装要求( pip install -r requirements.txt ," -r "用于递归)。

注意:此步骤可能需要 5-10 分钟,具体取决于您的互联网连接。如果您遇到错误,您可能需要先升级 pip : pip install --upgrade pip 。

Run the app (python3 app.py).

(7)运行应用程序 ( python3 app.py )。

然后就可以通过http://127.0.0.1:7860/ 这样的 URL 本地运行。

  1. 上传Hugging Face

(1)注册一个 Hugging Face 帐户。

(2)转到您的个人资料,然后单击"New Space",Start a new Hugging Face Space

注意:Hugging Face 中的空间也称为"code repository"(存储代码/文件的地方)或简称为"repo"。

(3)给空间命名.

(4)选择一个许可证

(5)选择Gradio作为Space SDK(软件开发工具包)。

注意:您可以使用其他选项,例如 Streamlit,但由于我们的应用程序是使用 Gradio 构建的。

(6)选择您的空间是公共空间还是私人空间(选择公共空间,希望其他人可以使用我的空间)。

(7)单击"创建空间"。

(8)通过在终端或命令提示符中运行以下命令来在本地克隆存储库: git clone https://huggingface.co/spaces/[YOUR_USERNAME]/[YOUR_SPACE_NAME]

注意:您还可以通过在"文件和版本"选项卡下上传文件来添加文件。

(9)将下载的 foodvision_mini 文件夹的内容复制/移动到克隆的存储库文件夹中。

(10)要上传和跟踪较大的文件(例如超过 10MB 的文件或在我们的例子中是我们的 PyTorch 模型文件),您需要安装 Git LFS(代表"git 大文件存储")。

(11)安装 Git LFS 后,可以通过运行 git lfs install 安装它。

(12)在 foodvision_mini 目录中,使用 Git LFS 和 git lfs track "*.file_extension" 跟踪超过 10MB 的文件。

  • 使用 git lfs track "09_pretrained_effnetb2_feature_extractor_pizza_steak_sushi_20_percent.pth" 跟踪 EffNetB2 PyTorch 模型文件。

(13)跟踪 .gitattributes (从 HuggingFace 克隆时自动创建,此文件将有助于确保使用 Git LFS 跟踪我们的较大文件)。您可以在 FoodVision Mini Hugging Face Space 上查看示例 .gitattributes 文件。

  • git add .gitattributes

(14)添加其余的 foodvision_mini 应用程序文件并使用以下命令提交它们:

  • git add *
  • git commit -m "first commit"

(15)将文件推送(上传)到 Hugging Face:

  • git push

(16)等待 3-5 分钟,以便构建发生。

一切正常,应该会看到我们的 FoodVision Mini Gradio 演示的实时运行示例,如下所示:https://huggingface.co/spaces/GaoLang/foodvision_mini_test

好像没有成功,这里需要输入自己的用户名和token:
成功了:
访问自己创建的Hugging face :https://huggingface.co/spaces/GaoLang/foodvision_mini_test

成功部署!!

测试上传图片进行预测:nice


补充:创建更大的FoodVision

由于 FoodVision Mini 是根据 Food101 数据集(101 类食物 x 1000 张图像)中的披萨、牛排和寿司图像进行训练的,我们如何通过在所有 101 类上训练模型来使 FoodVision Big 变得更强大。

将从3个类别变成101个类别。

所要做的就是稍微改变我们的 EffNetB2 模型并准备一个不同的数据集。

  1. 为 FoodVision Big 创建模型并进行转换

同样使用EffNetB2 模型,可以使用上面第 3.1 节中创建的 create_effnetb2_model() 函数为 Food101 创建 EffNetB2 特征提取器,并向其传递参数 num_classes=101 (因为 Food101 有 101 个类)。

python 复制代码
# Create EffNetB2 model capable of fitting to 101 classes for Food101
effnetb2_food101, effnetb2_transforms = create_effnetb2_model(num_classes=101)

用summary 总结一下模型

python 复制代码
from torchinfo import summary

# # Get a summary of EffNetB2 feature extractor for Food101 with 101 output classes (uncomment for full output)
summary(effnetb2_food101, 
        input_size=(1, 3, 224, 224),
        col_names=["input_size", "output_size", "num_params", "trainable"],
        col_width=20,
        row_settings=["var_names"])

基础层被冻结(这些是在 ImageNet 上预训练的),而外层( classifier 层)是可训练的,输出形状为 [batch_size, 101] (对于 Food101 中的 101 类, 101 )。

由于将处理比平时更多的数据,因此可以在转换中添加一些数据增强 ( effnetb2_transforms ) 以增强训练数据。

编写一个 torchvision.transforms 管道来使用 torchvision.transforms.TrivialAugmentWide() (PyTorch 团队在其计算机视觉配方中使用的相同数据增强)以及 effnetb2_transforms 来转换我们的训练图像。

python 复制代码
# Create Food101 training data transforms (only perform data augmentation on the training images)
food101_train_transforms = torchvision.transforms.Compose([
    torchvision.transforms.TrivialAugmentWide(),
    effnetb2_transforms,
])
  1. 获取 FoodVision Big 数据

要获取整个 Food101 数据集,可以使用 torchvision.datasets.Food101()

设置目录 data/ 的路径来存储图像,然后,我们将使用 food101_train_transforms 和 effnetb2_transforms 分别下载和转换训练和测试数据集分割来转换每个数据集。

python 复制代码
from torchvision import datasets

# Setup data directory
from pathlib import Path
data_dir = Path("data")

# Get training data (~750 images x 101 food classes)
train_data = datasets.Food101(root=data_dir, # path to download data to
                              split="train", # dataset split to get
                              transform=food101_train_transforms, # perform data augmentation on training data
                              download=True) # want to download?

# Get testing data (~250 images x 101 food classes)
test_data = datasets.Food101(root=data_dir,
                             split="test",
                             transform=effnetb2_transforms, # perform normal EffNetB2 transforms on test data
                             download=True)

可以使用 train_data.classes 获取所有类名的列表:

python 复制代码
# Get Food101 class names
food101_class_names = train_data.classes

# View the first 10
food101_class_names[:10]
  1. 创建 Food101 数据集的子集以加快实验速度

可选项,为了保持训练速度快,我们将训练数据集和测试数据集划分为 20%。

为了使 FoodVision Big(20% 数据)分割,让我们创建一个名为 split_dataset() 的函数来将给定的数据集分割成一定的比例。

可以使用 torch.utils.data.random_split() 使用 lengths 参数创建给定大小的分割。

lengths 参数接受所需分割长度的列表,其中列表的总长度必须等于数据集的总长度。

例如,对于大小为 100 的数据集,可以传入 lengths=[20, 80] 来接收 20% 和 80% 的分割。

希望函数返回两个分割,一个具有目标长度(例如训练数据的 20%),另一个具有剩余长度(例如训练数据的剩余 80%)。

将 generator 参数设置为 torch.manual_seed() 值以实现可重复性:

python 复制代码
def split_dataset(dataset:torchvision.datasets, split_size:float=0.2, seed:int=42):
    """Randomly splits a given dataset into two proportions based on split_size and seed.

    Args:
        dataset (torchvision.datasets): A PyTorch Dataset, typically one from torchvision.datasets.
        split_size (float, optional): How much of the dataset should be split? 
            E.g. split_size=0.2 means there will be a 20% split and an 80% split. Defaults to 0.2.
        seed (int, optional): Seed for random generator. Defaults to 42.

    Returns:
        tuple: (random_split_1, random_split_2) where random_split_1 is of size split_size*len(dataset) and 
            random_split_2 is of size (1-split_size)*len(dataset).
    """
    # Create split lengths based on original dataset length
    length_1 = int(len(dataset) * split_size) # desired length
    length_2 = len(dataset) - length_1 # remaining length
        
    # Print out info
    print(f"[INFO] Splitting dataset of length {len(dataset)} into splits of size: {length_1} ({int(split_size*100)}%), {length_2} ({int((1-split_size)*100)}%)")
    
    # Create splits with given random seed
    random_split_1, random_split_2 = torch.utils.data.random_split(dataset, 
                                                                   lengths=[length_1, length_2],
                                                                   generator=torch.manual_seed(seed)) # set the random seed for reproducible splits
    return random_split_1, random_split_2

通过创建 Food101 的 20% 训练和测试数据集来测试它:

python 复制代码
# Create training 20% split of Food101
train_data_food101_20_percent, _ = split_dataset(dataset=train_data,
                                                 split_size=0.2)

# Create testing 20% split of Food101
test_data_food101_20_percent, _ = split_dataset(dataset=test_data,
                                                split_size=0.2)

len(train_data_food101_20_percent), len(test_data_food101_20_percent)
  1. 将 Food101 Dataset转换为 DataLoader
python 复制代码
import os
import torch

BATCH_SIZE = 32
NUM_WORKERS = 2 if os.cpu_count() <= 4 else 4 # this value is very experimental and will depend on the hardware you have available, Google Colab generally provides 2x CPUs

# Create Food101 20 percent training DataLoader
train_dataloader_food101_20_percent = torch.utils.data.DataLoader(train_data_food101_20_percent,
                                                                  batch_size=BATCH_SIZE,
                                                                  shuffle=True,
                                                                  num_workers=NUM_WORKERS)
# Create Food101 20 percent testing DataLoader
test_dataloader_food101_20_percent = torch.utils.data.DataLoader(test_data_food101_20_percent,
                                                                 batch_size=BATCH_SIZE,
                                                                 shuffle=False,
                                                                 num_workers=NUM_WORKERS)
  1. 训练FoodVision大模型

使用 torch.optim.Adam() 和学习率 1e-3 创建优化器:

使用 torch.nn.CrossEntropyLoss() 和 label_smoothing=0.1 设置一个损失函数,与 torchvision 的状态内联最先进的训练配方。

标签平滑是一种正则化技术(正则化是描述防止过度拟合过程的另一个词),它减少模型赋予任何标签的价值并将其分散到其他标签上。从本质上讲,标签平滑并不是模型对单个标签过于自信,而是为其他标签提供非零值以帮助泛化。

没有标签平滑的模型具有以下 5 个类别的输出:[0, 0, 0.99, 0.01, 0]

具有标签平滑的模型可能具有以下输出:[0.01, 0.01, 0.96, 0.01, 0.01]

模型仍然对 3 类的预测充满信心,但为其他标签提供较小的值会迫使模型至少考虑其他选项。

开始训练:

python 复制代码
from going_modular.going_modular import engine

# Setup optimizer
optimizer = torch.optim.Adam(params=effnetb2_food101.parameters(),
                             lr=1e-3)

# Setup loss function
loss_fn = torch.nn.CrossEntropyLoss(label_smoothing=0.1) # throw in a little label smoothing because so many classes

# Want to beat original Food101 paper with 20% of data, need 56.4%+ acc on test dataset
set_seeds()    
effnetb2_food101_results = engine.train(model=effnetb2_food101,
                                        train_dataloader=train_dataloader_food101_20_percent,
                                        test_dataloader=test_dataloader_food101_20_percent,
                                        optimizer=optimizer,
                                        loss_fn=loss_fn,
                                        epochs=5,
                                        device=device)
  1. 检查FoodVision Big模型的损失曲线
python 复制代码
from helper_functions import plot_loss_curves

# Check out the loss curves for FoodVision Big
plot_loss_curves(effnetb2_food101_results)
  1. 保存和加载 FoodVision Big

保存:

python 复制代码
from going_modular.going_modular import utils

# Create a model path
effnetb2_food101_model_path = "09_pretrained_effnetb2_feature_extractor_food101_20_percent.pth" 

# Save FoodVision Big model
utils.save_model(model=effnetb2_food101,
                 target_dir="models",
                 model_name=effnetb2_food101_model_path)

加载:

首先使用 create_effnetb2_model(num_classes=101) 创建一个模型实例(所有 Food101 类有 101 个类)。然后用 torch.nn.Module.load_state_dict() 和 torch.load() 加载保存的 state_dict() 。

python 复制代码
# Create Food101 compatible EffNetB2 instance
loaded_effnetb2_food101, effnetb2_transforms = create_effnetb2_model(num_classes=101)

# Load the saved model's state_dict()
loaded_effnetb2_food101.load_state_dict(torch.load("models/09_pretrained_effnetb2_feature_extractor_food101_20_percent.pth"))
  1. 检查 FoodVision 大模型尺寸
python 复制代码
from pathlib import Path

# Get the model size in bytes then convert to megabytes
pretrained_effnetb2_food101_model_size = Path("models", effnetb2_food101_model_path).stat().st_size // (1024*1024) # division converts bytes to megabytes (roughly) 
print(f"Pretrained EffNetB2 feature extractor Food101 model size: {pretrained_effnetb2_food101_model_size} MB")

补充:将 FoodVision Big 模型转变为可部署的应用程序

将以与部署 FoodVision Mini 模型相同的方式部署 FoodVision Big 模型,作为 Hugging Face Spaces 上的 Gradio 演示。

创建一个 demos/foodvision_big/ 目录来存储 FoodVision Big 演示文件,并创建一个 demos/foodvision_big/examples 目录来保存用于测试演示的示例图像。

文件结构:

python 复制代码
demos/
  foodvision_big/
    09_pretrained_effnetb2_feature_extractor_food101_20_percent.pth
    app.py
    class_names.txt
    examples/
      example_1.jpg
    model.py
    requirements.txt

class_names.txt 包含 FoodVision Big 的所有类名称。

python 复制代码
from pathlib import Path

# Create FoodVision Big demo path
foodvision_big_demo_path = Path("demos/foodvision_big/")

# Make FoodVision Big demo directory
foodvision_big_demo_path.mkdir(parents=True, exist_ok=True)

# Make FoodVision Big demo examples directory
(foodvision_big_demo_path / "examples").mkdir(parents=True, exist_ok=True)
  1. 下载示例图像并将其移动到 examples 目录

通过 !wget 命令从 GitHub 下载图片,然后使用 !mv 命令("move"的缩写)将其移动到 demos/foodvision_big/examples 。

将经过训练的 Food101 EffNetB2 模型从 models/09_pretrained_effnetb2_feature_extractor_food101_20_percent.pth 移动到 demos/foodvision_big 。

python 复制代码
# Download and move an example image
!wget https://raw.githubusercontent.com/mrdbourke/pytorch-deep-learning/main/images/04-pizza-dad.jpeg 
!mv 04-pizza-dad.jpeg demos/foodvision_big/examples/04-pizza-dad.jpg

# Move trained model to FoodVision Big demo folder (will error if model is already moved)
!mv models/09_pretrained_effnetb2_feature_extractor_food101_20_percent.pth demos/foodvision_big
  1. 将 Food101 类名保存到文件 ( class_names.txt )

由于 Food101 数据集中有很多类,因此我们不要将它们作为列表存储在 app.py 文件中,而是将它们保存到 .txt 文件中,并在必要时读取它们。

python 复制代码
# Check out the first 10 Food101 class names
food101_class_names[:10]

首先创建 demos/foodvision_big/class_names.txt 的路径,然后使用 Python 的 open() 打开一个文件,然后写入该文件,为每个类留下一个新行。

python 复制代码
# Create path to Food101 class names
foodvision_big_class_names_path = foodvision_big_demo_path / "class_names.txt"

# Write Food101 class names list to file
with open(foodvision_big_class_names_path, "w") as f:
    print(f"[INFO] Saving Food101 class names to {foodvision_big_class_names_path}")
    f.write("\n".join(food101_class_names)) # leave a new line between each class

将在读取模式 ( "r" ) 下使用 Python 的 open() ,然后使用 readlines() 方法读取 class_names.txt

可以通过使用列表理解和 strip() 去除每个类名的换行符值,将类名保存到列表中。

python 复制代码
# Open Food101 class names file and read each line into a list
with open(foodvision_big_class_names_path, "r") as f:
    food101_class_names_loaded = [food.strip() for food in  f.readlines()]
    
# View the first 5 class names loaded back in
food101_class_names_loaded[:5]
  1. 将 FoodVision Big 模型转换为 Python 脚本 ( model.py )

创建一个能够实例化 EffNetB2 特征提取器模型及其必要转换的脚本。

python 复制代码
%%writefile demos/foodvision_big/model.py
import torch
import torchvision

from torch import nn


def create_effnetb2_model(num_classes:int=3, 
                          seed:int=42):
    """Creates an EfficientNetB2 feature extractor model and transforms.

    Args:
        num_classes (int, optional): number of classes in the classifier head. 
            Defaults to 3.
        seed (int, optional): random seed value. Defaults to 42.

    Returns:
        model (torch.nn.Module): EffNetB2 feature extractor model. 
        transforms (torchvision.transforms): EffNetB2 image transforms.
    """
    # Create EffNetB2 pretrained weights, transforms and model
    weights = torchvision.models.EfficientNet_B2_Weights.DEFAULT
    transforms = weights.transforms()
    model = torchvision.models.efficientnet_b2(weights=weights)

    # Freeze all layers in base model
    for param in model.parameters():
        param.requires_grad = False

    # Change classifier head with random seed for reproducibility
    torch.manual_seed(seed)
    model.classifier = nn.Sequential(
        nn.Dropout(p=0.3, inplace=True),
        nn.Linear(in_features=1408, out_features=num_classes),
    )
    
    return model, transforms
  1. 将 FoodVision Big Gradio 应用程序转换为 Python 脚本 ( app.py )
python 复制代码
%%writefile demos/foodvision_big/app.py
### 1. Imports and class names setup ### 
import gradio as gr
import os
import torch

from model import create_effnetb2_model
from timeit import default_timer as timer
from typing import Tuple, Dict

# Setup class names
with open("class_names.txt", "r") as f: # reading them in from class_names.txt
    class_names = [food_name.strip() for food_name in  f.readlines()]
    
### 2. Model and transforms preparation ###    

# Create model
effnetb2, effnetb2_transforms = create_effnetb2_model(
    num_classes=101, # could also use len(class_names)
)

# Load saved weights
effnetb2.load_state_dict(
    torch.load(
        f="09_pretrained_effnetb2_feature_extractor_food101_20_percent.pth",
        map_location=torch.device("cpu"),  # load to CPU
    )
)

### 3. Predict function ###

# Create predict function
def predict(img) -> Tuple[Dict, float]:
    """Transforms and performs a prediction on img and returns prediction and time taken.
    """
    # Start the timer
    start_time = timer()
    
    # Transform the target image and add a batch dimension
    img = effnetb2_transforms(img).unsqueeze(0)
    
    # Put model into evaluation mode and turn on inference mode
    effnetb2.eval()
    with torch.inference_mode():
        # Pass the transformed image through the model and turn the prediction logits into prediction probabilities
        pred_probs = torch.softmax(effnetb2(img), dim=1)
    
    # Create a prediction label and prediction probability dictionary for each prediction class (this is the required format for Gradio's output parameter)
    pred_labels_and_probs = {class_names[i]: float(pred_probs[0][i]) for i in range(len(class_names))}
    
    # Calculate the prediction time
    pred_time = round(timer() - start_time, 5)
    
    # Return the prediction dictionary and prediction time 
    return pred_labels_and_probs, pred_time

### 4. Gradio app ###

# Create title, description and article strings
title = "FoodVision Big 🍔👁"
description = "An EfficientNetB2 feature extractor computer vision model to classify images of food into [101 different classes](https://github.com/mrdbourke/pytorch-deep-learning/blob/main/extras/food101_class_names.txt)."
article = "Created at [09. PyTorch Model Deployment](https://www.learnpytorch.io/09_pytorch_model_deployment/)."

# Create examples list from "examples/" directory
example_list = [["examples/" + example] for example in os.listdir("examples")]

# Create Gradio interface 
demo = gr.Interface(
    fn=predict,
    inputs=gr.Image(type="pil"),
    outputs=[
        gr.Label(num_top_classes=5, label="Predictions"),
        gr.Number(label="Prediction time (s)"),
    ],
    examples=example_list,
    title=title,
    description=description,
    article=article,
)

# Launch the app!
demo.launch()
  1. 为 FoodVision Big 创建需求文件 ( requirements.txt )
python 复制代码
%%writefile demos/foodvision_big/requirements.txt
torch==1.12.0
torchvision==0.13.0
gradio==2.0.0
  1. 下载 FoodVision Big 应用程序文件
python 复制代码
# Zip foodvision_big folder but exclude certain files
!cd demos/foodvision_big && zip -r ../foodvision_big.zip * -x "*.pyc" "*.ipynb" "*__pycache__*" "*ipynb_checkpoints*"

# Download the zipped FoodVision Big app (if running in Google Colab)
try:
    from google.colab import files
    files.download("demos/foodvision_big.zip")
except:
    print("Not running in Google Colab, can't use google.colab.files.download()")
  1. 将 FoodVision Big 应用程序部署到 HuggingFace Spaces

步骤同9. 将 FoodVision Mini 应用程序部署到 HuggingFace Spaces


相关推荐
计算机源码社5 分钟前
分享一个餐饮连锁店点餐系统 餐馆食材采购系统Java、python、php三个版本(源码、调试、LW、开题、PPT)
java·python·php·毕业设计项目·计算机课程设计·计算机毕业设计源码·计算机毕业设计选题
汤兰月10 分钟前
Python中的观察者模式:从基础到实战
开发语言·python·观察者模式
chnyi6_ya21 分钟前
论文笔记:Online Class-Incremental Continual Learning with Adversarial Shapley Value
论文阅读·人工智能
中杯可乐多加冰21 分钟前
【AI驱动TDSQL-C Serverless数据库技术实战】 AI电商数据分析系统——探索Text2SQL下AI驱动代码进行实际业务
c语言·人工智能·serverless·tdsql·腾讯云数据库
西柚与蓝莓2 小时前
【开源开放体系总结】
python
萱仔学习自我记录2 小时前
PEFT库和transformers库在NLP大模型中的使用和常用方法详解
人工智能·机器学习
hsling松子5 小时前
使用PaddleHub智能生成,献上浓情国庆福
人工智能·算法·机器学习·语言模型·paddlepaddle
belldeep5 小时前
python:reportlab 将多个图片合并成一个PDF文件
python·pdf·reportlab
正在走向自律5 小时前
机器学习框架
人工智能·机器学习
好吃番茄6 小时前
U mamba配置问题;‘KeyError: ‘file_ending‘
人工智能·机器学习