当前位置: 首页 > news >正文

腾讯TNN神经网络推理框架手动实现多设备单算子卷积推理

在这里插入图片描述

文章目录

    • 前言
    • 1. 简介
    • 2. 快速开始
      • 2.1 onnx转tnn
      • 2.2 编译目标平台的 TNN 引擎
      • 2.3 使用编译好的 TNN 引擎进行推理
    • 3. 手动实现单算子卷积推理(浮点)
    • 4. 代码解析
      • 4.1 构建模型(单卷积层)
      • 4.2 构建解释器
      • 4.3 初始化tnn
    • 5. 模型量化
      • 5.1 编译量化工具
      • 5.2 量化scale的计算
      • 5.3 量化流程
    • 6. im2col实现卷积计算
      • 6.1 input为单通道,weight为单通道(输出)
      • 6.2 input为多通道,weight为单通道(输出)
      • 6.3 input为多通道,weight为多通道(输出)
    • 结束语

前言

  近期调研了一下腾讯的TNN神经网络推理框架,因此这篇博客主要介绍一下TNN的基本架构、模型量化以及手动实现x86arm设备上单算子卷积推理。

1. 简介

  TNN是由腾讯优图实验室开源的高性能、轻量级神经网络推理框架,同时拥有跨平台、高性能、模型压缩、代码裁剪等众多突出优势。TNN框架在原有Rapidnetncnn框架的基础上进一步加强了移动端设备的支持以及性能优化,同时借鉴了业界主流开源框架高性能和良好拓展性的特性,拓展了对于后台X86NV GPU的支持。手机端TNN已经在手机QQ、微视、P图等众多应用中落地,服务TNN作为腾讯云AI基础加速框架已为众多业务落地提供加速支持。

在这里插入图片描述

  TNN开源地址:https://github.com/Tencent/TNN

2. 快速开始

2.1 onnx转tnn

在这里插入图片描述

 &emsp目前 TNN 支持业界主流的模型文件格式,包括ONNXPyTorchTensorFlowTesorFlow-Lite 以及 Caffe 等。如上图所示,TNNONNX 作为中间层,借助于ONNX 开源社区的力量,来支持多种模型文件格式。如果要将PyTorchTensorFlow 以及 Caffe 等模型文件格式转换为 TNN,首先需要使用对应的模型转换工具,统一将各种模型格式转换成为 ONNX 模型格式,然后将 ONNX 模型转换成 TNN 模型。

在这里插入图片描述

  为了简化 convert2tnn转换工具的安装和编译步骤,官方推荐使用docker镜像:

# 建议直接从 docker hub 上拉取镜像
docker pull ccr.ccs.tencentyun.com/qcloud/tnn-convert# 对 docker 镜像进行重命名
docker tag ccr.ccs.tencentyun.com/qcloud/tnn-convert tnn-convert:latest
docker rmi ccr.ccs.tencentyun.com/qcloud/tnn-convert# 通过打印 convert2tnn 的帮助信息来验证下 docker 镜像能够正常使用
docker run -it tnn-convert:latest python3 ./converter.py -h

在这里插入图片描述
  进一步的,查看下ONNXTNN工具:

docker run -it tnn-convert:latest python3 ./converter.py onnx2tnn -h

在这里插入图片描述

  具体参数不再进行过多详述,可参阅官方文档。

  本例就以Resnet50为例,将其转为tnn格式:

import torch
from torchvision.models.resnet import resnet50if __name__ == '__main__':model = resnet50()model.load_state_dict(torch.load('model/resnet50-0676ba61.pth'))model.eval()input_data = torch.randn(size=(1, 3, 224, 224), dtype=torch.float32)input_names, output_names = ["input"], ["output"]torch.onnx.export(model, input_data, "model/resnet50.onnx", input_names=input_names, output_names=output_names)
# 当然,也可以直接使用onnx格式的resnet50,下载链接为:https://github.com/onnx/models/tree/main/vision/classification/resnet/model
# 启动docker
docker run -v /home/liyanpeng/tnn_docker:/home/liyanpeng/tnn_docker --rm -it tnn-convert:latest /bin/bash
# cd /opt/TNN/tools/convert2tnn(default)
# onnx2tnn
python3 ./converter.py onnx2tnn /home/liyanpeng/tnn_docker/model/resnet50.onnx -in input:1,3,224,224

在这里插入图片描述

2.2 编译目标平台的 TNN 引擎

  编译相关注意事项请参考官方文档。

  arm-linux平台编译:

