libtorch c++ 使用预训练权重(以resnet为例)

news2024/11/29 4:42:10

任务: 识别猫咪。

目录

1. 直接使用

1.1 获取预训练权重

 1.2 libtorch直接使用pt权重

2. 间接使用

2.1 BasicBlock

2.2 实现ResNet

2.3 BottleNeck


1. 直接使用

1.1 获取预训练权重

比如直接使用Pytorch版的预训练权重。先把权重保存下来,并打印分类类别(方便后面对比)

import torch
import torchvision.models as models
from PIL import Image
import numpy as np

# input
image = Image.open("E:\\code\\c++\\libtorch_models\\data\\cat.jpg")  # 图片发在了build文件夹下
image = image.resize((224, 224), Image.ANTIALIAS)
image = np.asarray(image)
image = image / 255.0
image = torch.Tensor(image).unsqueeze_(dim=0)  # (b,h,w,c)
image = image.permute((0, 3, 1, 2)).float()  # (b,h,w,c) -> (b,c,h,w)

# model
model = models.resnet18(pretrained=True)
model = model.eval()
resnet = torch.jit.trace(model, torch.rand(1, 3, 224, 224))

# infer
output = resnet(image)
max_index = torch.max(output, 1)[1].item()
print(max_index)  # ImageNet1000类的类别序号
resnet.save('resnet.pt')

将保存权重resnet.pt,并打印分类索引号是283,对应的是猫。

 1.2 libtorch直接使用pt权重

使用接口torch::jit::load 即可载入权重并获取resnet18模型。

然后再使用std::vector<torch::jit::IValue>传送数据到模型中,即可得到类别。

打印结果是283,和前面的pytorch版是一样。

#include <iostream>
#include <opencv.hpp>

int main()
{
	// load weights and model.
	auto resnet18 = torch::jit::load("E:\\code\\c++\\libtorch_models\\weights\\resnet18.pt");
	assert(module != nullptr);
	resnet18.to(torch::kCUDA);
	resnet18.eval();

	// pre
	cv::Mat image = cv::imread("E:\\code\\c++\\libtorch_models\\data\\cat.jpg");
	cv::resize(image, image, cv::Size(224, 224));
	torch::Tensor tensor_image = torch::from_blob(image.data, {224, 224,3 }, torch::kByte);
	tensor_image = torch::unsqueeze(tensor_image, 0).permute({ 0,3,1,2 }).to(torch::kCUDA).to(torch::kFloat).div(255.0);  // (b,h,w,c) -> (b,c,h,w)
	std::cout << tensor_image.options() << std::endl;
	std::vector<torch::jit::IValue> inputs;
	inputs.push_back(tensor_image);

	// infer
	auto output = resnet18.forward(inputs).toTensor();
	auto max_result = output.max(1, true);  
	auto max_index = std::get<1>(max_result).item<float>();
	std::cout << max_index << std::endl;

	return 0;
}

2. 间接使用

间接使用是指基于libtorch c++ 复现一遍resnet网络,再利用前面得到的权重,初始化模型。输出结果依然是283.

#include <iostream>
#include "resnet.h"  // libtorch实现的resnet
#include <opencv.hpp>

int main()
{
	// load weights and model.
	ResNet resnet = resnet18(1000);  // orig net
	torch::load(resnet, "E:\\code\\c++\\libtorch_models\\weights\\resnet18.pt");  // load weights.
	assert(resnet != nullptr);
	resnet->to(torch::kCUDA);
	resnet->eval();

	// pre
	cv::Mat image = cv::imread("E:\\code\\c++\\libtorch_models\\data\\cat.jpg");
	cv::resize(image, image, cv::Size(224, 224));
	torch::Tensor tensor_image = torch::from_blob(image.data, { 224, 224,3 }, torch::kByte);
	tensor_image = torch::unsqueeze(tensor_image, 0).permute({ 0,3,1,2 }).to(torch::kCUDA).to(torch::kFloat).div(255.0);  // (b,h,w,c) -> (b,c,h,w)
	std::cout << tensor_image.options() << std::endl;

	// infer
	auto output = resnet->forward(tensor_image);
	auto max_result = output.max(1, true);
	auto max_index = std::get<1>(max_result).item<float>();
	std::cout << max_index << std::endl;
	return 0;
}

 接下来介绍resnet详细实现过程。

2.1 BasicBlock

