从零构建深度学习推理框架-6 构建计算图

PNNX

PNNX项目 PyTorch Neural Network eXchange(PNNX)是PyTorch模型互操作性的开放标准。PNNX为PyTorch提供了一种开源的模型格式,它定义了与Pytorch相匹配的数据流图和运算图,我们的框架在PNNX之上封装了一层更加易用和简单的计算图格式。

pytorch训练好一个模型之后,然后模型需要转换到pnnx格式,然后pnnx格式我们再去读取,形成计算图.

PNNX帮我们做了很多的图优化、算子融合的工作,所以底层的用它PNNX的话,我们可以吸收图优化的结果,后面推理更快.

但是不直接在项目中用PNNX,因为别人的工作和自己推理框架开发思路总是有不同的。所以在这上面封装,又快速又好用方便,符合自己的使用习惯。

那该如何将Pytorch导到我们的计算图呢?

https://github.com/Tencent/ncnn/tree/master/tools/pnnx

1 Export your model to TorchScript

import torch
import torchvision.models as models

net = models.resnet18(pretrained=True)
net = net.eval()

x = torch.rand(1, 3, 224, 224)

# You could try disabling checking when tracing raises error
# mod = torch.jit.trace(net, x, check_trace=False)
mod = torch.jit.trace(net, x)

mod.save("resnet18.pt")

2 Convert TorchScript to PNNX

pnnx resnet18.pt inputshape=[1,3,224,224]

之后,我们会得到:

  • resnet18.pnnx.param        PNNX graph definition
  • resnet18.pnnx.bin          PNNX model weight
  • resnet18_pnnx.py            PyTorch script for inference, the python code for model construction and weight initialization
  • resnet18.pnnx.onnx        PNNX model in onnx format
  • resnet18.ncnn.param        ncnn graph definition
  • resnet18.ncnn.bin             ncnn model weight
  • resnet18_ncnn.py                pyncnn script for inference

 

PNNX的格式定义

PNNX由操作数operand(运算数)和operator(运算符号),PNNX::Graph用来管理和操作这两者。

Operand 操作数:

操作数(operand),也可以通过操作数来方向访问到这个数字的产生者和使用者Customer

https://github.com/Tencent/ncnn/blob/master/tools/pnnx/src/ir.h

Operand有以下几个部分组成:

  1. Producer: 类型是operator, 表示产生了这个操作数的运算符(operator). 也就是说这个操作数(operand)是Producer的输出. Producer这个操作符号产生了当前的Operand,Producer是有个Add,Operand就是对应的Add结果。
  2. Customer:类型是operator, 表示需要这个操作数下一个操作的的运算符(operator),也就是说这个操作数(operand)作为Customer的输入存在.

Add-->Conv 1

Add中间是Values --Conv Value就是Operand,Add Operator就是她的Producer(有且只有一个),Conv Operator就是她的消费者Producer,当然消费者可以有很多个。

  1. Name: 类型是std::string, 表示这个操作数的名称.Values
  2. Shape: 类型是std::vector<int> , 用来表示操作数的大小,Operand操作数,Add-->values-->Conv 1,values可能是(1x3x320x320)
class Operand
{
public:
    void remove_consumer(const Operator* c);

    Operator* producer;
    std::vector<Operator*> consumers;

    // 0=null 1=f32 2=f64 3=f16 4=i32 5=i64 6=i16 7=i8 8=u8 9=bool 10=cp64 11=cp128 12=cp32
    int type;
    std::vector<int> shape;

    // keep std::string typed member the last for cross cxxabi compatibility
    std::string name;

    std::map<std::string, Parameter> params;

private:
    friend class Graph;
    Operand()
    {
    }
};

 Operator(操作符):

operator有以下几个部分组成: 可以是一个Conv,也可以是Add

1  Inputs, 类型为std::vector<operand*>, 表示这个运算符计算过程中所需要的输入操作数(operand)

Operator比如是一个Add,Add做需要两个输入才能相加,输入存放在Operand中,operand1和operand2。

Operator是一个Conv, 它只需要一个输入,输入存放在Operand中,operand1.