apt-get install g++-aarch64-linux-gnu gcc-aarch64-linux-gnu
apt-get install g++-arm-linux-gnueabihf gcc-arm-linux-gnueabihf
# apt-get install vim gdbcd scripts
./build_aarch_linux.sh

  x86-linux平台编译:

cd scripts
./build_linux_native.sh

在这里插入图片描述

2.3 使用编译好的 TNN 引擎进行推理

  上面那个没有编译具体的实例,接下来编译x86平台下各任务下的TNN引擎:

# x86平台编译
cd examples/linux/x86
./build_linux_native.sh# arm-linux交叉编译
# cd examples/linux/cross
# ./build_aarch64_linux.sh

在这里插入图片描述

在这里插入图片描述
  执行图像分类任务:

./demo_x86_imageclassify -p /home/liyanpeng/tnn_docker/model/resnet50.tnnproto -m /home/liyanpeng/tnn_docker/model/resnet50.
tnnmodel -i /home/liyanpeng/tnn_docker/model/tiger_cat.jpg

  推理结果也是正确的:

在这里插入图片描述

  各任务源码位置:examples/linux/src

3. 手动实现单算子卷积推理(浮点)

  TNN框架构建神经网络推理实例需要输入两个文件,一个是模型结构文件.tnnproto,一个是模型权重文件.tnnmodel,这两个文件是必须的。但由于一些特殊的需要,这种文件的方式不太适用,因此我这里提供了一个手动创建模型结构的实例,不用依赖于模型文件。

  仿照examples/linux/src目录下的TNNImageClassify图像分类demo,我在根目录下创建了一个my_cnn_model目录,其中包括my_conv.cppCMakeLists.txt两个文件。

  my_conv.cpp文件内容如下:

// Author:   xiayouran
// Email:    youran.xia@foxmail.com
// Datetime: 2023/4/8 15:17
// Filename: my_conv.cpp
#include "tnn/core/tnn.h"
#include "tnn/interpreter/abstract_model_interpreter.h"
#include "tnn/interpreter/tnn/model_interpreter.h"using namespace TNN_NS;int main(int argc, char* argv[]) {auto model_type = MODEL_TYPE_TNN;auto device_type = DEVICE_X86;auto data_type = DATA_TYPE_FLOAT;ModelConfig model_config;model_config.model_type = model_type;NetworkConfig net_config;net_config.device_type = device_type;TNN tnn;Status status = tnn.MyInit(model_config);auto instance = tnn.CreateInst(net_config, status);BlobMap input_blobs;status = instance->GetAllInputBlobs(input_blobs);Blob* input_blob = input_blobs.begin()->second;float* data_ptr = static_cast<float*>(input_blob->GetHandle().base);for (int i = 0; i < 1 * 1 * 4 * 4; i++) {data_ptr[i] = (float)1.0 + i;}status = instance->Forward();BlobMap output_blobs;status = instance->GetAllOutputBlobs(output_blobs);Blob* output_blob = output_blobs.begin()->second;float* out_data_ptr = static_cast<float*>(output_blob->GetHandle().base);for (int i = 0; i < 1 * 1 * 2 * 2; i++) {std::cout << out_data_ptr[i] << std::endl;}return 0;
}

  卷积的输入shape(1, 1, 4, 4),卷积的shape(1, 1, 3, 3),卷积的输出shape(1, 1, 2, 2),具体为:

在这里插入图片描述
  运行结果如下:

在这里插入图片描述
  在CMakeLists.txt文件中除了添加了本示例代码my_conv.cpp,还添加了官方提供的图像分类demoTNNImageClassify.cc及其依赖,具体内容如下:

file(GLOB MyCNNModel_SRCS my_conv.cpp)
file(GLOB ImageClassify_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/../examples/linux/src/TNNImageClassify/TNNImageClassify.cc)message(${MyCNNModel_SRCS})
message(${ImageClassify_SRCS})#include_directories(../include)
#include_directories(../source)include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../examples/base)
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../examples/base/utils)
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../examples/utils)
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../third_party/gflags ${CMAKE_CURRENT_SOURCE_DIR}/../third_party/gflags)
get_target_property(GFLAGS_INCLUDE_DIRS gflags INTERFACE_INCLUDE_DIRECTORIES)
include_directories(BEFORE "${GFLAGS_INCLUDE_DIRS}")
link_libraries(gflags)
file(GLOB FLAG_SRC "${CMAKE_CURRENT_SOURCE_DIR}/../examples/linux/src/*.cc")
file(GLOB_RECURSE BASE_SRC"${CMAKE_CURRENT_SOURCE_DIR}/../examples/base/*.cc""${CMAKE_CURRENT_SOURCE_DIR}/../examples/base/utils/*.cc")
file(GLOB_RECURSE UTIL_SRC"${CMAKE_CURRENT_SOURCE_DIR}/../examples/utils/*.cc")include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../source/tnn/interpreter/tnn)
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../third_party/stb)add_executable(my_conv_cmd ${MyCNNModel_SRCS})
add_executable(demo_x86_imageclassify_cmd ${ImageClassify_SRCS} ${BASE_SRC} ${UTIL_SRC} ${FLAG_SRC})target_link_libraries(my_conv_cmd TNN)
target_link_libraries(demo_x86_imageclassify_cmd TNN)set_target_properties(my_conv_cmd PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR})
set_target_properties(demo_x86_imageclassify_cmd PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR})

4. 代码解析

  按照官方提供的API说明,运行一个神经网络需要五个步骤:

# Step1. 模型解析
model_config.params.push_back(proto_buffer);# proto文件内容存入proto_buffer
model_config.params.push_back(model_buffer);# model文件内容存入model_buffer
Status ret = tnn.Init(model_config);# Step2. 网络构建
auto net_instance = tnn.CreateInst(config, status);# Step3. 输入设定
auto status = net_instance->SetInputMat(input_mat, input_cvt_param);# Step4. 网络运行
auto status = net_instance->Forward();# Step5. 输出获取
auto status = instance->GetOutputMat(output_mat);

  在第一步模型解析中涉及到文件操作,理论上只要按照其他模型转tnn的格式写模型文件是不需要修改源码的,这里没有阅读这部分源码,因此就直接修改了源码。
  经过源码分析,手动构建一个模型主要需要构建神经网络模型的各层layer并完成参数的初始化、模型解释器及tnn的初始化的构建,具体如下:

4.1 构建模型(单卷积层)

  在source/tnn/interpreter/tnn/model_interpreter.cc文件中新增了ModelInterpreter::MyInterpret()函数,区别于官方的ModelInterpreter::Interpret(std::vector<std::string> &params)函数,本函数不需要从文件中去解析模型的结构和权重:

// Interpret the proto and model without file.
Status ModelInterpreter::MyInterpret() {Status status = TNN_OK;/****************初始化卷积层参数****************/NetStructure *structure = GetNetStructure();structure->source_model_type = MODEL_TYPE_TNN;DimsVector &input_shape = structure->inputs_shape_map["input"];input_shape.push_back(1);input_shape.push_back(1);input_shape.push_back(4);input_shape.push_back(4);DataType data_type = DATA_TYPE_FLOAT;// DATA_TYPE_FLOATstructure->input_data_type_map["input"] = data_type;structure->outputs.insert("output");auto cur_layer = std::make_shared<LayerInfo>();std::string type_str = "Convolution";type_str = Transfer(type_str);LayerType type = GlobalConvertLayerType(type_str);cur_layer->type = type;cur_layer->type_str = type_str;cur_layer->name = Transfer("Conv_0");cur_layer->inputs.clear();cur_layer->outputs.clear();cur_layer->inputs.push_back("input");structure->blobs.insert("input");cur_layer->outputs.push_back("output");structure->blobs.insert("output");LayerParam *layer_param = NULL;LayerParam** param = &layer_param;auto p = CreateLayerParam<ConvLayerParam>(param);p->input_channel = 1;p->output_channel = 1;p->kernels = {3, 3};p->strides = {1, 1};p->pads = {0, 0, 0, 0};p->dialations = {1, 1};p->bias = 0;p->pad_type = -1;p->group = 1;p->activation_type = 0;layer_param->type = cur_layer->type_str;layer_param->name = cur_layer->name;if (data_type == DATA_TYPE_INT8) {layer_param->quantized = true;}cur_layer->param = shared_ptr<LayerParam>(layer_param);structure->layers.push_back(cur_layer);/**************卷积层参数初始化结束**************//****************初始化卷积层权重****************/NetResource *net_resource = GetNetResource();LayerResource *layer_resource = NULL;LayerResource** resource = &layer_resource;auto layer_res = CreateLayerRes<ConvLayerResource>(resource);layer_res->filter_format = OIHW;// weightRawBuffer weight_buf;DimsVector weight_dims = {1, 1, 3, 3};weight_buf = TNN_NS::RawBuffer(1*1*3*3*4);weight_buf.SetDataType(data_type);weight_buf.SetBufferDims(weight_dims);float weight_data[1][1][3][3] = {{{{1.0, 0.0, 0.0},{0.0, 1.0, 0.0},{0.0, 0.0, 1.0}}}};memcpy(weight_buf.force_to<float*>(), weight_data, 1*1*3*3*4);layer_res->filter_handle = weight_buf;// biasRawBuffer bias_buf;DimsVector bias_dims = {1};bias_buf = TNN_NS::RawBuffer(1);bias_buf.SetDataType(data_type);bias_buf.SetBufferDims(bias_dims);float bias_data[1] = {0.0};memcpy(bias_buf.force_to<float*>(), bias_data, 1*4);layer_res->bias_handle = bias_buf;// scaleRawBuffer scale_buf;DimsVector scale_dims = {1};scale_buf = TNN_NS::RawBuffer(1);scale_buf.SetDataType(DATA_TYPE_FLOAT);scale_buf.SetBufferDims(scale_dims);float scale_data[1] = {1.0};memcpy(scale_buf.force_to<float*>(), scale_data, 1*4);layer_res->scale_handle = scale_buf;// zero_pointRawBuffer zero_point_buf;DimsVector zero_point_dims = {1};zero_point_buf = TNN_NS::RawBuffer(1);zero_point_buf.SetDataType(DATA_TYPE_INT8);zero_point_buf.SetBufferDims(zero_point_dims);int zero_point_data[1] = {0};memcpy(zero_point_buf.force_to<int*>(), zero_point_data, 1*4);layer_res->zero_point_handle = zero_point_buf;net_resource->resource_map["Conv_0"] = std::shared_ptr<LayerResource>(layer_resource);// 不用解析constant_map/**************卷积层权重初始化结束**************/return status;
}

  相应的,需要在source/tnn/interpreter/tnn/model_interpreter.hsource/tnn/interpreter/abstract_model_interpreter.hsource/tnn/interpreter/ncnn/ncnn_model_interpreter.h三个文件中添加本函数的声明:

// model_interpreter.h文件中的ModelInterpreter
virtual Status MyInterpret();// abstract_model_interpreter.h文件中的AbstractModelInterpreter
virtual Status MyInterpret() = 0;// ncnn_model_interpreter.h文件中的NCNNModelInterpreter
virtual Status MyInterpret();

4.2 构建解释器

  在source/tnn/core/tnn_impl_default.cc文件中新增了TNNImplDefault::MyInit(ModelConfig& config)函数,函数实现大体与官方的TNNImplDefault::Init(ModelConfig& config)函数一样,只不过这里构建解释器时使用了MyInterpret()函数:

Status TNNImplDefault::MyInit(ModelConfig& config) {auto status = TNNImpl::MyInit(config);if (status != TNN_OK) {return status;}auto interpreter = CreateModelInterpreter(config.model_type);if (!interpreter) {return Status(TNNERR_NET_ERR, "interpreter is nil");}interpreter_ = std::shared_ptr<AbstractModelInterpreter>(interpreter);return interpreter_->MyInterpret();
}

  TNNImpl::MyInit(config)函数的实现在在source/tnn/core/tnn_impl.cc文件中:

Status TNNImpl::MyInit(ModelConfig &config) {model_config_.model_type = config.model_type;return TNN_OK;
}

  相应的,需要在source/tnn/core/tnn_impl_default.hsource/tnn/core/tnn_impl.h两个文件中添加本函数的声明:

// tnn_impl_default.h文件中的MyInit
virtual Status MyInit(ModelConfig& config);// tnn_impl.h文件中的MyInit
virtual Status MyInit(ModelConfig& config);

4.3 初始化tnn

  为了使tnn能够正确按照我们的方法进行初始化,需要添加TNN::MyInit(ModelConfig& config)函数以代替官方的TNN::Init(ModelConfig& config)函数进行初始化,具体在source/tnn/core/tnn.cc文件中:

Status TNN::MyInit(ModelConfig& config) {impl_ = TNNImplManager::GetTNNImpl(config.model_type);if (!impl_) {LOGE("Error: not support mode type: %d. If TNN is a static library, link it with option -Wl,--whole-archive tnn -Wl,--no-whole-archive on android or add -force_load on iOS\n", config.model_type);return Status(TNNERR_NET_ERR, "unsupported mode type, If TNN is a static library, link it with option -Wl,--whole-archive tnn -Wl,--no-whole-archive on android or add -force_load on iOS");}return impl_->MyInit(config);
}

  相应的,需要在include/tnn/core/tnn.h文件中添加本函数的声明:

// tnn.h文件中的MyInit
Status MyInit(ModelConfig& config);

  至此,手动构建单算子卷积推理所需的要素已经构建完毕,在根目录下的CMakeLists.txt文件中添加本示例的代码目录进行编译即可:

add_subdirectory(my_cnn_model)

5. 模型量化

5.1 编译量化工具

# 编译
cd platforms/linux/
./build_quanttool.sh -c# 执行量化
cd build_quantize/
./quantization_cmd -p /home/liyanpeng/tnn_docker/model/resnet50.tnnproto -m /home/liyanpeng/tnn_docker/model/resnet50.tnnmodel -i /home/liyanpeng/tnn_docker/imagenet128/ -o resnet50

在这里插入图片描述
  浮点模型大小为98M,量化后的定点模型为26M
在这里插入图片描述

在这里插入图片描述

  使用量化模型进行推理:

./demo_x86_imageclassify -p /opt/TNN/platforms/linux/build_quantize/resnet50.quantized.tnnproto -m /opt/TNN/platforms/linux/build_quantize/resnet50.quantized.tnnmodel -i /home/liyanpeng/tnn_docker/model/tiger_cat.jpg

  这里只是用128张图片进行的量化,所以精度损失较大,推理结果不大对:

在这里插入图片描述

5.2 量化scale的计算

  这部分主要涉及到量化过程中个scale的计算,包括feature mapscale和算子权重的scale
  feature mapscale计算如下:

float max_val     = std::max(std::abs(range_per_channel_[i].first), std::abs(range_per_channel_[i].second));
valid_channel_[i] = max_val > 0.00001;
if (valid_channel_[i]) {interval_per_channel_[i] = (float)bin_nums_ / max_val;
}int index = static_cast<int>(std::abs(val) * interval_per_channel_[channel_idx]);
index     = std::min(index, bin_nums_ - 1);
distribute_data[index] += 1.0;
// 相当于是统计数据,2048个bar,每个bar中有多少个数据
// 即将feature map中的数据分到2048个bar中// 对于MIN_MAX来说 threshold 为 2047
scale_vec = ((float)threshold + 0.5) / interval / 127.0;

  weightscale计算如下:

// weight_multiby_inputscale[idx] = weight_data[idx] * input_scale_data[s_idx];
// 下面的weight已经是weight*input_scale
auto minmax               = std::minmax_element(weight_start, weight_start + s_size);
float max_val_abs         = std::max(std::abs(*minmax.first), std::abs(*minmax.second));weight_scale[s_idx]    = max_val_abs / 127.0f;
/*
s_size为in_c*k_w*k_h = 3*3*3=27
*/
scale_float2int8 = 1 / weight_scale[s_idx];// quantize weights
for (int i = 0; i < s_size; ++i) {int value         = static_cast<int>(std::round(weight_start[i] * scale_float2int8));weight_q_start[i] = std::min(127, std::max(-127, value));
}// quantize bias
bias_quantized_data[oc] = static_cast<int32_t>(bias_data[oc] / weight_scale_data[weight_scale_idx]);

5.3 量化流程

  TNN默认采用Min-Max量化方式,除此之外,feature map支持KL量化方法,weight支持ADMM量化方法,具体的量化流程如下:

calibration.Init(net_config, model_config)
/*根据输入shape,计算出每个网络层的输出shape*/calibration.SetCalibrationParams(cali_params)
/*设置量化方式为MIN_MAX*/calibration.RunCalibration(dataset)
/*scale计算和量化*/CalBlobScale(dataset);// Compute Feature ScaleInitFeatureMap();// Init Feature map(在此之前进行了reshape),初始化每个feature map的range_per_channel_等参数UpdateBlobRange(dataset);// Collect the Range of Feature map,更新range_per_channel_UpdateRange()UpdateBlobDistribute(dataset);// Calculate Distribute of Feature mapResetDistribute()// 根据range_per_channel_计算valid_channel_和interval_per_channel_,并初始化distribute_per_channel_UpdateDistribute()// CalculateScale(scale_vec, zero_point_vec);// Compute Scale of Feature map and save to resource mapQuantizeParams();// Quantize paramsMergeBlobScale();// Merge Blob Scale of some layerscalibration.Serialize(output_name + ".quantized.tnnproto", output_name + ".quantized.tnnmodel")
/*保存量化模型*/

  其中range_per_channel_表示每个channel中的最大最小值:first(min)second(max)

  量化源码位置在:tools/quantization