先实现resnet最小单元BasicBlock,该单元是两次卷积组成的残差块。结构如下。

两种形式,如果第一个卷积stride等于2进行下采样,则跳层连接也需要下采样,维度才能一致,再进行对应相加。

// resnet18 and resnet34
class BasicBlockImpl : public torch::nn::Module {
public:
	BasicBlockImpl(int64_t in_channels, int64_t out_channels, int64_t stride, torch::nn::Sequential downsample);
	torch::Tensor forward(torch::Tensor x);
public:
	torch::nn::Sequential downsample{ nullptr };
private:
	torch::nn::Conv2d conv1{ nullptr };
	torch::nn::BatchNorm2d bn1{ nullptr };
	torch::nn::Conv2d conv2{ nullptr };
	torch::nn::BatchNorm2d bn2{ nullptr };
};
TORCH_MODULE(BasicBlock);

// other resnet using BottleNeck
class BottleNeckImpl : public torch::nn::Module {
public:
	BottleNeckImpl(int64_t in_channels, int64_t out_channels, int64_t stride,
		torch::nn::Sequential downsample, int groups, int base_width);
	torch::Tensor forward(torch::Tensor x);
public:
	torch::nn::Sequential downsample{ nullptr };
private:
	torch::nn::Conv2d conv1{ nullptr };
	torch::nn::BatchNorm2d bn1{ nullptr };
	torch::nn::Conv2d conv2{ nullptr };
	torch::nn::BatchNorm2d bn2{ nullptr };
	torch::nn::Conv2d conv3{ nullptr };
	torch::nn::BatchNorm2d bn3{ nullptr };
};
TORCH_MODULE(BottleNeck);
// conv3x3+bn+relu, conv3x3+bn, 
// downsample: 用来对原始输入进行下采样.
// stride: 控制是否下采样,stride=2则是下采样,且downsample将用于对原始输入进行下采样.
BasicBlockImpl::BasicBlockImpl(int64_t in_channels, int64_t out_channels, int64_t stride, torch::nn::Sequential downsample) {
	this->downsample = downsample;
	conv1 = torch::nn::Conv2d(torch::nn::Conv2dOptions(in_channels, out_channels, 3).stride(stride).padding(1).bias(false));
	bn1 = torch::nn::BatchNorm2d(torch::nn::BatchNorm2dOptions(out_channels));
	conv2 = torch::nn::Conv2d(torch::nn::Conv2dOptions(out_channels, out_channels, 3).stride(1).padding(1).bias(false));
	bn2 = torch::nn::BatchNorm2d(torch::nn::BatchNorm2dOptions(out_channels));

	register_module("conv1", conv1);
	register_module("bn1", bn1);
	register_module("conv2", conv2);
	register_module("bn2", bn2);
	if (!downsample->is_empty()) {
		register_module("downsample", downsample);
	}
}
torch::Tensor BasicBlockImpl::forward(torch::Tensor x) {
	torch::Tensor identity = x.clone();

	x = conv1->forward(x);  // scale/2. or keep scale unchange.
	x = bn1->forward(x);
	x = torch::relu(x);

	x = conv2->forward(x);
	x = bn2->forward(x);

	// 加入x的维度减半,则原始输入必须也减半。
	if (!downsample->is_empty()) identity = downsample->forward(identity);

	x += identity;
	x = torch::relu(x);
	return x;
}

2.2 实现ResNet

这里以resnet18为例。网络结构如下。

简单一句话,使用残差块多次卷积,最后接一个全链接层进行分类。

注意上图中的layer1到layer4是由BasicBlock0和BasicBlock1两种残差块组成。实现如下。

