深度学习模型部署(三)Onnxruntime部署yolov5实战

2024-03-11 07:44

本文主要是介绍深度学习模型部署(三)Onnxruntime部署yolov5实战,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

模型分析

先使用yolov5下的yolo.py输出查看一下yolov5s的结构

python ./yolov5/models/yolo.py
YOLOv5 🚀 v7.0-287-g574331f9 Python-3.8.18 torch-2.2.1+cu118 CUDA:0 (NVIDIA GeForce RTX 3060 Laptop GPU, 6144MiB)
## 这里面的from是指输入来自哪一层,-1表示来自上一层,6表示来自第6层from  n    params  module                                  arguments                     0                -1  1      3520  models.common.Conv                      [3, 32, 6, 2, 2]              1                -1  1     18560  models.common.Conv                      [32, 64, 3, 2]                2                -1  1     18816  models.common.C3                        [64, 64, 1]                   3                -1  1     73984  models.common.Conv                      [64, 128, 3, 2]               4                -1  2    115712  models.common.C3                        [128, 128, 2]                 5                -1  1    295424  models.common.Conv                      [128, 256, 3, 2]              6                -1  3    625152  models.common.C3                        [256, 256, 3]                 7                -1  1   1180672  models.common.Conv                      [256, 512, 3, 2]              8                -1  1   1182720  models.common.C3                        [512, 512, 1]                 9                -1  1    656896  models.common.SPPF                      [512, 512, 5]                 10                -1  1    131584  models.common.Conv                      [512, 256, 1, 1]              11                -1  1         0  torch.nn.modules.upsampling.Upsample    [None, 2, 'nearest']          12           [-1, 6]  1         0  models.common.Concat                    [1]                           13                -1  1    361984  models.common.C3                        [512, 256, 1, False]          14                -1  1     33024  models.common.Conv                      [256, 128, 1, 1]              15                -1  1         0  torch.nn.modules.upsampling.Upsample    [None, 2, 'nearest']          16           [-1, 4]  1         0  models.common.Concat                    [1]                           17                -1  1     90880  models.common.C3                        [256, 128, 1, False]          18                -1  1    147712  models.common.Conv                      [128, 128, 3, 2]              19          [-1, 14]  1         0  models.common.Concat                    [1]                           20                -1  1    296448  models.common.C3                        [256, 256, 1, False]          21                -1  1    590336  models.common.Conv                      [256, 256, 3, 2]              22          [-1, 10]  1         0  models.common.Concat                    [1]                           23                -1  1   1182720  models.common.C3                        [512, 512, 1, False]          24      [17, 20, 23]  1    229245  Detect                                  [80, [[10, 13, 16, 30, 33, 23], [30, 61, 62, 45, 59, 119], [116, 90, 156, 198, 373, 326]], [128, 256, 512]]
YOLOv5s summary: 214 layers, 7235389 parameters, 7235389 gradients, 16.6 GFLOPsFusing layers... 
YOLOv5s summary: 157 layers, 7225885 parameters, 7225885 gradients, 16.4 GFLOPs

可以看到yolov5s一共214层,层融合后还有157层
其中的C3层的结构是:
在这里插入图片描述
在这里插入图片描述

这里面的ConvBNSiLU就是指Conv+BN+SiLU,SiLU是ReLU的改进版激活函数,可以简单理解为y=x*sigmoid(x)。

SPPF的结构如下,SPPF的作用是图像金字塔池化,进行多尺度特征融合:
在这里插入图片描述
目标检测的三件套:Backbone,Neck,Head,
yolov5的Backbone就是CSP-DarkNet53
Neck是PANet
head比较简单,就是三个尺度各一个Conv卷积层
(这是6.0版本的图,我们用的是7.0版本的模型,将就着看,反正大致结构差不多)
在这里插入图片描述
不过这些对于我们部署来说不重要,我们只需要看数据流以及模型的计算图就行,至于哪一部分叫什么名字无所谓,不care。

另外还可以看一下fusing layers到底fuse了哪些层

def fuse(self):"""Fuses Conv2d() and BatchNorm2d() layers in the model to improve inference speed."""LOGGER.info("Fusing layers... ")for m in self.model.modules():if isinstance(m, (Conv, DWConv)) and hasattr(m, "bn"):m.conv = fuse_conv_and_bn(m.conv, m.bn)  # update convdelattr(m, "bn")  # remove batchnormm.forward = m.forward_fuse  # update forwardself.info()return self