6. im2col实现卷积计算

  根据硬件具体实现,大部分卷积的计算都会转换为矩阵乘法(GEMM),最常用的方法就是im2col,下面给出一些im2col实现卷积计算的示例图,结合这篇博客一起食用效果更佳!

6.1 input为单通道,weight为单通道(输出)

在这里插入图片描述

6.2 input为多通道,weight为单通道(输出)

在这里插入图片描述

6.3 input为多通道,weight为多通道(输出)

在这里插入图片描述

结束语

  本篇博客主要介绍了TNN的基本使用、量化工具的使用以及手动实现单算子卷积推理,除了浮点卷积推理外,8bit定点卷积推理也有实现,不过目前的结果还没有对上,后续再进行补充8bit定点卷积推理的实现代码。

相关文章:

腾讯TNN神经网络推理框架手动实现多设备单算子卷积推理

文章目录前言1. 简介2. 快速开始2.1 onnx转tnn2.2 编译目标平台的 TNN 引擎2.3 使用编译好的 TNN 引擎进行推理3. 手动实现单算子卷积推理(浮点)4. 代码解析4.1 构建模型(单卷积层)4.2 构建解释器4.3 初始化tnn5. 模型量化5.1 编译量化工具5.2 量化scale的计算5.3 量化流程6. i…...

基础解惑:Linux 下文件描述符标志和文件状态标志区别

简述 文件描述符标志&#xff0c;是体现进程的文件描述符的状态&#xff0c;fork进程时&#xff0c;文件描述符被复制&#xff1b;目前只有一种文件描述符&#xff1a;FD_CLOEXEC文件状态标志&#xff0c;是体现进程打开文件的一些标志&#xff0c;fork时不会复制file 结构&am…...

学弟:如何在3个月内学会自动化测试?

有小学弟问&#xff1a;如何在3个月内学会自动化测试&#xff1f; 老实说如果你现在上班&#xff0c;之前主要在做功能测试&#xff0c;或者编程基础比较弱的话&#xff0c;三个月够呛。 如果你是脱产学习&#xff0c;每天能保持6&#xff5e;8小时学习时间的话&#xff0c;可…...

C-NCAP 2025主动安全ADAS测试研究

中汽中心汽车测评管理中心&#xff08;简称“中汽测评”&#xff09;是负责运营C-NCAP、CCRT等测评项目的管理机构。中汽测评以引领汽车行业进步、支撑汽车强国建设为使命&#xff0c;通过独立、公正、专业、开放的测试评价&#xff0c;服务消费者&#xff0c;当好选车购车参谋…...

【Apifox】测试工具自动编写接口文档

在开发过程中&#xff0c;我们总是避免不了进行接口的测试&#xff0c; 而相比手动敲测试代码&#xff0c;使用测试工具进行测试更为便捷&#xff0c;高效 今天发现了一个非常好用的接口测试工具Apifox 相比于Postman&#xff0c;他还拥有一个非常nb的功能&#xff0c; 在接…...

解决brew安装opencv报错问题

目录1.报错12. 解决方案3. 报错24. 解决方案4.1 原因分析4.2 手动下载portable-ruby-2.6.8_1.el_capitan.bottle.tar.gz4.3 拷贝portable-ruby-2.6.8_1.el_capitan.bottle.tar.gz到指定目录1.报错1 mac本用brew报如下错误&#xff1a; xialiangzhideMacBook-Pro:~ xialz$ bre…...

Linux软件安装---Tomcat安装

安装Tomcat 操作步骤&#xff1a; 使用xftp上传工具将tomcat的 二进制发布包上传到Linux解压安装包&#xff0c;命令为tar -zxvf apache-tomcat*** -C /usr/local进入Tomcat的bin的启动目录&#xff0c;命令为sh startup.sh或者./startup.sh 验证Tomcat启动是否成功&#xff0…...

提示工程师是什么工作?

提示工程师是什么工作&#xff1f; 因为ChatGPT的爆火&#xff0c;大家都把眼光锁定在这个号称“ChatGPT新兴职业” 的“提示工程师”上。“提示工程师”是什么工作&#xff1f;为什么说未来所有职业 都需要提示工程的能力&#xff1f; 先解释一下“提示”&#xff0c;它最早…...