// out_channels: 每一个block输出的通道数。
// blocks: 每个layer包含的blocks数.
torch::nn::Sequential ResNetImpl::_make_layer(int64_t out_channels, int64_t blocks, int64_t stride) {
	// 1, downsampe: stride or channel
	torch::nn::Sequential downsample;
	if (stride != 1 || this->in_channels != out_channels * expansion) {  // 步长等于2,或者输入通道不等于输出通道,则都是接conv操作,改变输入x的维度
		downsample = torch::nn::Sequential(
			torch::nn::Conv2d(torch::nn::Conv2dOptions(this->in_channels, out_channels * this->expansion, 1).stride(stride).padding(0).groups(1).bias(false)),
			torch::nn::BatchNorm2d(out_channels * this->expansion)
		);
	}
	// 2, layers: first is downsample and others are conv with 1 stride.
	torch::nn::Sequential layers;
	if (this->is_basic) {
		layers->push_back(BasicBlock(this->in_channels, out_channels, stride, downsample));  // 控制是否下采样
		this->in_channels = out_channels;  // 更新输入通道,以备下次使用
		for (int64_t i = 1; i < blocks; i++) {  // 剩余的block都是in_channels == out_channels. and stride = 1. 
			layers->push_back(BasicBlock(this->in_channels, this->in_channels, 1, torch::nn::Sequential()));  // 追加多个conv3x3,且不改变维度
		}
	}
	else {
		layers->push_back(BottleNeck(this->in_channels, out_channels, stride, downsample, this->groups, this->base_width));
		this->in_channels = out_channels * this->expansion;  // 更新输入通道,以备下次使用
		for (int64_t i = 1; i < blocks; i++) {  // 剩余的block都是in_channels == out_channels. and stride = 1. 
			layers->push_back(BottleNeck(this->in_channels, this->in_channels, 1, torch::nn::Sequential(), this->groups, this->base_width));
		}
	}
	return layers;
}

resnet实现。 

class ResNetImpl : public torch::nn::Module {
public:
	ResNetImpl(std::vector<int> layers, int num_classes, std::string model_type,
		int groups, int width_per_group);
	torch::Tensor forward(torch::Tensor x);
public:
	torch::nn::Sequential _make_layer(int64_t in_channels, int64_t blocks, int64_t stride = 1);
private:
	int expansion = 1;  // 通道扩大倍数,resnet50会用到
	bool is_basic = true;  // 是BasicBlock,还是BottleNeck
	int in_channels = 64;  // 记录输入通道数
	int groups = 1, base_width = 64;
	torch::nn::Conv2d conv1{ nullptr };
	torch::nn::BatchNorm2d bn1{ nullptr };
	torch::nn::Sequential layer1{ nullptr };
	torch::nn::Sequential layer2{ nullptr };
	torch::nn::Sequential layer3{ nullptr };
	torch::nn::Sequential layer4{ nullptr };
	torch::nn::Linear fc{ nullptr };
};
TORCH_MODULE(ResNet);
// layers: resnet18: { 2, 2, 2, 2 }, resnet34: { 3, 4, 6, 3 }, resnet50: { 3, 4, 6, 3 };
ResNetImpl::ResNetImpl(std::vector<int> layers, int num_classes = 1000, std::string model_type = "resnet18", int groups = 1, int width_per_group = 64) {
	if (model_type != "resnet18" && model_type != "resnet34")  // 即不使用BasicBlock,使用BottleNeck
	{  
		this->expansion = 4;
		is_basic = false;
	}
	this->groups = groups;  // 1
	this->base_width = base_width;  // 64
	
	this->conv1 = torch::nn::Conv2d(torch::nn::Conv2dOptions(3, 64, 7).stride(2).padding(3).groups(1).bias(false));  // scale/2
	this->bn1 = torch::nn::BatchNorm2d(torch::nn::BatchNorm2dOptions(64));
	this->layer1 = torch::nn::Sequential(_make_layer(64, layers[0]));  // stride=1, scale and channels unchange
	this->layer2 = torch::nn::Sequential(_make_layer(128, layers[1], 2)); // stride=2, scale/2. channels double
	this->layer3 = torch::nn::Sequential(_make_layer(256, layers[2], 2)); // stride=2, scale/2. channels double
	this->layer4 = torch::nn::Sequential(_make_layer(512, layers[3], 2)); // stride=2, scale/2. channels double

	this->fc = torch::nn::Linear(512 * this->expansion, num_classes);
	
	register_module("conv1", conv1);
	register_module("bn1", bn1);
	register_module("layer1", layer1);
	register_module("layer2", layer2);
	register_module("layer3", layer3);
	register_module("layer4", layer4);
	register_module("fc", fc);
}
torch::Tensor ResNetImpl::forward(torch::Tensor x) {
	// 1,先是两次下采样. (b,3,224,224) -> (b,64,56,56)
	x = conv1->forward(x);  // (b,3,224,224)->(b,64,112,112)
	x = bn1->forward(x);
	x = torch::relu(x);  // feature 1
	x = torch::max_pool2d(x, 3, 2, 1);  // k=3,s=2,p=1. (b,64,112,112)->(b,64,56,56)

	x = layer1->forward(x);  // feature 2. (b,64,56,56)
	x = layer2->forward(x);  // feature 3. (b,128,28,28)
	x = layer3->forward(x);  // feature 4. (b,256,14,14)
	x = layer4->forward(x);  // feature 5. (b,512,7,7)
	
	x = torch::adaptive_avg_pool2d(x, {1, 1});  // (b,512,1,1)
	//x = torch::avg_pool2d(x, 7, 1);  // (b,512,1,1)
	x = x.view({ x.sizes()[0], -1 });  // (b,512)
	x = fc->forward(x); // (b,1000)
	
	return torch::log_softmax(x, 1);  // score (负无穷,0]
}

 创建resnet18和resnet34。其中layers中的数字代表当前layer中包含的BasicBlock个数。