我们可以看到,yolov5是将卷积和BN层融合到了一起

DWConv是指深度卷积depth-wise conv,相较于传统卷积的区别是一个输入channel卷积后对应一个输出channel,而不是多个输入channel卷积加和到一起对应一个输出channel,减少了计算量和参数量

具体fuse的代码如下,跟我们前面常见算子融合blog中讲的原理一模一样,不过并没有用torch自带的方法,而是自己实现的方法:

def fuse_conv_and_bn(conv, bn):"""Fuses Conv2d and BatchNorm2d layers into a single Conv2d layer.See https://tehnokv.com/posts/fusing-batchnorm-and-conv/."""fusedconv = (nn.Conv2d(conv.in_channels,conv.out_channels,kernel_size=conv.kernel_size,stride=conv.stride,padding=conv.padding,dilation=conv.dilation,groups=conv.groups,bias=True,).requires_grad_(False).to(conv.weight.device))# Prepare filtersw_conv = conv.weight.clone().view(conv.out_channels, -1)# 提取卷积的参数w_bn = torch.diag(bn.weight.div(torch.sqrt(bn.eps + bn.running_var)))# 这里就是计算γ除以根号σ方,再加一个小常数防止分母为0fusedconv.weight.copy_(torch.mm(w_bn, w_conv).view(fusedconv.weight.shape))# 卷积的参数乘上算出来的w_bn,然后再reshape一下# Prepare spatial biasb_conv = torch.zeros(conv.weight.size(0), device=conv.weight.device) if conv.bias is None else conv.biasb_bn = bn.bias - bn.weight.mul(bn.running_mean).div(torch.sqrt(bn.running_var + bn.eps))fusedconv.bias.copy_(torch.mm(w_bn, b_conv.reshape(-1, 1)).reshape(-1) + b_bn)# 计算偏差,跟上面差不多,具体原理可以见blogreturn fusedconv

可以看一下pt文件中的结构:
在这里插入图片描述
可以看出在pt中是25层,将C3这种视为一层来看。
再看看导出的onnx文件中的模型结构:
在这里插入图片描述
这图简直没法看,这是因为导出为onnx,它可不认你那套C3了,BottleNeck了的,这里也体现了一个我们之前谈到的问题:模型有多少种格式?他们直接的算子是不互通的,如何让一个框架训练出来的模型能为另一个框架所用?
yolov5导出onnx文件也非常简单,在export文件中有详细的用法简介,可以自行阅读。

模型部署

模型部署分为三部分:预处理,推理,后处理
先定义好Yolo模型类:

#include <fstream>
#include <sstream>
#include <iostream>
#include <opencv2/imgproc.hpp>
#include <opencv2/highgui.hpp>
//#include <cuda_provider_factory.h>
#include <onnxruntime/onnxruntime_cxx_api.h>
#include<iomanip>using namespace std;
using namespace cv;
using namespace Ort;struct Net_config
{float confThreshold; // 置信度阈值,小于阈值认为该框中物体不是这个classfloat nmsThreshold;  // NMS非极大值抑制阈值float objThreshold;  // 物体检测阈值,小于该阈值认为框中没有物体string modelpath;   //模型文件地址
};typedef struct BoxInfo
{float x1;float y1;float x2;float y2;float score;int label;
} BoxInfo;int endsWith(string s, string sub) {return s.rfind(sub) == (s.length() - sub.length()) ? 1 : 0;
}const float anchors_640[3][6] = { {10.0,  13.0, 16.0,  30.0,  33.0,  23.0},{30.0,  61.0, 62.0,  45.0,  59.0,  119.0},{116.0, 90.0, 156.0, 198.0, 373.0, 326.0} };class YOLO
{
public:YOLO(Net_config config);Mat detect(Mat& frame);private:float* anchors;   //anchor框,yolo中预置了640分辨率的anchor尺寸,每两个数表示一个anchor的size,例如(10,13),yolo中有三个尺度的输出,每个尺度的anchor数为3int num_stride; // stride的数量,yolo中有三个尺度的输出,每个尺度的stride为8,16,32int inpWidth; //输入宽度int inpHeight; //输入高度int nout; //输出通道数int num_proposal; //输出的每个proposal的数据数,为85vector<string> class_names; //类别名称int num_class; //类别数量int seg_num_class; //分割类别数量,用不到float confThreshold; // 置信度阈值,小于阈值认为该框中物体不是这个classfloat nmsThreshold; // NMS非极大值抑制阈值float objThreshold; // 物体检测阈值,小于该阈值认为框中没有物体const bool keep_ratio = true; //是否保持原图比例vector<float> input_image_; //输入图像void normalize_(Mat img); //归一化void nms(vector<BoxInfo>& input_boxes); //非极大值抑制Mat resize_image(Mat srcimg, int *newh, int *neww, int *top, int *left); //图像缩放到固定输入尺寸Env env = Env(ORT_LOGGING_LEVEL_ERROR, "yolov5-7"); //初始化环境Ort::Session *ort_session = nullptr; //模型sessionSessionOptions sessionOptions = SessionOptions(); //模型session配置vector<string> input_names; //输入节点名称vector<string> output_names; //输出节点名称vector<vector<int64_t>> input_node_dims; // 输入节点维度vector<vector<int64_t>> output_node_dims; // 输出节点维度
};YOLO::YOLO(Net_config config)
{this->confThreshold = config.confThreshold;this->nmsThreshold = config.nmsThreshold;this->objThreshold = config.objThreshold;string classesFile = "/home/wyq/hobby/model_deploy/onnx/onnxruntime/YoloV5/class.names"; //类别名称文件string model_path = config.modelpath;sessionOptions.SetGraphOptimizationLevel(ORT_ENABLE_BASIC);std::vector<std::string> avaliable_providers = GetAvailableProviders();auto cuda_provider = std::find(avaliable_providers.begin(), avaliable_providers.end(), "CUDA");if(cuda_provider != avaliable_providers.end()){cout<<"cuda provider is available"<<endl;OrtCUDAProviderOptions cuda_options = OrtCUDAProviderOptions{}; //使用cuda推理sessionOptions.AppendExecutionProvider_CUDA(cuda_options);}else{cout<<"cuda provider is not available"<<endl;}ort_session = new Session(env, model_path.c_str(), sessionOptions);size_t numInputNodes = ort_session->GetInputCount();size_t numOutputNodes = ort_session->GetOutputCount();AllocatorWithDefaultOptions allocator;cout<<numInputNodes<<endl;cout<<numOutputNodes<<endl;for (int i = 0; i < numInputNodes; i++) //获取输入节点信息{auto name = ort_session->GetInputNameAllocated(i, allocator);input_names.push_back(string(name.get()));cout<<input_names[i]<<endl;Ort::TypeInfo input_type_info = ort_session->GetInputTypeInfo(i);auto input_tensor_info = input_type_info.GetTensorTypeAndShapeInfo();auto input_dims = input_tensor_info.GetShape();input_node_dims.push_back(input_dims);}for (int i = 0; i < numOutputNodes; i++) //获取输出节点信息{auto name = ort_session->GetOutputNameAllocated(i, allocator);output_names.push_back(string(name.get()));cout<<output_names[i]<<endl;Ort::TypeInfo output_type_info = ort_session->GetOutputTypeInfo(i);auto output_tensor_info = output_type_info.GetTensorTypeAndShapeInfo();auto output_dims = output_tensor_info.GetShape();output_node_dims.push_back(output_dims);}this->inpHeight = input_node_dims[0][2];this->inpWidth = input_node_dims[0][3];this->nout = output_node_dims[0][2];this->num_proposal = output_node_dims[0][1];ifstream ifs(classesFile.c_str());string line;while (getline(ifs, line)) this->class_names.push_back(line);this->num_class = class_names.size();this->anchors = (float*)anchors_640;this->num_stride = 3; //设置stride数量
}

预处理部分即:将输入resize到固定尺寸,并进行归一化。其中归一化部分可以用cuda来实现,速度会快很多,这个后续再讲,现在我们的目的是先run起来