operand1和operand2之类的输入都存放在inputs结构中。

2 Outputs, 类型为std::vector<operand*>, 表示这个运算符计算过程中得到的输出操作数(operand)

Conv1 operator的输出,输出当然一个Operand类,一个层可能有多个输出,然后用有个一个vector存放。

Conv1层的输出,既要送到Conv2也要送到下面的Conv3

outputs1 = self.conv1(inputs)
# outputs用了两次
output2 = self.conv2(outputs1)
output3 = self.conv2(outputs1)

 

Operator类的结构

  • Type, Name 类型均为std::string, 分别表示运算符号的类型和名称

Conv Operator stride,padding,kernel_size之类的

maxPooling Operator 存放 stride,padding,kernel_size

  • Params, 类型为std::map,用于存放该运算符的所有参数(例如对应Convolution oprator, params中将存放stride, padding, kernel_size等信息)

卷积核,卷积核需要一个参数,这个是从训练中得到的。

  • Attrs,类型为std::map,用于存放运算符号所需要的具体权重属性(例如对应Convolution oprator,它的 attrs中就存放着卷积的权重参数和偏移量参数)
class Operator
{
public:
    std::vector<Operand*> inputs;
    std::vector<Operand*> outputs;

    // keep std::string typed member the last for cross cxxabi compatibility
    std::string type;
    std::string name;

    std::vector<std::string> inputnames;
    std::map<std::string, Parameter> params;
    std::map<std::string, Attribute> attrs;

private:
    friend class Graph;
    Operator()
    {
    }
};

因为不符合框架的使用要求,所以对其重新封装:

对PNNX封装:

1 对Operands(运算数)的封装。

struct RuntimeOperand {
  std::string name; /// 操作数的名称
  std::vector<int32_t> shapes; /// 操作数的形状
  std::vector<std::shared_ptr<Tensor<float>>> datas; /// 存储操作数
  RuntimeDataType type = RuntimeDataType::kTypeUnknown; /// 操作数的类型,一般是float
};

2 对Operator(运算符)本身的封装RuntimeOperator

/// 计算图中的计算节点
struct RuntimeOperator {
  ~RuntimeOperator();
  std::string name; /// 运算符号节点的名称
  std::string type; /// 运算符号节点的类型

  std::shared_ptr<Layer> layer; /// 节点对应的计算Layer

  std::vector<std::string> output_names; /// 运算符号的输出节点名称
// 对PNNX::Operand的一个封装 RuntimeOPerand RuntimeOperator 作为一个操作符号总有输入和输出
  std::shared_ptr<RuntimeOperand> output_operands; /// 运算符号的输出操作数

  std::map<std::string, std::shared_ptr<RuntimeOperand>> input_operands; /// 运算符的输入操作数
  std::vector<std::shared_ptr<RuntimeOperand>> input_operands_seq; /// 运算符的输入操作数,顺序排列
// RuntimeParameterFloat*
// RuntimeParameterIntArray
// kernel_size = {3,3} 一个参数,那他就是需要RuntimeParameterIntArray, "expr" RuntimeParameterString

  std::map<std::string, RuntimeParameter *> params;  /// 算子的参数信息,记录了一个kernel_size,padding之类的信息
  // RuntimeAttribute对PNNX::Attribute的一个封装,也是存放权重的。
std::map<std::string, std::shared_ptr<RuntimeAttribute> > attribute; /// 算子的属性信息,内含权重信息


};

从PNNX计算图到KuiperInfer计算图的过程

在Init函数中

1 、加载PNNX的计算图,PNNX一个加载过程,它加载完毕后,我们将它转换为KuiperInfer::的计算图

param_path 和bin_path里面读取它的网络结构和具体参数 PNNX::Graph存放在这

int load_result = this->graph_->load(param_path_, bin_path_);

这个load还是pnnx里的load

2、获取PNNX计算图中的运算符(operators) 开始自己的转换过程

std::vector<pnnx::Operator *> operators = this->graph_->ops;   // 得到PNNX::opeator
if (operators.empty()) {
    LOG(ERROR) << "Can not read the layers' define";
    return false;
}