// 创建不同resnet分类网络的函数
ResNet resnet18(int64_t num_classes) {
	std::vector<int> layers = { 2, 2, 2, 2 };
	ResNet model(layers, num_classes, "resnet18");
	return model;
}

ResNet resnet34(int64_t num_classes) {
	std::vector<int> layers = { 3, 4, 6, 3 };
	ResNet model(layers, num_classes, "resnet34");
	return model;
}

2.3 BottleNeck

resnet系列框架是一样的,不同点是组件有差异。 

resnet18和resnet34都是用BasicBlock组件,而resnet50及以上则使用BottleNeck结构。如下所示。

BottleNeck有三种形式:

(1)BottleNeck0: stride=1, only 4*channels;

(2)BottleNeck1: stride=1, only 4*channels;

(3)BottleNeck2: stride=2, 4*channels and scales/2

// other resnet using BottleNeck
class BottleNeckImpl : public torch::nn::Module {
public:
	BottleNeckImpl(int64_t in_channels, int64_t out_channels, int64_t stride,
		torch::nn::Sequential downsample, int groups, int base_width);
	torch::Tensor forward(torch::Tensor x);
public:
	torch::nn::Sequential downsample{ nullptr };
private:
	torch::nn::Conv2d conv1{ nullptr };
	torch::nn::BatchNorm2d bn1{ nullptr };
	torch::nn::Conv2d conv2{ nullptr };
	torch::nn::BatchNorm2d bn2{ nullptr };
	torch::nn::Conv2d conv3{ nullptr };
	torch::nn::BatchNorm2d bn3{ nullptr };
};
TORCH_MODULE(BottleNeck);
// stride: 控制是否下采样,stride=2则是下采样,且downsample将用于对原始输入进行下采样.
// conv1x1+bn+relu, conv3x3+bn+relu, conv1x1+bn+relu
BottleNeckImpl::BottleNeckImpl(int64_t in_channels, int64_t out_channels, int64_t stride,
	torch::nn::Sequential downsample, int groups, int base_width) {
	this->downsample = downsample;
	// 64 * (64 / 64) / 1 = 64, 128 * (64 / 64) / 1 = 128, 128 * (64 / 64) / 2 = 64.
	int width = int(out_channels * (base_width / 64.)) * groups;  // 64 * (64/64) / 1. 当前的输出通道数

	// 1x1 conv
	conv1 = torch::nn::Conv2d(torch::nn::Conv2dOptions(in_channels, width, 1).stride(1).padding(0).groups(1).bias(false));
	bn1 = torch::nn::BatchNorm2d(torch::nn::BatchNorm2dOptions(width));
	// 3x3 conv
	conv2 = torch::nn::Conv2d(torch::nn::Conv2dOptions(width, width, 3).stride(stride).padding(1).groups(groups).bias(false));
	bn2 = torch::nn::BatchNorm2d(torch::nn::BatchNorm2dOptions(width));
	// 1x1 conv
	conv3 = torch::nn::Conv2d(torch::nn::Conv2dOptions(width, out_channels * 4, 1).stride(1).padding(0).groups(1).bias(false));
	bn3 = torch::nn::BatchNorm2d(torch::nn::BatchNorm2dOptions(out_channels * 4));

	register_module("conv1", conv1);
	register_module("bn1", bn1);
	register_module("conv2", conv2);
	register_module("bn2", bn2);
	register_module("conv3", conv3);
	register_module("bn3", bn3);

	if (!downsample->is_empty()) {
		register_module("downsample", downsample);
	}
}
torch::Tensor BottleNeckImpl::forward(torch::Tensor x) {
	torch::Tensor identity = x.clone();

	// conv1x1+bn+relu
	x = conv1->forward(x); 
	x = bn1->forward(x);
	x = torch::relu(x);
	// conv3x3+bn+relu
	x = conv2->forward(x);  // if stride==2, scale/2
	x = bn2->forward(x);
	x = torch::relu(x);
	// conv1x1+bn+relu
	x = conv3->forward(x);  // double channels
	x = bn3->forward(x);

	if (!downsample->is_empty()) identity = downsample->forward(identity);

	x += identity;
	x = torch::relu(x);

	return x;
}

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

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