WXSS-WXML-WXS语法

目录&#xff1a; 1 WXSS编写程序样式 2 Mustache语法绑定 3 WXML的条件渲染 4 WXML的列表渲染 5 WXS语法基本使用 6 WXS语法案例练习 小程序的自适应单位rpx。在设计稿为iPhone6的时候1px2rpx wxml必须是闭合标签&#xff0c;或者单标签加/&#xff0c;否则会报错&#…...

POSIX正则表达式

维基百科 POSIX基本表达式 https://en.wikibooks.org/wiki/Regular_Expressions/POSIX_Basic_Regular_Expressions POSIX扩展正则表达式 https://en.wikibooks.org/wiki/Regular_Expressions/POSIX-Extended_Regular_Expressions 正则表达式 https://en.wikipedia.org/wiki/R…...

数据分析工具集合:Tableau入门及其他工具简介

目录 一、Tableau简介 1、下载链接 2、使用技巧 二、其他常用数据分析工具 1、Microsoft Excel简介 1.1、下载链接 1.2、使用技巧 2、Python简介 2.1、下载链接 2.2、常用库的安装方式和使用技巧 2.2.1、Pandas 2.2.2、NumPy 2.2.3、Matplotlib 3、R语言简介 3.…...

响应式布局的五种方法

响应式布局的五种方法1.百分比布局2.rem布局3. 媒体查询 media screen4. flex布局5.vw 和 vh响应式布局是同一页面在不同的屏幕上有不同的布局&#xff0c;即只需要一套代码使页面适应不同的屏幕。 1.百分比布局 1.有父元素就相对于父元素 2.没有父元素就相对于视口的大小 举一…...

Javase学习文档------数组

Java 数组是 Java 编程中非常基础和重要的一个知识点。 以下是 Java 数组的主要学习内容&#xff1a; 数组的几个特点 数组在声明时必须指定长度&#xff0c;且长度不可变&#xff1a;数组的长度在声明时就需要确定&#xff0c;一旦确定就不能修改。因此&#xff0c;在使用数组…...

百度高德地图JS-API学习手记:地图基本设置与省市区数据加载

无论是百度还是高德地图开发&#xff0c;还是高德地图开发。官方的给的案例启示很多&#xff0c;copy再修改下&#xff0c;就完成了 概述-地图 JS API | 高德地图API 地图 JS API | 百度地图API SDK 这个大致看一下&#xff0c;我想。有点GIS基础都能完成地图开发。 个人认…...

c语言—指针详解***内存地址***指针字节数***注意事项

创作不易&#xff0c;本篇文章如果帮助到了你&#xff0c;还请点赞支持一下♡>&#x16966;<)!! 主页专栏有更多知识&#xff0c;如有疑问欢迎大家指正讨论&#xff0c;共同进步&#xff01; 给大家跳段街舞感谢支持&#xff01;ጿ ኈ ቼ ዽ ጿ ኈ ቼ ዽ ጿ ኈ ቼ ዽ ጿ…...

VMware虚拟机之WindowsXP系统超详细下载安装与使用教程

文章目录前言一、WindowsXP虚拟机系统下载二、WindowsXP虚拟机系统安装三、WindowsXP虚拟机系统使用总结前言 本博客的主要内容为使用VMware虚拟机下载安装与使用WindowsXP系统&#xff0c;WindowsXP系统虽然早已过时&#xff0c;但是仍对我们的学习有着很大的帮助&#xff0c;…...