3、 遍历PNNX计算图中的运算符,构建KuiperInfer计算图

  // 根据const pnnx::Operator *op 去赋值std::shared_ptr<RuntimeOperator> runtime_operator
  for (const pnnx::Operator *op : operators) {
    if (!op) {
      LOG(ERROR) << "Meet the empty node";
      continue;
    } else {
      // 现在是空的,下面
      std::shared_ptr<RuntimeOperator> runtime_operator = std::make_shared<RuntimeOperator>();
      // 初始化算子的名称
      runtime_operator->name = op->name;
      runtime_operator->type = op->type;

      // 初始化算子中的input,对操作符号operator赋予runtimeoperand作为输入,输入是根据pnnx::operand来的
      const std::vector<pnnx::Operand *> &inputs = op->inputs;
      if (!inputs.empty()) {
        InitInputOperators(inputs, runtime_operator);
      }

      // 记录输出operand中的名称
      // 有一个pnnx::operator 来自与load_graph这个操作
      // load_graph pnnx::operators数组 进行遍历 pnnx::operator
      // 每一个遍历中operator,我们再初始化自己的kuiperinfer::RuntimeOperator

      /// RuntimeOperator根据pnnx::operator赋予inputs和outputs
      const std::vector<pnnx::Operand *> &outputs = op->outputs;
      if (!outputs.empty()) {
        InitOutputOperators(outputs, runtime_operator);
      }

      // 初始化算子中的attribute(权重)
      //没一个pnnx::operator里面有一个权重,我们根据pnnx::Attr这个权重去初始化RuntimeAttr
      /// 初始化RutimeAttr之后呢,存放在runtime_operator
      const std::map<std::string, pnnx::Attribute> &attrs = op->attrs;
      if (!attrs.empty()) {
        InitGraphAttrs(attrs, runtime_operator);
      }

      // 初始化算子中的parameter
      // 根据const pnnx::Operator *op 去赋值std::shared_ptr<RuntimeOperator> runtime_operator
      // 先得到pnnx::parameter再根据这个去赋值RuntimeOperator中的RuntimeParameter
      const std::map<std::string, pnnx::Parameter> &params = op->params;
      if (!params.empty()) {
        InitGraphParams(params, runtime_operator);
      }
      // runtime_operator初始化玩成了
      this->operators_.push_back(runtime_operator);
    }
  }

那些Init初始化函数也不难理解:

就是按照pnnx的那些属性的性质,一层一层构建到Kuiper中

4、pnnx::inputs---->KuiperInfer::operator::inputs

初始化RuntimeOperator中的RuntimeOperator.input_operands和RuntimeOperator.input_operands_seq两个属性. 通过解析pnnx的计算图来初始化KuiperInfer计算符号中的输入部分

struct RuntimeOperator {
  /// 本过程要初始化的两个属性
  std::map<std::string, std::shared_ptr<RuntimeOperand>> input_operands; /// 运算符的输入操作数
  std::vector<std::shared_ptr<RuntimeOperand>> input_operands_seq; /// 运算符的输入操作数,顺序排列
  ...
}

这里再梳理一下,pnnx有两部分构成pnnx::operator以及pnnx::operands,通过其中的任意一个部分可以访问到另外一个部分。

PNNX::operator有一个inputs的输入操作数吗(input operand)

本节的内容是要根据pnnx中的两个部分来初始化Kuiper::Operator中的运算符输入部分, 也就是说从pnnx的计算图去初始化得到KuiperInfer计算图运算符(RuntimeOperator)的输入(input_operands和input_operands_seq).