Mat YOLO::resize_image(Mat srcimg, int *newh, int *neww, int *top, int *left)
{int srch = srcimg.rows, srcw = srcimg.cols;*newh = this->inpHeight;*neww = this->inpWidth;Mat dstimg;if (this->keep_ratio && srch != srcw) {float hw_scale = (float)srch / srcw;if (hw_scale > 1) {*newh = this->inpHeight;*neww = int(this->inpWidth / hw_scale);resize(srcimg, dstimg, Size(*neww, *newh), INTER_AREA);*left = int((this->inpWidth - *neww) * 0.5);copyMakeBorder(dstimg, dstimg, 0, 0, *left, this->inpWidth - *neww - *left, BORDER_CONSTANT, 114);}else {*newh = (int)this->inpHeight * hw_scale;*neww = this->inpWidth;resize(srcimg, dstimg, Size(*neww, *newh), INTER_AREA);*top = (int)(this->inpHeight - *newh) * 0.5;copyMakeBorder(dstimg, dstimg, *top, this->inpHeight - *newh - *top, 0, 0, BORDER_CONSTANT, 114);}}else {resize(srcimg, dstimg, Size(*neww, *newh), INTER_AREA);}return dstimg;
}void YOLO::normalize_(Mat img)
{//    img.convertTo(img, CV_32F);int row = img.rows;int col = img.cols;this->input_image_.resize(row * col * img.channels());for (int c = 0; c < 3; c++){for (int i = 0; i < row; i++){for (int j = 0; j < col; j++){float pix = img.ptr<uchar>(i)[j * 3 + 2 - c];this->input_image_[c * row * col + i * col + j] = pix / 255.0;}}}
}

推理部分:

Mat YOLO::detect(Mat& frame)
{int newh = 0, neww = 0, padh = 0, padw = 0;Mat dstimg = this->resize_image(frame, &newh, &neww, &padh, &padw);this->normalize_(dstimg);array<int64_t, 4> input_shape_{ 1, 3, this->inpHeight, this->inpWidth };auto allocator_info = MemoryInfo::CreateCpu(OrtDeviceAllocator, OrtMemTypeDefault);Value input_tensor_ = Value::CreateTensor<float>(allocator_info, input_image_.data(), input_image_.size(), input_shape_.data(), input_shape_.size());//vector<Value> ort_outputs = ort_session->Run(RunOptions{ nullptr }, &input_names[0], &input_tensor_, 1, output_names.data(), output_names.size());const array<const char*,1> input_names_array = { input_names[0].c_str() };const array<const char*,1> output_names_array = { output_names[0].c_str()};vector<Value> ort_outputs = ort_session->Run(RunOptions{ nullptr }, input_names_array.data(), &input_tensor_, 1, output_names_array.data(), output_names_array.size());//输出的组成:每个proposal由5个部分组成,分别是xmin,ymin,xmax,ymax,box_score,然后是类别的score,一共80个类别,所以一共85个值,/generate proposalsvector<BoxInfo> generate_boxes; //存储所有的boxfloat ratioh = (float)frame.rows / newh, ratiow = (float)frame.cols / neww; //计算原图和resize后图像的比例,用于将box坐标映射到原图const float* pdata = ort_outputs[0].GetTensorMutableData<float>();for (int n = 0; n < this->num_stride; n++)   {const float stride = pow(2, n + 3); //计算stride步长,不同的尺度对应不同的strideint num_grid_x = (int)ceil((this->inpWidth / stride)); //计算x方向的网格数量int num_grid_y = (int)ceil((this->inpHeight / stride)); //计算y方向的网格数量for (int q = 0; q < 3; q++)    ///anchor,每个尺度有三个anchor{const float anchor_w = this->anchors[n * 6 + q * 2]; //计算anchor的宽度const float anchor_h = this->anchors[n * 6 + q * 2 + 1]; //计算anchor的高度for (int i = 0; i < num_grid_y; i++) //遍历y方向的网格{for (int j = 0; j < num_grid_x; j++) //遍历x方向的网格{float box_score = pdata[4]; //输出的第四个值是box的置信度if (box_score > this->objThreshold) //如果置信度大于阈值,才认为检测到了物体{int max_ind = 0;float max_class_socre = 0;for (int k = 0; k < num_class; k++) //遍历80个类别,找到最大的类别得分{if (pdata[k + 5] > max_class_socre){max_class_socre = pdata[k + 5];max_ind = k;}}max_class_socre *= box_score; //类别得分乘以box的置信度,得到最终的得分if (max_class_socre > this->confThreshold) //如果最终得分大于阈值,才认为检测到了物体,还原box坐标到原图{ float cx = (pdata[0] * 2.f - 0.5f + j) * stride;  ///cxfloat cy = (pdata[1] * 2.f - 0.5f + i) * stride;   ///cyfloat w = powf(pdata[2] * 2.f, 2.f) * anchor_w;   ///wfloat h = powf(pdata[3] * 2.f, 2.f) * anchor_h;  ///hfloat xmin = (cx - padw - 0.5 * w)*ratiow;float ymin = (cy - padh - 0.5 * h)*ratioh;float xmax = (cx - padw + 0.5 * w)*ratiow;float ymax = (cy - padh + 0.5 * h)*ratioh;generate_boxes.push_back(BoxInfo{ xmin, ymin, xmax, ymax, max_class_socre, max_ind });}}pdata += nout; //移动到下一个proposal}}}}// Perform non maximum suppression to eliminate redundant overlapping boxes with// lower confidencesnms(generate_boxes);for (size_t i = 0; i < generate_boxes.size(); ++i) //画框{int xmin = int(generate_boxes[i].x1);int ymin = int(generate_boxes[i].y1);rectangle(frame, Point(xmin, ymin), Point(int(generate_boxes[i].x2), int(generate_boxes[i].y2)), Scalar(0, 0, 255), 2);string label = format("%.2f", generate_boxes[i].score);label = this->class_names[generate_boxes[i].label] + ":" + label;putText(frame, label, Point(xmin, ymin - 5), FONT_HERSHEY_SIMPLEX, 0.75, Scalar(0, 255, 0), 1);}return frame; //返回画好框的图像,其实不用返回也可以,因为是引用传递
}

