六. 部署分类器-deploy-classification-basic

news2024/12/29 16:37:56

目录

    • 前言
    • 0. 简述
    • 1. 案例运行
    • 2. 代码分析
      • 2.1 main.cpp
      • 2.2 model.cpp
    • 3. 补充说明
    • 结语
    • 下载链接
    • 参考

前言

自动驾驶之心推出的 《CUDA与TensorRT部署实战课程》,链接。记录下个人学习笔记,仅供自己参考

本次课程我们来学习课程第六章—部署分类器,一起来学习部署一个简单的分类器

课程大纲可以看下面的思维导图

在这里插入图片描述

0. 简述

本小节目标:学习部署一个简单的分类器

这个小节是一个初步分类器的实现

下面我们开始本次课程的学习🤗

1. 案例运行

在正式开始课程之前,博主先带大家跑通 6.1-deploy-classification 这个小节的案例🤗

源代码获取地址:https://github.com/kalfazed/tensorrt_starter

首先大家需要把 tensorrt_starter 这个项目给 clone 下来,指令如下:

git clone https://github.com/kalfazed/tensorrt_starter.git

也可手动点击下载,点击右上角的 Code 按键,将代码下载下来。至此整个项目就已经准备好了。也可以点击 here 下载博主准备好的源代码(注意代码下载于 2024/7/14 日,若有改动请参考最新

整个项目后续需要使用的软件主要有 CUDA、cuDNN、TensorRT、OpenCV,大家可以参考 Ubuntu20.04软件安装大全 进行相应软件的安装,博主这里不再赘述

假设你的项目、环境准备完成,下面我们一起来运行下 6.1-deploy-classification 小节案例代码

开始之前我们需要创建几个文件夹,在 tensorrt_starter/chapter6-deploy-classification-and-inference-design/6.1-deploy-classification 小节中创建一个 models 文件夹,接着在 models 文件夹下创建一个 onnx 和 engine 文件夹,总共三个文件夹需要创建

创建完后 6.1 小节整个目录结构如下:

在这里插入图片描述

接着我们需要执行 python 文件创建一个 ONNX 模型,先进入到 6.1 小节中:

cd tensorrt_starter/chapter6-deploy-classification-and-inference-design/6.1-deploy-classification

执行如下指令:

python src/python/export_pretained.py -d ./models/onnx/

Note:大家需要准备一个虚拟环境,安装好 torch、onnx、onnxsim 等第三方库

输出如下:

在这里插入图片描述

生成好的 reset50.onnx 模型文件保存在 models/onnx 文件夹下,大家可以查看

接着我们需要利用 ONNX 生成对应的 engine 完成推理,在此之前我们需要修改下整体的 Makefile.config,指定一些库的路径:

# tensorrt_starter/config/Makefile.config
# CUDA_VER                    :=  11
CUDA_VER                    :=  11.6
    
# opencv和TensorRT的安装目录
OPENCV_INSTALL_DIR          :=  /usr/local/include/opencv4
# TENSORRT_INSTALL_DIR        :=  /mnt/packages/TensorRT-8.4.1.5
TENSORRT_INSTALL_DIR        :=  /home/jarvis/lean/TensorRT-8.6.1.6

Note:大家查看自己的 CUDA 是多少版本,修改为对应版本即可,另外 OpenCV 和 TensorRT 修改为你自己安装的路径即可

接着我们就可以来执行编译,指令如下:

make -j64

输出如下:

在这里插入图片描述

接着执行:

./bin/trt-infer

输出如下:

在这里插入图片描述

在这里插入图片描述

我们这里可以看到模型推理的前处理、推理和后处理时间的记录,以及每张图片推理的结果以及置信度都可以从打印信息中获取

Note:这里大家可以测试下其它的 ONNX 模型看下推理结果,这里博主准备了导出好的各个分类模型的 ONNX,大家可以点击 here 下载

如果大家能够看到上述输出结果,那就说明本小节案例已经跑通,下面我们就来看看具体的代码实现

2. 代码分析

2.1 main.cpp

我们先从 main.cpp 看起:

#include <iostream>
#include <memory>

#include "model.hpp"
#include "utils.hpp"

using namespace std;

int main(int argc, char const *argv[])
{
    Model model("models/onnx/resnet50.onnx", Model::precision::FP32);

    if(!model.build()){
        LOGE("fail in building model");
        return 0;
    }

    if(!model.infer("data/fox.png")){
        LOGE("fail in infering model");
        return 0;
    }
    if(!model.infer("data/cat.png")){
        LOGE("fail in infering model");
        return 0;
    }

    if(!model.infer("data/eagle.png")){
        LOGE("fail in infering model");
        return 0;
    }

    if(!model.infer("data/gazelle.png")){
        LOGE("fail in infering model");
        return 0;
    }

    return 0;
}

首先 Model 构造函数需要传入 ONNX 模型路径以及指定要生成的 engine 的精度,接着通过 build 接口构建 engine,并调用 infer 接口推理对每一张图片进行分类预测

2.2 model.cpp

我们先看 build 接口:

bool Model::build(){
    if (fileExists(mEnginePath)){
        LOG("%s has been generated!", mEnginePath.c_str());
        return true;
    } else {
        LOG("%s not found. Building engine...", mEnginePath.c_str());
    }

    Logger logger;
    auto builder       = make_unique<nvinfer1::IBuilder>(nvinfer1::createInferBuilder(logger));
    auto network       = make_unique<nvinfer1::INetworkDefinition>(builder->createNetworkV2(1));
    auto config        = make_unique<nvinfer1::IBuilderConfig>(builder->createBuilderConfig());
    auto parser        = make_unique<nvonnxparser::IParser>(nvonnxparser::createParser(*network, logger));

    config->setMaxWorkspaceSize(1<<28);
    config->setProfilingVerbosity(nvinfer1::ProfilingVerbosity::kDETAILED);

    if (!parser->parseFromFile(mOnnxPath.c_str(), 1)){
        LOGE("ERROR: failed to %s", mOnnxPath.c_str());
        return false;
    }

    if (builder->platformHasFastFp16() && mPrecision == nvinfer1::DataType::kHALF) {
        config->setFlag(nvinfer1::BuilderFlag::kFP16);
        config->setFlag(nvinfer1::BuilderFlag::kPREFER_PRECISION_CONSTRAINTS);
    }

    auto engine        = make_unique<nvinfer1::ICudaEngine>(builder->buildEngineWithConfig(*network, *config));
    auto plan          = builder->buildSerializedNetwork(*network, *config);
    auto runtime       = make_unique<nvinfer1::IRuntime>(nvinfer1::createInferRuntime(logger));

    auto f = fopen(mEnginePath.c_str(), "wb");
    fwrite(plan->data(), 1, plan->size(), f);
    fclose(f);

    // 如果想要观察模型优化前后的架构变化,可以取消注释
    mEngine            = shared_ptr<nvinfer1::ICudaEngine>(runtime->deserializeCudaEngine(plan->data(), plan->size()));
    mInputDims         = network->getInput(0)->getDimensions();
    mOutputDims        = network->getOutput(0)->getDimensions();

    int inputCount = network->getNbInputs();
    int outputCount = network->getNbOutputs();
    string layer_info;

    LOGV("Before TensorRT optimization");
    print_network(*network, false);
    LOGV("");
    LOGV("After TensorRT optimization");
    print_network(*network, true);

    LOGV("Finished building engine");

    return true;
};

那这个 build 其实和前面的没有什么不同,不同的点在于 FP16 精度的指定:

if (builder->platformHasFastFp16() && mPrecision == nvinfer1::DataType::kHALF) {
    config->setFlag(nvinfer1::BuilderFlag::kFP16);
    config->setFlag(nvinfer1::BuilderFlag::kPREFER_PRECISION_CONSTRAINTS);
}

首先我们会做一个判断,检查当前的硬件平台是否支持 FP16 运算并检查我们设置的模型精度是否是 FP16,如果两个条件都满足,我们通过 config 的 setFalg 方法告诉 TensorRT 构建 engine 时启用 FP16 精度,并尽可能的优先保持指定的 FP16 精度要求,更多细节大家可以参考 https://docs.nvidia.com/deeplearning/tensorrt/api/c_api/namespacenvinfer1

下面我们重点来看下 infer 接口:

bool Model::infer(string imagePath){
    if (!fileExists(mEnginePath)) {
        LOGE("ERROR: %s not found", mEnginePath.c_str());
        return false;
    }

    vector<unsigned char> modelData;
    modelData = loadFile(mEnginePath);
    
    Timer timer;
    Logger logger;
    auto runtime      = make_unique<nvinfer1::IRuntime>(nvinfer1::createInferRuntime(logger));
    auto engine       = make_unique<nvinfer1::ICudaEngine>(runtime->deserializeCudaEngine(modelData.data(), modelData.size()));
    auto context      = make_unique<nvinfer1::IExecutionContext>(engine->createExecutionContext());
}

infer 前面的部分和我们之前说的没有什么区别,创建 runtime,通过 runtime 创建 engine,通过 engine 创建 context

我们接着看模型的前处理部分:

auto input_dims   = context->getBindingDimensions(0);
auto output_dims  = context->getBindingDimensions(1);

cudaStream_t stream;
CUDA_CHECK(cudaStreamCreate(&stream));

int input_width    = input_dims.d[3];
int input_height   = input_dims.d[2];
int input_channel  = input_dims.d[1];
int num_classes    = output_dims.d[1];
int input_size     = input_channel * input_width * input_height * sizeof(float);
int output_size    = num_classes * sizeof(float);

/* 
    为了让trt推理和pytorch的推理结果一致,我们需要对其pytorch所用的mean和std
    这里面opencv读取的图片是BGR格式,所以mean和std也按照BGR的顺序存储
    可以参考pytorch官方提供的前处理方案: https://pytorch.org/hub/pytorch_vision_resnet/
*/
float mean[]       = {0.406, 0.456, 0.485};
float std[]        = {0.225, 0.224, 0.229};

/*Preprocess -- 分配host和device的内存空间*/
float* input_host    = nullptr;
float* input_device  = nullptr;
float* output_host   = nullptr;
float* output_device = nullptr;
CUDA_CHECK(cudaMalloc(&input_device, input_size));
CUDA_CHECK(cudaMalloc(&output_device, output_size));
CUDA_CHECK(cudaMallocHost(&input_host, input_size));
CUDA_CHECK(cudaMallocHost(&output_host, output_size));

/*Preprocess -- 测速*/
timer.start_cpu();

/*Preprocess -- 读取数据*/
cv::Mat input_image;
input_image = cv::imread(imagePath);
if (input_image.data == nullptr) {
    LOGE("file not founded! Program terminated");
    return false;
} else {
    LOG("Model:      %s", getFileName(mOnnxPath).c_str());
    LOG("Precision:  %s", getPrecision(mPrecision).c_str());
    LOG("Image:      %s", getFileName(imagePath).c_str());
}

/*Preprocess -- resize(默认是bilinear interpolation)*/
cv::resize(input_image, input_image, cv::Size(input_width, input_height));

/*Preprocess -- host端进行normalization + BGR2RGB + hwc2cwh)*/
int index;
int offset_ch0 = input_width * input_height * 0;
int offset_ch1 = input_width * input_height * 1;
int offset_ch2 = input_width * input_height * 2;
for (int i = 0; i < input_height; i++) {
    for (int j = 0; j < input_width; j++) {
        index = i * input_width * input_channel + j * input_channel;
        input_host[offset_ch2++] = (input_image.data[index + 0] / 255.0f - mean[0]) / std[0];
        input_host[offset_ch1++] = (input_image.data[index + 1] / 255.0f - mean[1]) / std[1];
        input_host[offset_ch0++] = (input_image.data[index + 2] / 255.0f - mean[2]) / std[2];
    }
}

/*Preprocess -- 将host的数据移动到device上*/
CUDA_CHECK(cudaMemcpyAsync(input_device, input_host, input_size, cudaMemcpyKind::cudaMemcpyHostToDevice, stream));

timer.stop_cpu();
timer.duration_cpu<Timer::ms>("preprocess(resize + norm + bgr2rgb + hwc2chw + H2D)");

/*Inference -- 测速*/
timer.start_cpu();

首先我们通过调用 getBindingDimensions 获取推理输入和输出的张量维度,接着通过 input_dimsoutput_dims 获取输入宽高以及输出的类别数并计算输入输出所需要的内容空间大小

float* input_host    = nullptr;
float* input_device  = nullptr;
float* output_host   = nullptr;
float* output_device = nullptr;
CUDA_CHECK(cudaMalloc(&input_device, input_size));
CUDA_CHECK(cudaMalloc(&output_device, output_size));
CUDA_CHECK(cudaMallocHost(&input_host, input_size));
CUDA_CHECK(cudaMallocHost(&output_host, output_size));

接着我们使用 cudaMalloc 函数为输入和输出张量在 device 上分配内存,使用 cudaMallocHost 函数为输入和输出张量在 host 上分配内存,这些内存将用于存储和传递模型推理所需的数据

cv::Mat input_image;
input_image = cv::imread(imagePath);
if (input_image.data == nullptr) {
    LOGE("file not founded! Program terminated");
    return false;
}

cv::resize(input_image, input_image, cv::Size(input_width, input_height));

然后我们使用 opencv 来读取输入图像并将图像调整为模型所需的输入尺寸

int index;
int offset_ch0 = input_width * input_height * 0;
int offset_ch1 = input_width * input_height * 1;
int offset_ch2 = input_width * input_height * 2;
for (int i = 0; i < input_height; i++) {
    for (int j = 0; j < input_width; j++) {
        index = i * input_width * input_channel + j * input_channel;
        input_host[offset_ch2++] = (input_image.data[index + 0] / 255.0f - mean[0]) / std[0];
        input_host[offset_ch1++] = (input_image.data[index + 1] / 255.0f - mean[1]) / std[1];
        input_host[offset_ch0++] = (input_image.data[index + 2] / 255.0f - mean[2]) / std[2];
    }
}

接着我们使用上节课提到的 CPU 端的预处理代码对输入图像进行预处理操作,主要包括 bgr2rgb、normalization、hwc2chw 以及减均值除以标准差

Note:这里需要大家注意为了让 tensorrt 推理和 pytorch 的推理结果一致,我们需要保证它们做的前处理一模一样,也就是说 pytorch 中做了哪些图像预处理操作在 tensorrt 中也需要做同样的操作,所以大家如果发现部署的模型在 tensorrt 和 pytorch 上精度对不齐的时候,可以考虑从模型的前处理入手,是不是没有做 bgr2rgb 呢,是不是没有除以 255 呢,是不是没有减均值除以标准差呢

pytorch 官方在实现 resnet 时所做的前处理操作大家可以参考:https://pytorch.org/hub/pytorch_vision_resnet/

CUDA_CHECK(cudaMemcpyAsync(input_device, input_host, input_size, cudaMemcpyKind::cudaMemcpyHostToDevice, stream));

最后使用 cudaMemcpyAsync 将处理好的输入数据从主机内存 host 上复制到设备内存 device 上,传输是在指定的 CUAD 流中异步进行的

以上就是整个分类模型的前处理操作,下面我们来看推理部分

float* bindings[] = {input_device, output_device};
if (!context->enqueueV2((void**)bindings, stream, nullptr)){
    LOG("Error happens during DNN inference part, program terminated");
    return false;
}

首先定义一个 bindings 数组,将输入和输出张量的 device 指针传递给 TensorRT 执行上下文,调用 enqueueV2 函数在执行的 CUDA 流中异步执行推理操作,推理过程将使用预先加载的模型和输入数据在 GPU 上计算输出结果

以上就是整个分类模型的推理操作,比较简单,下面我们来看后处理部分

CUDA_CHECK(cudaMemcpyAsync(output_host, output_device, output_size, cudaMemcpyKind::cudaMemcpyDeviceToHost, stream));
CUDA_CHECK(cudaStreamSynchronize(stream));

首先使用 cudaMemcpyAsync 将推理结果从 device 内存复制回 host 内存,cudaStreamSynchronize 函数确保所有异步操作在这里完成

ImageNetLabels labels;
int pos = max_element(output_host, output_host + num_classes) - output_host;
float confidence = output_host[pos] * 100;

然后我们从推理中寻找分类结果,max_element 函数用于找到推理结果中概率最大的类别索引,并计算对应的置信度,利用 ImageNetLabels 类获取对应的标签名

LOG("Inference result: %s, Confidence is %.3f%%\n", labels.imagenet_labelstring(pos).c_str(), confidence);   

CUDA_CHECK(cudaFree(output_device));
CUDA_CHECK(cudaFree(input_device));
CUDA_CHECK(cudaStreamDestroy(stream));

最后释放资源并输出结果

3. 补充说明

这个小节是一个根据第五章节的代码的改编,构建出来的一个 classification 的推理框架,可以实现一系列 preprocess + enqueue + postprocess 的推理。这里大家可以简单的参考下实现的方法,但是建议大家如果自己从零构建推理框架的话不要这么写,主要是因为目前这个框架有非常多的缺陷导致有很多潜在性的问题,主要是因为我们在设计初期并没有考虑太多,这里罗列几点

1. 代码看起来比较乱,主要有以下几个原因:

  • 整体上没有使用任何 C++ 设计模式,导致代码复用不好、可读性差、灵活性也比较低,对以后的代码扩展不是很友好
    • 比如说如果想让这个框架支持 detection 或者 segmentation 的话需要怎么办?
    • 再比如说如果想要让框架做成 multi-stage,multi-task 模型的话需要怎么办?
    • 再比如说如果想要这个框架既支持 image 输入,也支持 3D point cloud 输入需要怎么办?
  • 封装没有做好,导致有一些没有必要的信息和操作暴露在外面,让代码的可读性差
    • 比如说我们是否需要从 main 函数中考虑如果 build/infer 失败应该怎么办?
    • 再比如说我们在创建一个 engine 的时候,是否可以只从 main 中提供一系列参数,其余的信息不暴露?
  • 内存复用没有做好,导致出现一些额外的开辟销毁内存的开销
    • 比如说多张图片依次推理的时候,是否需要每次都 cudaMalloc 或者 cudaMallocHost 以及 cudaStreamCreate 呢?
    • 再比如说我们是否可以在 model 的构造函数初始化的时候,就把这个 model 所需要的 CPU 和 GPU 内存分配好以后就不变了呢?

2. 其次仍然还有很多功能没有写全:

  • INT8 量化的 calibrator 的实现
  • trt plugin 的实现
  • CPU 和 GPU overlap 推理的实现
  • 多 batch 或者动态 batch 的实现
  • CPU 端 threa 级别的并行处理的实现

当然还有很多需要扩展的地方,但是我们可以根据目标出现的一些问题,考虑一些解决方案,具体细节我们下节课程来讲

结语

本次课程我们学习了一个简单的分类器模型的部署,在 build 阶段主要是通过 onnxparser 构建 engine,在 infer 阶段主要包括前处理、推理以及后处理三部分,其中前处理需要和 pytorch 保持一致。另外这个小节的代码存在很多问题,比如代码看起来比较乱,没有封装,没有使用 C++ 设计模式等等,下节课我们就来解决这些问题

OK,以上就是 6.1 小节案例的全部内容了,下节我们来学习 6.2 小节优化分类器代码,敬请期待😄

下载链接

  • tensorrt_starter源码
  • 6.1-deploy-classification案例文件

参考

  • Ubuntu20.04软件安装大全
  • https://github.com/kalfazed/tensorrt_starter.git
  • https://pytorch.org/hub/pytorch_vision_resnet/
  • https://docs.nvidia.com/deeplearning/tensorrt/api/c_api/namespacenvinfer1

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2050304.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Flutter-自适用高度PageView

需求 在 Flutter 中&#xff0c;PageView 是一个非常常用的组件&#xff0c;能够实现多个页面的滑动切换。然而&#xff0c;默认的 PageView 高度是固定的&#xff0c;这在展示不同高度的页面时&#xff0c;可能会导致不必要的空白或内容裁剪问题。为了使 PageView 能够根据每…

Educational Codeforces Round 169 (Rated for Div. 2)(ABCDE)

A. Closest Point 签到 #define _rep(i,a,b) for(int i(a);i<(b);i) int n,m; int q[N]; void solve() {cin>>n;_rep(i,1,n)cin>>q[i];if(n!2)cout<<"NO\n";else if(abs(q[1]-q[2])!1)cout<<"YES\n";else cout<<"…

堆排序-优先级队列

我们用堆排来实现优先级队列&#xff0c;那么优先级队列是什么&#xff0c;就是 我们给每一个任务都添加一个优先级&#xff0c;优先级越高执行的越早我们用&#xff0c;但是我们怎么能按照顺序优先拿到优先级高的任务呢&#xff0c;我们可以用排序 来进行&#xff0c;也可以用…

Mybatis-Plus分页插件注意事项

使用Mybatis-Plus的分页插件进行分页查询时&#xff0c;如果结果需要使用<collection>进行映射&#xff0c;只能使用嵌套查询&#xff0c;而不能使用嵌套结果映射 嵌套查询和嵌套结果映射是Collection映射的两种方式&#xff0c;下面通过一个案例进行介绍 例如有room_i…

MyBatis源码系列3(解析配置文件,创建SqlSessionFactory对象)

创建SqlSessionFactory&#xff1b; 首先读取配置文件&#xff0c;使用构造者模式创建SqlSessionFactory对象。 InputStream inputStream Resources.getResourceAsStream("mybatis-config.xml");SqlSessionFactory sqlSessionFactory new SqlSessionFactoryBuilder…

C++面试基础系列-struct

系列文章目录 文章目录 系列文章目录C面试基础系列-struct1.C中struct2.C中struct2.1.同名函数2.2.typedef定义结构体别名2.3.继承 3.总结3.1.C和C中的Struct区别 4.struct字节对齐5.struct与const 关于作者 C面试基础系列-struct 1.C中struct struct里面只能放数据类型&#…

算法力扣刷题记录 八十六【47.全排列 II】

前言 回溯章节第12篇。 记录 八十四【46.全排列】初步学习了集合中无重复元素的排列求解。 本文&#xff1a;记录 八十六【47.全排列 II】当集合中有重复元素时&#xff0c;求解排列&#xff1b; 一、题目阅读 给定一个可包含重复数字的序列 nums &#xff0c;按任意顺序 返回…

VirtualBox安装Oracle Linux 7.9全流程

1.准备工作 1.1 VirtualBox下载 下载地址1&#xff1a; Downloads – Oracle VM VirtualBoxhttps://www.virtualbox.org/wiki/Downloads 下载地址2&#xff1a; https://www.oracle.com/virtualization/virtualbox/ 选择以上的任意一个地址都可下载到。 1.2 Oracle Linux 操作…

购物车系统设计方案

背景 在电商领域&#xff0c;购物车&#xff08;Shopping Cart&#xff09;扮演着至关重要的角色&#xff0c;它是连接用户浏览商品与最终完成购买行为的桥梁。 从两个视角来阐述&#xff0c;作为ToC的购物车&#xff0c;存在的意义&#xff1a; 从用户角度&#xff1a; 收…

ssm大学生实习管理系统的设计与实现-计算机毕业设计源码45837

摘 要 在信息时代&#xff0c;随着网络的快速发展&#xff0c;各个行业都离不开信息的处理。在这样的背景下&#xff0c;高校需要以学生管理信息为导向&#xff0c;并与学生实习的持续创新相结合。因此&#xff0c;设计一个高校学生实习管理系统就显得非常必要。 该系统采用了B…

维基百科向量搜索;简单易用的GraphRAG实现;友好的人工智能助手;AI的音乐多模态

✨ 1: Semantic Search on Wikipedia 维基百科向量搜索 为了证明 Upstash Vector 的可扩展性&#xff0c;Upstash在一个数据库中以 11 种语言&#xff08;144m 向量&#xff09;索引了整个维基百科 ◆ 超过700GB的数据 ◆ 快速语义搜索 ◆ 与维基百科聊天 为您提供了一款可…

Unity--AssetBundle AB包管理器

1.简介 AB包&#xff08;AssetBundle&#xff09;是Unity中用于资源管理的一种机制&#xff0c;它允许开发者将多个文件&#xff08;如纹理、模型、音频等&#xff09;打包成一个单独的文件&#xff0c;以便于在游戏运行时动态加载和卸载。 但是现在出现了最新的Addressable来…

Python匿名函数之lambda表达式使用详解

概要 在Python编程中,函数是组织代码和实现逻辑的基础单元。除了使用def关键字定义命名函数外,Python还提供了创建匿名函数的方式,即lambda表达式。lambda表达式是一种简洁的函数定义方式,通常用于需要简短函数的场景。本文将详细介绍Python匿名函数的概念、使用场景及其高…

基于51单片机的双机通信控制系统proteus仿真

地址&#xff1a; https://pan.baidu.com/s/1Y4wOJKOYf2E4JeEktyKdTw 提取码&#xff1a;1234 仿真图&#xff1a; 芯片/模块的特点&#xff1a; AT89C52/AT89C51简介&#xff1a; AT89C52/AT89C51是一款经典的8位单片机&#xff0c;是意法半导体&#xff08;STMicroelectro…

微分方程(Blanchard Differential Equations 4th)中文版Section3.1

3.1 PROPERTIES OF LINEAR SYSTEMS AND THE LINEARITY PRINCIPLE(线性系统问题与线性算子原理) 在第2章中,我们专注于研究微分方程组的定性和数值方法。之所以这样做,是因为我们很少能找到具有两个或更多个因变量的系统的明确解公式。唯一的例外是线性系统。在本章中,我们…

Linux·权限与工具-yum与vim

1. Linux软件包管理器 yum 1.1 什么是软件包 在Linux下安装软件&#xff0c;一个通常的办法是下载到程序的源代码&#xff0c;并进行编译&#xff0c;得到可执行程序。但这样做太麻烦了&#xff0c;于是有些人把一些常用的软件提前编译好&#xff0c;做成软件包(可以理解成Win…

12.2 使用prometheus-sdk向pushgateway打点

本节重点介绍 : 使用golang sdk打prometheus4种指标&#xff0c;推送到pushgateway gauge、counter、histogram、summary的初始化4种类似的设置值的方法推送到pushgateway的方法 prometheus配置采集pushgateway&#xff0c;grafana上配大盘 golang-sdk 项目地址 https://git…

【图数据库系列】Cypher查询语句:常用语法指南

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

HTML常用标签和CSS的运用,以及使用HTML做一个简历

目录 1.HTML标签 1.1 文档结构标签 1.2 文本格式标签 1.3 列表标签 1.4 链接和媒体标签 1.5 表格标签 1.6 表单标签 1.7 分区和布局标签 1.8 元数据标签 2.css样式 2.1 字体样式 2.2 文本样式 2.3 背景样式 2.4 边框样式 2.5 间距样式 2.6 宽度和高度 2.7 显示…

三种简单排序:插入排序、冒泡排序与选择排序 【算法 05】

三种简单排序&#xff1a;插入排序、冒泡排序与选择排序 在编程中&#xff0c;排序算法是基础且重要的知识点。虽然在实际开发中&#xff0c;我们可能会直接使用标准库中的排序函数&#xff08;如C的std::sort&#xff09;&#xff0c;但了解并实现这些基础排序算法对于理解算法…