void RuntimeGraph::InitInputOperators(const std::vector<pnnx::Operand *> &inputs,
                                      const std::shared_ptr<RuntimeOperator> &runtime_operator) {
  for (const pnnx::Operand *input : inputs) {
    if (!input) {
      continue;
    }
    // 得到pnnx操作数对应的生产者(类型是pnnx::operator)
    const pnnx::Operator *producer = input->producer;
    // 初始化RuntimeOperator的输入runtime_operand
    std::shared_ptr<RuntimeOperand> runtime_operand = std::make_shared<RuntimeOperand>();
    // 赋值runtime_operand的名称和形状
    runtime_operand->name = producer->name;
    runtime_operand->shapes = input->shape;

    switch (input->type) {
      case 1: {
        runtime_operand->type = RuntimeDataType::kTypeFloat32;
        break;
      }
      case 0: {
        runtime_operand->type = RuntimeDataType::kTypeUnknown;
        break;
      }
      default: {
        LOG(FATAL) << "Unknown input operand type: " << input->type;
      }
    }
    runtime_operator->input_operands.insert({producer->name, runtime_operand});
    runtime_operator->input_operands_seq.push_back(runtime_operand);
  }
}

通过上面的load , 已经得到了graph的结构还有属性等定义。

对input进行遍历。

通过pnnx::Operand得到自己的kuiperinfer::RuntimeOperand 并根据进行赋值
kuiperinfer::RuntimeOperand放回到runtime_operator

5、从pnnx::outputs---->KuiperInfer::operator::output_names

void RuntimeGraph::InitOutputOperators(const std::vector<pnnx::Operand *> &outputs,
                                       const std::shared_ptr<RuntimeOperator> &runtime_operator) {
  for (const pnnx::Operand *output : outputs) {
    if (!output) {
      continue;
    }
    const auto &consumers = output->consumers;
    for (const auto &c : consumers) {
      runtime_operator->output_names.push_back(c->name);
    }
  }
}

6、从pnnx::operators::Attrs去初始化KuiperInfer::RuntimeOperator::RuntimeAttributes

void RuntimeGraph::InitGraphAttrs(const std::map<std::string, pnnx::Attribute> &attrs,
                                  const std::shared_ptr<RuntimeOperator> &runtime_operator) {
  for (const auto &pair : attrs) {
    const std::string &name = pair.first;
    // 1.得到pnnx中的Attribute
    const pnnx::Attribute &attr = pair.second;
    switch (attr.type) {
      case 1: {
        // 2. 根据Pnnx的Attribute初始化KuiperInferOperator中的Attribute
        std::shared_ptr<RuntimeAttribute> runtime_attribute = std::make_shared<RuntimeAttribute>();
        runtime_attribute->type = RuntimeDataType::kTypeFloat32;
         // 2.1 赋值权重weight(此处的data是std::vector<uchar>类型)
        runtime_attribute->weight_data = attr.data;
        runtime_attribute->shape = attr.shape;
        runtime_operator->attribute.insert({name, runtime_attribute});
        break;
      }
      default : {
        LOG(FATAL) << "Unknown attribute type";
      }
    }
  }
}

 

7、从pnnx::operators::Params去初始化KuiperInfer::RuntimeOperator::Params

KuiperInfer::RuntimeOperator::RuntimeParameter有多个派生类构成,以此来对应中多种多样的参数,例如ConvOperator中有std::string类型的参数,padding_mode,也有像uint32_t类型的kernel_size和padding_size参数,所以我们需要以多种参数类型去支持他。换句话说,一个KuiperInfer::operator::Params,param可以是其中的任意一个派生类,这里我们利用了多态的特性。

KuiperInfer::RuntimeOperator::RuntimeParameter的多种派生类,如下分别表示为Int参数和Float参数,他们都是RuntimeParameter的派生类.

std::map<std::string, RuntimeParameter *> params;  /// 算子的参数信息
// 用指针来实现多态

struct RuntimeParameter { /// 计算节点中的参数信息
  virtual ~RuntimeParameter() = default;