后处理nms:

void YOLO::nms(vector<BoxInfo>& input_boxes)
{sort(input_boxes.begin(), input_boxes.end(), [](BoxInfo a, BoxInfo b) { return a.score > b.score; }); //按照score降序排列vector<float> vArea(input_boxes.size()); //存储每个box的面积for (int i = 0; i < int(input_boxes.size()); ++i){vArea[i] = (input_boxes.at(i).x2 - input_boxes.at(i).x1 + 1)* (input_boxes.at(i).y2 - input_boxes.at(i).y1 + 1);}vector<bool> isSuppressed(input_boxes.size(), false); //存储每个box是否被抑制for (int i = 0; i < int(input_boxes.size()); ++i) //遍历所有box{if (isSuppressed[i]) { continue; }for (int j = i + 1; j < int(input_boxes.size()); ++j) //计算当前box与其它box的IOU{if (isSuppressed[j]) { continue; }float xx1 = (max)(input_boxes[i].x1, input_boxes[j].x1);float yy1 = (max)(input_boxes[i].y1, input_boxes[j].y1);float xx2 = (min)(input_boxes[i].x2, input_boxes[j].x2);float yy2 = (min)(input_boxes[i].y2, input_boxes[j].y2);float w = (max)(float(0), xx2 - xx1 + 1);float h = (max)(float(0), yy2 - yy1 + 1);float inter = w * h;float ovr = inter / (vArea[i] + vArea[j] - inter);if (ovr >= this->nmsThreshold){isSuppressed[j] = true; //抑制IOU大于阈值的box,也就是这个box和box[i]重叠度很高}}}// return post_nms;int idx_t = 0;input_boxes.erase(remove_if(input_boxes.begin(), input_boxes.end(), [&idx_t, &isSuppressed](const BoxInfo& f) { return isSuppressed[idx_t++]; }), input_boxes.end());//这里用到了C++11中的新特性lambda,匿名函数,可以自己去了解一下,推荐深入理解C++11:C++11新特性解析与应用这本书,对于C++11讲解的很好。
}

主函数:

int main()
{Net_config yolo_nets = { 0.3, 0.5, 0.3,"/home/wyq/hobby/model_deploy/onnx/onnxruntime/YoloV5/build/yolov5s.onnx" };YOLO yolo_model(yolo_nets);Mat srcimg;// VideoCapture cap("/home/wyq/hobby/model_deploy/video.mp4");VideoCapture cap=VideoCapture(0);cap.set(CAP_PROP_FOURCC, VideoWriter::fourcc('M', 'J', 'P', 'G'));cap.set(CAP_PROP_FRAME_WIDTH, 640);cap.set(CAP_PROP_FRAME_HEIGHT, 480);cap.set(CAP_PROP_FPS, 60);while(true){double inference_time = 0;double fps = 0.0;cap >> srcimg;if(srcimg.empty()){cout<<"can not load image"<<endl;break;}double begin = static_cast<double>(getTickCount());yolo_model.detect(srcimg);inference_time = (static_cast<double>(getTickCount()) - begin) / getTickFrequency();cout<<"inference time:"<<inference_time<<endl;fps = 1.0f / inference_time;putText(srcimg, "FPS:"+to_string(fps), Point(16, 32),FONT_HERSHEY_COMPLEX, 0.8, Scalar(0, 0, 255));imshow("yolo", srcimg);cout<<"fps:"<<fps<<endl;if(waitKey(1) == 27)break;}
}