【VMD-SSA-LSSVM】基于变分模态分解与麻雀优化Lssvm的负荷预测【多变量】(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…...

积极心态,助力人生成功

无论生活中遇到多少困难和挫折&#xff0c;只要我们保持积极心态、努力拼搏&#xff0c;就有望最终实现自己的梦想和目标。...

ADRC线性跟踪微分器(ST+SCL语言)

ADRC自抗扰相关算法源代码和公式请参看下面文章链接: ADRC/Matlab一步步实现跟踪微分器TD(附完整PLC测试代码链接)_ladrc线性跟踪微分器差分方程_RXXW_Dor的博客-CSDN博客关于Adrc的理论分析不是本篇博客的重点,主要也是能力所限,相关理论大家可以看韩京清教授的论文,专栏…...

Linux C/C++ 崩溃诊断大师:解锁软件问题定位与修复的秘密武器

让崩溃成为历史&#xff1a;详解有效诊断与解决技巧引言崩溃信息的类型设置信号处理函数&#xff08;Setting up signal handlers&#xff09;信号来源和上下文信息使用 siginfo_t 结构体获取信号来源信息使用 ucontext 结构体获取上下文信息将崩溃信息写入日志标准的信号处理函…...

ChatGPT能代替Oracle DBA吗?用Oracle OCP(1z0-083)的真题测试一下。

让我们来看看ChatGPT不能通过Oracle OCP的考试&#xff1f; 文章目录引言测试过程总结和分析关于博主&#xff0c;姚远&#xff1a;Oracle ACE&#xff08;Oracle和MySQL数据库方向&#xff09;。Oracle MAA 大师。华为云MVP。《MySQL 8.0运维与优化》的作者。拥有 Oracle 10g和…...

《扬帆优配》二季度投资策略出炉 机构调仓换股露踪迹

随着多家上市公司公告发布&#xff0c;其发表的股东数据使得基金的最新持仓浮出水面。与此同时&#xff0c;组织也在密集调研中寻觅出资时机。站在二季度的起点&#xff0c;基金公司二季度出资策略渐次发表。多家基金公司以为&#xff0c;宏观经济将延续修正态势&#xff0c;仍…...

【SpringMVC】2—传统方式实现增删改查

⭐⭐⭐⭐⭐⭐ Github主页&#x1f449;https://github.com/A-BigTree 笔记链接&#x1f449;https://github.com/A-BigTree/Code_Learning ⭐⭐⭐⭐⭐⭐ 如果可以&#xff0c;麻烦各位看官顺手点个star~&#x1f60a; 如果文章对你有所帮助&#xff0c;可以点赞&#x1f44d;…...

图像阈值化

图像阈值化 图像阈值化简介 ⚫ 图像阈值化是图像处理的重要基础部分, 应用很广泛, 可以根据灰度差异来分割图像不同部分 ⚫ 阈值化处理的图像一般为单通道图像(灰度图) ⚫ 阈值化参数的设置可以使用滑动条来debug ⚫ 阈值化处理易光照影响, 处理时应注意 ⚫ 本节主要介绍…...

1.5 极限运算法则

思维导图&#xff1a; 我的理解&#xff1a; 如果一个数列{a_n}是一个无穷小&#xff0c;那么它的极限为0&#xff0c;即lim(n→∞)a_n0。同样地&#xff0c;如果另一个数列{b_n}也是一个无穷小&#xff0c;那么它的极限为0&#xff0c;即lim(n→∞)b_n0。 当我们考虑这两个无…...

首批因AI失业的人出现-某游戏公司裁掉半数原画师

如今各种AI爆火&#xff0c;不可避免的的会与某些功能撞车职业发生冲突&#xff0c;每一次生产力的变革&#xff0c;在带来技术进步与更高效率的同时&#xff0c;也都无可避免的会带来一波失业浪潮&#xff0c;当下的人工智能浪潮自然也不例外。 现在&#xff0c;第一批因为AI…...

字符串转换整数(atoi)

请你来实现一个 myAtoi(string s) 函数&#xff0c;使其能将字符串转换成一个 32 位有符号整数&#xff08;类似 C/C 中的 atoi 函数&#xff09;。 函数 myAtoi(string s) 的算法如下&#xff1a; 读入字符串并丢弃无用的前导空格 检查下一个字符&#xff08;假设还未到字符…...

Servlet练习

练习准备 编写Student和StudentDao package beans;public class Student{private String num;private String name;public Student(){}public String getNum() {return num;}public String getName() {return name;}public void setNum(String num) {this.num num;}public v…...

美国高速公路信号灯控制项目的大致逻辑和步骤 智慧公路设计

美国高速公路信号灯控制项目的大致逻辑和步骤&#xff1a; 美国那边先提供一个关于具体做什么需求、那边的设备&#xff08;信号灯&#xff09;有什么参数&#xff0c;什么接口&#xff0c;分别是什么属性等等的详细设计文档&#xff0c;开发人员拿到这个文档以后把它看懂&…...

数字电源专用IC,国产C2000, QX320F280049

一、特性参数 1、独立双核&#xff0c;32位CPU&#xff0c;单核主频400MHz 2、IEEE 754 单精度浮点单元 &#xff08;FPU&#xff09; 3、三角函数单元 &#xff08;TMU&#xff09; 4、1MB 的 FLASH &#xff08;ECC保护&#xff09; 5、1MB 的 SRAM &#xff08;ECC保护&…...