  explicit RuntimeParameter(RuntimeParameterType type = RuntimeParameterType::kParameterUnknown) : type(type) {

  }
  RuntimeParameterType type = RuntimeParameterType::kParameterUnknown;
};
/// int类型的参数
struct RuntimeParameterInt : public RuntimeParameter {
  RuntimeParameterInt() : RuntimeParameter(RuntimeParameterType::kParameterInt) {

  }
  int value = 0;
};
/// float类型的参数
struct RuntimeParameterFloat : public RuntimeParameter {
  RuntimeParameterFloat() : RuntimeParameter(RuntimeParameterType::kParameterFloat) {

  }
  float value = 0.f;
};
...
void RuntimeGraph::InitGraphParams(const std::map<std::string, pnnx::Parameter> &params,
                                   const std::shared_ptr<RuntimeOperator> &runtime_operator) {
  for (const auto &pair : params) {
    const std::string &name = pair.first;
    const pnnx::Parameter &parameter = pair.second;
    const int type = parameter.type;
  // 根据PNNX的Parameter去初始化KuiperInfer::RuntimeOperator中的Parameter
    switch (type) {
      case int(RuntimeParameterType::kParameterUnknown): {
        RuntimeParameter *runtime_parameter = new RuntimeParameter;
        runtime_operator->params.insert({name, runtime_parameter});
        break;
      }
  // 在这应该使用派生类RuntimeParameterBool 
      case int(RuntimeParameterType::kParameterBool): {
        RuntimeParameterBool *runtime_parameter = new RuntimeParameterBool;
        runtime_parameter->value = parameter.b;
        runtime_operator->params.insert({name, runtime_parameter});
        break;
      }
 // 在这应该使用派生类RuntimeParameterInt
      case int(RuntimeParameterType::kParameterInt): {
        RuntimeParameterInt *runtime_parameter = new RuntimeParameterInt;
        runtime_parameter->value = parameter.i;
        runtime_operator->params.insert({name, runtime_parameter});
        break;
      }

      case int(RuntimeParameterType::kParameterFloat): {
        RuntimeParameterFloat *runtime_parameter = new RuntimeParameterFloat;
        runtime_parameter->value = parameter.f;
        runtime_operator->params.insert({name, runtime_parameter});
        break;
      }

      case int(RuntimeParameterType::kParameterString): {
        RuntimeParameterString *runtime_parameter = new RuntimeParameterString;
        runtime_parameter->value = parameter.s;
        runtime_operator->params.insert({name, runtime_parameter});
        break;
      }

      case int(RuntimeParameterType::kParameterIntArray): {
        RuntimeParameterIntArray *runtime_parameter = new RuntimeParameterIntArray;
        runtime_parameter->value = parameter.ai;
        runtime_operator->params.insert({name, runtime_parameter});
        break;
      }

      case int(RuntimeParameterType::kParameterFloatArray): {
        RuntimeParameterFloatArray *runtime_parameter = new RuntimeParameterFloatArray;
        runtime_parameter->value = parameter.af;
        runtime_operator->params.insert({name, runtime_parameter});
        break;
      }
      case int(RuntimeParameterType::kParameterStringArray): {
        RuntimeParameterStringArray *runtime_parameter = new RuntimeParameterStringArray;
        runtime_parameter->value = parameter.as;
        runtime_operator->params.insert({name, runtime_parameter});
        break;
      }
      default: {
        LOG(FATAL) << "Unknown parameter type";
      }
    }
  }
}

遍历得到属性,通过属性初始化RuntimeParameter,再将runtimeparameter存放到Runtime Operator中。啊

8. 将通过如上步骤初始化好的KuiperInfer::Operator存放到一个vector中

this->operators_.push_back(runtime_operator);

总结:

int load_result = this->graph_->load(param_path_, bin_path_); 得到pnnx::graph,一个一个算子去初始化 runtimegraph

得到pnnx::operators -->依次便利

pnnx operator 遍历,初始化一个RuntimeOperator

1. 得到pnnx operator的inputs,再根据这个inputs去初始化我们RuntimeOperator::runtime_operator->input_operands

2. 同理得到pnnx operator的outputs去初始化RuntimeOperator::runtime_operator->output_operands

3. runtimeParameter 根据pnnx::param初始化

4, runtimeAttr根据pnnx::attr初始化

1.2.3.4的初始化过程中, runtimeParameter,runtimeAttr,output_operands.inputs_operand放在一个runtime_operator里面

5. 再把这个runtime_operator存放好

这个runtime_operator中既有输入的参数,又有输出的数、参数,又有层的参数,又有层的权重!