本文用到的模型文件以及标签文件都在下面链接:
链接:https://pan.baidu.com/s/1agn2iAPqcs0f5wFLw7hhew?pwd=byvw
提取码:byvw

这篇关于深度学习模型部署(三)Onnxruntime部署yolov5实战的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!


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

相关文章

Spring Boot + MyBatis Plus 高效开发实战从入门到进阶优化(推荐)

《SpringBoot+MyBatisPlus高效开发实战从入门到进阶优化(推荐)》本文将详细介绍SpringBoot+MyBatisPlus的完整开发流程,并深入剖析分页查询、批量操作、动... 目录Spring Boot + MyBATis Plus 高效开发实战:从入门到进阶优化1. MyBatis

SpringCloud动态配置注解@RefreshScope与@Component的深度解析

《SpringCloud动态配置注解@RefreshScope与@Component的深度解析》在现代微服务架构中,动态配置管理是一个关键需求,本文将为大家介绍SpringCloud中相关的注解@Re... 目录引言1. @RefreshScope 的作用与原理1.1 什么是 @RefreshScope1.

MyBatis 动态 SQL 优化之标签的实战与技巧(常见用法)

《MyBatis动态SQL优化之标签的实战与技巧(常见用法)》本文通过详细的示例和实际应用场景,介绍了如何有效利用这些标签来优化MyBatis配置,提升开发效率,确保SQL的高效执行和安全性,感... 目录动态SQL详解一、动态SQL的核心概念1.1 什么是动态SQL?1.2 动态SQL的优点1.3 动态S

Pandas使用SQLite3实战

《Pandas使用SQLite3实战》本文主要介绍了Pandas使用SQLite3实战,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学... 目录1 环境准备2 从 SQLite3VlfrWQzgt 读取数据到 DataFrame基础用法:读

Java的IO模型、Netty原理解析

《Java的IO模型、Netty原理解析》Java的I/O是以流的方式进行数据输入输出的,Java的类库涉及很多领域的IO内容:标准的输入输出,文件的操作、网络上的数据传输流、字符串流、对象流等,这篇... 目录1.什么是IO2.同步与异步、阻塞与非阻塞3.三种IO模型BIO(blocking I/O)NI

tomcat多实例部署的项目实践

《tomcat多实例部署的项目实践》Tomcat多实例是指在一台设备上运行多个Tomcat服务,这些Tomcat相互独立,本文主要介绍了tomcat多实例部署的项目实践,具有一定的参考价值,感兴趣的可... 目录1.创建项目目录,测试文China编程件2js.创建实例的安装目录3.准备实例的配置文件4.编辑实例的

SpringBoot配置Ollama实现本地部署DeepSeek

《SpringBoot配置Ollama实现本地部署DeepSeek》本文主要介绍了在本地环境中使用Ollama配置DeepSeek模型,并在IntelliJIDEA中创建一个Sprin... 目录前言详细步骤一、本地配置DeepSeek二、SpringBoot项目调用本地DeepSeek前言随着人工智能技

Python 中的异步与同步深度解析(实践记录)

《Python中的异步与同步深度解析(实践记录)》在Python编程世界里,异步和同步的概念是理解程序执行流程和性能优化的关键,这篇文章将带你深入了解它们的差异,以及阻塞和非阻塞的特性,同时通过实际... 目录python中的异步与同步:深度解析与实践异步与同步的定义异步同步阻塞与非阻塞的概念阻塞非阻塞同步

基于Flask框架添加多个AI模型的API并进行交互

《基于Flask框架添加多个AI模型的API并进行交互》:本文主要介绍如何基于Flask框架开发AI模型API管理系统,允许用户添加、删除不同AI模型的API密钥,感兴趣的可以了解下... 目录1. 概述2. 后端代码说明2.1 依赖库导入2.2 应用初始化2.3 API 存储字典2.4 路由函数2.5 应

通过Docker Compose部署MySQL的详细教程

《通过DockerCompose部署MySQL的详细教程》DockerCompose作为Docker官方的容器编排工具,为MySQL数据库部署带来了显著优势,下面小编就来为大家详细介绍一... 目录一、docker Compose 部署 mysql 的优势二、环境准备与基础配置2.1 项目目录结构2.2 基