相关文章

vue+element详细完整实现个人博客、个人网站

一.前言 博客成品在线预览&#xff1a;点击访问 接上一篇《vueelementui实现非常好看的博客、网站首页&#xff0c;网站模板》。 上一篇实现了整个框架的搭建和首页的编码&#xff0c;在此基础上&#xff0c;这一期进行了最终的功能页面完善&#xff0c;增加了功能&#xff1a…

【自学Python】Python合并字符串

Python合并字符串 Python合并字符串教程 在开发过程中&#xff0c;很多时候我们有合并 字符串 的需求&#xff0c;即把一个 元祖 或 列表 中包含的多个字符串连接成一个字符串。这个操作是 分割字符串 的逆操作。 在 Python 中&#xff0c;合并字符串我们使用 join() 函数。…

过气明星李嘉明和《丁香花》唐磊,找哪个录制祝福视频值100万

说起娱乐圈的明星&#xff0c;给人的感觉都是光鲜亮丽&#xff0c;因为他们有流量有热度&#xff0c;想要赚钱就太容易了。过去的娱乐圈明星&#xff0c;都是靠自己的专业赚钱&#xff0c;比如说你是著名歌星&#xff0c;就要靠唱歌赚钱&#xff0c;如果你是影视明星&#xff0…

天天都接触的以太网接口,你知道有哪些类型和参数吗?

以太网接口简介 以太网接口是一种用于局域网组网的接口&#xff0c;包括&#xff1a;以太网电接口、以太网光接口。 为了适应网络需求&#xff0c;设备上定义了以下几种以太网接口类型&#xff1a; 二层以太网接口 是一种物理接口&#xff0c;工作在数据链路层&#xff0c;不…

ASP.NET Core 3.1系列(30)——Newtonsoft.Json实现JSON的序列化和反序列化

1、前言 在早期版本的ASP.NET Core项目中&#xff0c;Newtonsoft.Json的使用率非常高。虽然微软当前主推System.Text.Json来处理JSON的序列化和反序列化&#xff0c;但Newtonsoft.Json在这方面做的也是相当不错&#xff0c;下面就来介绍一下它的用法。 2、引入Newtonsoft.Jso…

echarts柱状图值为0是不显示以及柱状图百分比展示

echarts柱状图值为0是不显示以及柱状图百分比展示 1.效果展示 2.代码 <template><div id"container"><div id"main"></div></div> </template> <script>import * as echarts from echarts import * as lodash…

代码随想录第四天(203、707)

文章目录一、链表知识203. 移除链表元素提交看答案之后的豁然开朗707. 设计链表完全不会&#xff0c;看完答案后改的一、链表知识 1、链表在内存中的存储不是连续的 意思是这个链表的其实节点是2&#xff0c;终止节点是7 2、链表和数组的对比 数组的长的是固定的&#xff0c…

【Iot】阿里云物联网平台入门之什么是消息解析、什么是Topic、JavaScript脚本示例解析

在IoT场景中&#xff0c;很多传感器采集到的都是二进制数据&#xff0c;或者私有协议格式数据流&#xff0c;设备端又不具备转换成结构化JSON的能力&#xff0c;这时候我们可以借助IoT物联网平台云端自定义数据解析能力&#xff0c;转换Modbus&#xff0c;电力协议&#xff0c;…

YOLO家族系列模型的演变:从v1到v8(下)

昨天的文章中&#xff0c;我们回顾了 YOLO 家族的前 9 个架构。本文中将继续总结最后3个框架&#xff0c;还有本月最新发布的YOLO V8. YOLOR Chien-Yao Wang, I-Hau Yeh, Hong-Yuan Mark Liao “You Only Learn One Representation: Unified Network for Multiple Tasks”202…