转换成功!

 

 

 

 

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

ISC 2023︱诚邀您参与赛宁“安全验证评估”论坛

​​8月9日-10日&#xff0c;第十一届互联网安全大会&#xff08;简称ISC 2023&#xff09;将在北京国家会议中心举办。本次大会以“安全即服务&#xff0c;开启人工智能时代数字安全新范式”为主题&#xff0c;打造全球首场AI数字安全峰会&#xff0c;赋予安全即服务新时代内涵…

无涯教程-Perl - 环境配置

在开始编写Perl程序之前&#xff0c;让我们了解如何设置我们的Perl环境。 您的系统更有可能安装了perl。只需尝试在$提示符下给出以下命令- $perl -v 如果您的计算机上安装了perl&#xff0c;那么您将收到以下消息: This is perl 5, version 16, subversion 2 (v5.16.2) b…

JAVA SE -- 第十六天

&#xff08;全部来自“韩顺平教育”&#xff09; IO流 一、文件 是保存数据的地方 2、文件流 文件在程序中是以流的形式来操作 流&#xff1a;数据在数据源&#xff08;文件&#xff09;和程序&#xff08;内存&#xff09;之间经历的路径 输入流&#xff1a;数据从数据…

RISC-V基础之浮点指令(包含实例)

RISC-V体系结构定义了可选的浮点扩展&#xff0c;分别称为RVF、RVD和RVQ&#xff0c;用于操作单精度、双精度和四倍精度的浮点数。RVF/D/Q定义了32个浮点寄存器&#xff0c;f0到f31&#xff0c;它们的宽度分别为32位、64位或128位。当一个处理器实现了多个浮点扩展时&#xff0…

GD32F103待机模式与唤醒

GD32F103待机模式与唤醒&#xff0c;本程序使用RTC报警唤醒。 电源管理单元有3种省电模式:睡眠模式,深度睡眠模式和待机模式&#xff1b; 进入待机模式的步骤如下&#xff1a; 若需要RTC闹钟输出&#xff0c;则需要将TAMPER-RTC映射到PC13引脚; 若需要LXTAL晶振32.768KHz&…

Go 语言面试题(一):基础语法

文章目录 Q1 和 : 的区别&#xff1f;Q2 指针的作用&#xff1f;Q3 Go 允许多个返回值吗&#xff1f;Q4 Go 有异常类型吗&#xff1f;Q5 什么是协程&#xff08;Goroutine&#xff09;Q6 如何高效地拼接字符串Q7 什么是 rune 类型Q8 如何判断 map 中是否包含某个 key &#xf…

Python高阶技巧 正则表达式

正则表达式&#xff0c;又称规则表达式&#xff08;Regular Expression&#xff09;&#xff0c;是使用单个字符串来描述、匹配某个句法规则的字符串&#xff0c;常被用来检索、替换那些符合某个模式&#xff08;规则&#xff09;的文本。 简单来说&#xff0c;正则表达式就是使…

Prometheus + Grafana安装

Prometheus是一款基于时序数据库的开源监控告警系统&#xff0c;非常适合Kubernetes集群的监控。Prometheus的基本原理是通过HTTP协议周期性抓取被监控组件的状态&#xff0c;任意组件只要提供对应的HTTP接口就可以接入监控。不需要任何SDK或者其他的集成过程。这样做非常适合做…

Ceph入门到精通-远程开发Windows下使用SSH密钥实现免密登陆Linux服务器

工具&#xff1a; win10、WinSCP 服务器生成ssh密钥&#xff1a; 打开终端&#xff0c;使账号密码登录&#xff0c;输入命令 ssh-keygen -t rsa Winscp下载 Downloading WinSCP-6.1.1-Setup.exe :: WinSCP window 生成密钥 打开powershell ssh-keygen -t rsa 注意路径 …

Day10-NodeJS和NPM配置

Day10-NodeJS和NPM 一 Nodejs 1 简介 Nodejs学习中文网:https://www.nodeapp.cn/synopsis.html Nodejs的官网:https://nodejs.org/ 概念:Nodejs是JavaScript的服务端运行环境.Nodejs不是框架,也不是编程语言,就是一个运行环境. Nodejs是基于chrome V8引擎开发的一套js代码…

【雕爷学编程】Arduino动手做(184)---快餐盒盖,极低成本搭建机器人实验平台2

吃完快餐粥&#xff0c;除了粥的味道不错之外&#xff0c;我对个快餐盒的圆盖子产生了兴趣&#xff0c;能否做个极低成本的简易机器人呢&#xff1f;也许只需要二十元左右 知识点&#xff1a;轮子&#xff08;wheel&#xff09; 中国词语。是用不同材料制成的圆形滚动物体。简…

Python以及Pycharm保姆级的下载安装教程,看完就懂

&#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 接下来看看由辉辉所写的关于Python的相关操作吧 目录 &#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 一.Python的安装 1.下载地址 2.下载步骤…

Netty+springboot开发即时通讯系统笔记(一)

业务部分从sql开始&#xff1a; /*Navicat Premium Data TransferSource Server : localhostSource Server Type : MySQLSource Server Version : 50740Source Host : localhost:3306Source Schema : im-coreTarget Server Type : MySQLTarge…

微信小程序的自定义TabBar及Vant的使用

一、安装Vant 1、在 资源管理器 空白位置&#xff0c;点右键打开 在外部终端窗口打开 2、初始化NPM npm init -y 3、安装命令 npm i vant/weapp1.3.3 -S --production 4、构建NPM包 在 工具 里选择构建NPM包 5、删除style:v2 在app.json里&#xff0c;删除"style"…

SpringBoot中使用Swagger详解

在Spring Boot中规范的使用Swagger进行接口测试 文章目录 Swagger是什么&#xff1f;为什么要用Swagger&#xff1f;准备工作创建项目编写接口验证 高级配置文档注释接口过滤自定义响应 SwaggerUI的使用遇到的问题&#xff1a; Swagger是什么&#xff1f; Swagger是一套基于Op…

黑马大数据学习笔记4-Hive部署和基本操作

目录 思考规划安装MySQL数据库修改root用户密码配置Hadoop下载解压Hive提供MySQL Driver包配置Hive初始化元数据库启动Hive&#xff08;使用Hadoop用户&#xff09;Hive体验HiveServer2HiveServer2服务启动 Beeline p48、51、52 https://www.bilibili.com/video/BV1WY4y197g7/?…

机器学习——SMO算法推导与实践

一、 硬间隔-SMO算法推导 明天再说&#xff0c;啊。。。。感觉天空明朗了很多&#xff0c;即使现在已经很晚了 还是要打开柯南&#xff0c;看看电视&#xff0c;等待天气预报所说的台风天吧&#xff01; 一时之间&#xff0c;忽然失去了用markdown语法写下推导过程的勇气。。。…

【Docker】Docker+Zipkin+Elasticsearch+Kibana部署分布式链路追踪

文章目录 1. 组件介绍2. 服务整合2.1. 前提&#xff1a;安装好Elaticsearch和Kibana2.2. 再整合Zipkin 点击跳转&#xff1a;Docker安装MySQL、Redis、RabbitMQ、Elasticsearch、Nacos等常见服务全套&#xff08;质量有保证&#xff0c;内容详情&#xff09; 本文主要讨论在Ela…

Typescript 枚举类型

枚举是用来表示一组明确的可选值列表 // enum是枚举类型的关键字 //枚举如果不设置值&#xff0c;默认从0开始 enum Direction {Up, // 0 Down, // 1 Left, // 2Right // 3} //如果给第一个值赋值为100&#xff0c;则第二、第三第四个都会在第一个的基础上1 分别是101,102…

算法通过村第二关-链表黄金笔记|K个一组反转

文章目录 前言链表反转|K个一组翻转链表解题方法&#xff1a;头插法处理&#xff1a;穿针引线法处理&#xff1a; 总结 前言 提示&#xff1a;没有人天生就喜欢一种气味而讨厌另一种气味。文明的暗示而已。 链表反转|K个一组翻转链表 给你链表的头节点 head &#xff0c;每 k…
最新文章