JavaWeb-JavaScript

JavaWeb-JavaScript 1&#xff0c;JavaScript简介 JavaScript 是一门跨平台、面向对象的脚本语言&#xff0c;而Java语言也是跨平台的、面向对象的语言&#xff0c;只不过Java是编译语言&#xff0c;是需要编译成字节码文件才能运行的&#xff1b;JavaScript是脚本语言&#…

43. 【农产品溯源项目前后端Demo】后端二次开发的重点修改位置

前面讲过农产品溯源Demo比较简单,如果想二次开发需要重点关注的目录。 如果要开发一个新的API、对接新的合约,需要有哪些步骤? 定义数据结构,在domain包新增Class,定义好数据字段,定义好get、set方法。domain包没有业务的逻辑实现,只有结构、字段定义。 如果字段首字母小…

手摸手学会node框架之一——koa 傻瓜式小白教程

一、Koa简介 基于 Node.js 平台的下一代 web 开发框架。 由 Express 幕后的原班人马打造&#xff0c; 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。 详细请参考Koa官网进行学习。 二、Koa基础入门 1.项目初始化 执行 npm init -y, 生成…

博客之星规则能否参照“金球奖”

文章目录课前小差粉丝对我的价值粉丝数量的提升KOL与粉丝链接粉丝影响收入博客之星规则设想博客之星新玩法&#xff1f;内部评审展望2023写在最后课前小差 哈喽&#xff0c;大家好&#xff0c;我是几何心凉&#xff0c;这是一份全新的专栏&#xff0c;唯一得倒CSDN王总的授权&…

小侃设计模式(廿一)-状态模式

1.概述 状态模式&#xff08;State Pattern&#xff09;是行为型设计模式的一种&#xff0c;它主要用来解决对象存在多种状态转换时&#xff0c;需要对外输出不同的行为。状态模式与策略模式有一定相似&#xff0c;区别在于策略模式行为彼此独立&#xff0c;可以进行相互替换&…

VueJs中的toRef与toRefs函数的一个比较

前言ref是处理基本数据类型响应式API函数,在setup中声明定义的变量,可以直接在模板中使用没有被响应式API包裹处理的变量数据,是不具备响应式能力的也就是往往在逻辑中修改了数据,但是页面不会更新,那怎么样将一个非响应式数据变成响应式数据就需要用到toRef()与toRefs()这两个…

【计算机网络】网络编程套接字

文章目录理解源IP地址和目的IP地址理解端口号和进程ID理解源端口号和目的端口号认识TCP协议认识UDP协议网络字节序socket编程接口socket网址查看socket常见APIUDP协议实现网络通信UDP创建socket文件描述符sockaddr结构UDP绑定端口号UDP接收发送网络数据简单的UDP网络程序TCP协议…

拉伯证券|大股东或易主,阿里巴巴换股入局

三大股指齐上扬&#xff0c;早盘主力埋伏这些股。 到午间收盘&#xff0c;“家居零售榜首股”之称的美凯龙一字涨停&#xff0c;港股红星美凯龙涨24%&#xff0c;此前一度涨超30%。 消息面上&#xff0c;1月13日晚间&#xff0c;美凯龙发布公告称&#xff0c;公司控股股东红星…

transformer算法解析

本文参考&#xff1a; 详解Transformer &#xff08;Attention Is All You Need&#xff09; - 知乎 Transformer 代码完全解读&#xff01;_AI科技大本营的博客-CSDN博客 Transformer学习笔记一&#xff1a;Positional Encoding&#xff08;位置编码&#xff09; - 知乎 1、…

【C语言】自定义类型

前言男孩子在外面要保护好自己~一、结构体为什么会有结构体呢&#xff1f;但要描述一个复杂对象时&#xff0c;仅用之前学过的基本数据类型表达不了&#xff08;如&#xff1a;我要描述一个人&#xff0c;仅靠基本数据类型只能说定义他的一种属性<如用 int 定义他的年龄>…

字符串的处理

一、字符数组 用来存放字符型数据的数组称为字符数组&#xff0c;其元素是一个个的字符。 char 字符数组名[常量表达式]&#xff1b; C语言规定字符串是以\0字符作为结束符的字符数组。 在程序中可以通过判断数组元素是否为空字符来判断字符串是否结束。 字符串的输…