从零开始构建 Vision Transformer(ViT) 模型

Transformer 模型最早由 Vaswani 等人在 2017 年论文 Attention Is All You Need 中提出,并已广泛应用于自然语言处理。

2021年,Dosovitsky 等人在论文An Image is Worth 16x16 Words: Transformers for Image Recognition at Scale中提出将 Transformer 用于计算机视觉任务,与卷积网络相比取得了优异的结果。

在本文中,我将从头开始构建一个视觉 Transformer 模型,并在 MNIST 数据集上进行测试。这对于算法本身的理解和算法面试大有好处

喜欢本文记得收藏、点赞、关注,希望大模型技术交流的加入我们。

技术交流

技术要学会分享、交流,不建议闭门造车。一个人可以走的很快、一堆人可以走的更远。

成立了算法面试和技术交流群,相关资料、技术交流&答疑,均可加我们的交流群获取,群友已超过2000人,添加时最好的备注方式为:来源+兴趣方向,方便找到志同道合的朋友。

方式①、微信搜索公众号:机器学习社区,后台回复:加群
方式②、添加微信号:mlc2040,备注:来自CSDN + 技术交流

通俗易懂讲解大模型系列

  • 重磅消息!《大模型面试宝典》(2024版) 正式发布!

  • 重磅消息!《大模型实战宝典》(2024版) 正式发布!

  • 做大模型也有1年多了,聊聊这段时间的感悟!

  • 用通俗易懂的方式讲解:大模型算法工程师最全面试题汇总

  • 用通俗易懂的方式讲解:不要再苦苦寻觅了!AI 大模型面试指南(含答案)的最全总结来了!

  • 用通俗易懂的方式讲解:我的大模型岗位面试总结:共24家,9个offer

  • 用通俗易懂的方式讲解:大模型 RAG 在 LangChain 中的应用实战

  • 用通俗易懂的方式讲解:ChatGPT 开放的多模态的DALL-E 3功能,好玩到停不下来!

  • 用通俗易懂的方式讲解:基于扩散模型(Diffusion),文生图 AnyText 的效果太棒了

  • 用通俗易懂的方式讲解:在 CPU 服务器上部署 ChatGLM3-6B 模型

  • 用通俗易懂的方式讲解:ChatGLM3-6B 部署指南

  • 用通俗易懂的方式讲解:使用 LangChain 封装自定义的 LLM,太棒了

  • 用通俗易懂的方式讲解:基于 Langchain 和 ChatChat 部署本地知识库问答系统

  • 用通俗易懂的方式讲解:Llama2 部署讲解及试用方式

  • 用通俗易懂的方式讲解:一份保姆级的 Stable Diffusion 部署教程,开启你的炼丹之路

  • 用通俗易懂的方式讲解:LlamaIndex 官方发布高清大图,纵览高级 RAG技术

  • 用通俗易懂的方式讲解:为什么大模型 Advanced RAG 方法对于AI的未来至关重要?

  • 用通俗易懂的方式讲解:基于 Langchain 框架,利用 MongoDB 矢量搜索实现大模型 RAG 高级检索方法

导入库和模块

import torch
import torch.nn as nn
import torchvision.transforms as T
from torch.optim import Adam
from torchvision.datasets.mnist import MNIST
from torch.utils.data import DataLoader
import numpy as np

我们将使用PyTorch来构建我们的视觉Transformer,因此我们需要导入PyTorch库及其他将在本教程中使用的库。

首先,我们导入PyTorch和其神经网络模块:

import torch
import torch.nn as nn

我们还需要导入torchvision.transforms以调整输入图像的大小并将其转换为张量。调整输入图像的大小是可选的,只需确保图像尺寸可以被patch的尺寸整除。

import torchvision.transforms as T

我们将使用Adam作为优化器,因此需要从torch.optim中导入它。

from torch.optim import Adam

我们将从torchvision中导入本教程使用的MNIST数据集。我们将使用PyTorch的DataLoader来帮助加载数据,因此也需要导入它。

from torchvision.datasets.mnist import MNIST
from torch.utils.data import DataLoader

最后,我们需要导入numpy,在创建位置编码时,我们将使用它来执行sin和cos计算。

import numpy as np

Patch Embeddings

class PatchEmbedding(nn.Module):def __init__(self, d_model, img_size, patch_size, n_channels):super().__init__()self.d_model = d_model # 模型维度self.img_size = img_size # 图像尺寸self.patch_size = patch_size # Patch尺寸self.n_channels = n_channels # 通道数self.linear_project = nn.Conv2d(self.n_channels, self.d_model, kernel_size=self.patch_size, stride=self.patch_size)# B: 批量大小# C: 图像通道# H: 图像高度# W: 图像宽度# P_col: Patch列# P_row: Patch行def forward(self, x):x = self.linear_project(x) # (B, C, H, W) -> (B, d_model, P_col, P_row)x = x.flatten(2) # (B, d_model, P_col, P_row) -> (B, d_model, P)x = x.transpose(1, 2) # (B, d_model, P) -> (B, P, d_model)return x

创建视觉Transformer的第一步是将输入图像拆分为patch,并创建这些patch的线性嵌入序列。我们可以使用PyTorch的Conv2d方法实现这一点。

Conv2d方法将输入图像拆分为patch,并提供与模型宽度相等的线性投影。通过将kernel_sizestride设置为patch的大小,我们确保patch的尺寸正确且没有重叠。

self.linear_project = nn.Conv2d(self.n_channels, self.d_model, kernel_size=self.patch_size, stride=self.patch_size)

forward方法中,我们通过linear_project/Conv2D方法传递形状为(B, C, H, W)的输入,并接收形状为(B, d_model, P_col, P_row)的输出。

def forward(self, x):x = self.linear_project(x) # (B, C, H, W) -> (B, d_model, P_col, P_row)


我们使用 flatten 方法将补丁列和补丁行维度组合成一个补丁维度,使其形状变为 (B, d_model, P)

x = x.flatten(2) # (B, d_model, P_col, P_row) -> (B, d_model, P)

最后,我们使用 transpose 方法将 d_model 和补丁维度交换,使其形状变为 (B, P, d_model)。

x = x.transpose(-2, -1) # (B, d_model, P) -> (B, P, d_model)


类 Token 和位置编码

class PositionalEncoding(nn.Module):def __init__(self, d_model, max_seq_length):super().__init__()self.cls_token = nn.Parameter(torch.randn(1, 1, d_model))  # 分类 Token# 创建位置编码pe = torch.zeros(max_seq_length, d_model)for pos in range(max_seq_length):for i in range(d_model):if i % 2 == 0:pe[pos][i] = np.sin(pos / (10000 ** (i / d_model)))else:pe[pos][i] = np.cos(pos / (10000 ** ((i - 1) / d_model)))self.register_buffer('pe', pe.unsqueeze(0))def forward(self, x):# 扩展以使每个图像在批处理中都有分类 Tokentokens_batch = self.cls_token.expand(x.size()[0], -1, -1)# 将分类 Token 添加到每个嵌入的开头x = torch.cat((tokens_batch, x), dim=1)# 将位置编码添加到嵌入中x = x + self.pereturn x

视觉 Transformer 模型使用标准方法将可学习的分类 Token 添加到补丁嵌入中以进行分类。

self.cls_token = nn.Parameter(torch.randn(1, 1, d_model))


与 LSTM 等模型顺序地接收嵌入不同,Transformer 并行地接收嵌入。虽然这提高了速度,但 Transformer 并不了解序列的顺序。这是一个问题,因为改变图像补丁的顺序很可能会改变图像的内容及其所表示的内容。一个例子是图 5,它显示了改变图像补丁顺序可以将图像从一个 O 改变为更像 X 的东西。

为了解决这个问题,需要将位置编码添加到补丁嵌入中。每个位置编码都是唯一的,表示它所代表的位置,这使得模型能够识别每个嵌入应该放在哪个位置。为了将位置编码添加到嵌入中,它们必须具有相同的维度,即 d_model。我们通过使用图 6 中的方程来获取位置编码。

pe = torch.zeros(max_seq_length, d_model)for pos in range(max_seq_length):for i in range(d_model):if i % 2 == 0:pe[pos][i] = np.sin(pos / (10000 ** (i / d_model)))else:pe[pos][i] = np.cos(pos / (10000 ** ((i - 1) / d_model)))self.register_buffer('pe', pe.unsqueeze(0))

在 forward 方法中,输入是多个图像的补丁嵌入的批量。因为这个原因,我们需要使用 expand 函数来使用 self.cls_token 为批量中的每个图像创建分类 token。

def forward(self, x):tokens_batch = self.cls_token.expand(x.size()[0], -1, -1)

这些分类 token 然后通过使用 torch.cat 方法添加到每个补丁嵌入的开头。

x = torch.cat((tokens_batch, x), dim=1)

位置编码在输出前添加。

x = x + self.pereturn x

注意力头

class AttentionHead(nn.Module):def __init__(self, d_model, head_size):super().__init__()self.head_size = head_sizeself.query = nn.Linear(d_model, head_size)self.key = nn.Linear(d_model, head_size)self.value = nn.Linear(d_model, head_size)def forward(self, x):# 获取 Queries, Keys 和 ValuesQ = self.query(x)K = self.key(x)V = self.value(x)# Queries 和 Keys 的点积attention = Q @ K.transpose(-2, -1)# 缩放attention = attention / (self.head_size ** 0.5)attention = torch.softmax(attention, dim=-1)attention = attention @ Vreturn attention


视觉 Transformer 使用注意力机制,这是一种通信机制,允许模型关注图像的重要部分。注意力得分可以使用图 8 中的公式计算。

计算注意力的第一步是获取令牌的查询、键和值。令牌的查询表示该令牌所寻找的内容,键表示该令牌所包含的内容,而值表示令牌之间传递的信息。可以通过将令牌传递给线性模块来计算查询、键和值。

def forward(self, x):# 获取查询、键和值Q = self.query(x)K = self.key(x)V = self.value(x)

我们可以通过获取查询和键的点积来得到序列中令牌之间的关系。

# 查询和键的点积
attention = Q @ K.transpose(-2, -1)

我们需要对这些值进行缩放,以控制初始化时的方差,从而使令牌能够从多个其他令牌中聚合信息。缩放通过将点积除以注意力头大小的平方根来实现。

attention = attention / (self.head_size ** 0.5)

然后,我们需要对缩放后的点积应用软最大化。

attention = torch.softmax(attention, dim=-1)

最后,我们需要获取软最大化和值矩阵之间的点积。这本质上是传递相应令牌之间的信息。

attention = attention @ V
return attention

多头注意力

class MultiHeadAttention(nn.Module):def __init__(self, d_model, n_heads):super().__init__()self.head_size = d_model // n_headsself.W_o = nn.Linear(d_model, d_model)self.heads = nn.ModuleList([AttentionHead(d_model, self.head_size) for _ in range(n_heads)])def forward(self, x):# 合并注意力头out = torch.cat([head(x) for head in self.heads], dim=-1)out = self.W_o(out)return out

多头注意力是在并行运行多个自注意力头并将它们组合在一起。我们可以通过将注意力头添加到模块列表中来实现这一点,

self.heads = nn.ModuleList([AttentionHead(d_model, self.head_size) for _ in range(n_heads)])

然后通过输入并连接结果。

def forward(self, x):# 合并注意力头out = torch.cat([head(x) for head in self.heads], dim=-1)out = self.W_o(out)return out

Transformer编码器

class TransformerEncoder(nn.Module):def __init__(self, d_model, n_heads, r_mlp=4):super().__init__()self.d_model = d_modelself.n_heads = n_heads# 子层1归一化self.ln1 = nn.LayerNorm(d_model)# 多头注意力self.mha = MultiHeadAttention(d_model, n_heads)# 子层2归一化self.ln2 = nn.LayerNorm(d_model)# 多层感知器self.mlp = nn.Sequential(nn.Linear(d_model, d_model * r_mlp),nn.GELU(),nn.Linear(d_model * r_mlp, d_model))def forward(self, x):# 子层1后的残差连接out = x + self.mha(self.ln1(x))# 子层2后的残差连接out = out + self.mlp(self.ln2(out))return out

Transformer编码器由两个子层组成:第一个子层执行多头注意力,第二个子层包含一个多层感知器。多头注意力子层在令牌之间进行通信,而多层感知器子层允许令牌单独“思考”传递给它们的信息。

层归一化是一种优化技术,它独立地对批次中的每个输入进行特征归一化。对于我们的模型,我们将在每个子层的开始通过一个层归一化模块。

# 子层1归一化
self.ln1 = nn.LayerNorm(d_model)# 子层2归一化
self.ln2 = nn.LayerNorm(d_model)

MLP将由两个线性层和一个GELU层组成。使用GELU而不是RELU是因为它没有RELU在零点不可微的局限性。

# 编码器多层感知器
self.mlp = nn.Sequential(nn.Linear(width, width * r_mlp),nn.GELU(),nn.Linear(width * r_mlp, width)
)

在编码器的forward方法中,输入首先通过第一个层归一化模块,然后执行多头注意力。原始输入加上多头注意力的输出,创建一个残差连接。

然后,这个结果通过另一个层归一化模块,再输入到MLP中。通过将MLP的输出加到第一个残差连接的输出中,创建另一个残差连接。

残差连接用于帮助防止梯度消失问题,通过创建一个梯度可以不受阻碍地反向传播到原始输入的路径。

def forward(self, x):# 子层1后的残差连接out = x + self.mha(self.ln1(x))# 子层2后的残差连接out = out + self.mlp(self.ln2(out))return out

视觉Transformer

class VisionTransformer(nn.Module):def __init__(self, d_model, n_classes, img_size, patch_size, n_channels, n_heads, n_layers):super().__init__()assert img_size[0] % patch_size[0] == 0 and img_size[1] % patch_size[1] == 0, "img_size的维度必须能被patch_size的维度整除"assert d_model % n_heads == 0, "d_model必须能被n_heads整除"self.d_model = d_model  # 模型维度self.n_classes = n_classes  # 类别数量self.img_size = img_size  # 图片大小self.patch_size = patch_size  # patch大小self.n_channels = n_channels  # 通道数量self.n_heads = n_heads  # 注意力头数量self.n_patches = (self.img_size[0] * self.img_size[1]) // (self.patch_size[0] * self.patch_size[1])self.max_seq_length = self.n_patches + 1self.patch_embedding = PatchEmbedding(self.d_model, self.img_size, self.patch_size, self.n_channels)self.positional_encoding = PositionalEncoding(self.d_model, self.max_seq_length)self.transformer_encoder = nn.Sequential(*[TransformerEncoder(self.d_model, self.n_heads) for _ in range(n_layers)])# 分类MLPself.classifier = nn.Sequential(nn.Linear(self.d_model, self.n_classes),nn.Softmax(dim=-1))def forward(self, images):x = self.patch_embedding(images)x = self.positional_encoding(x)x = self.transformer_encoder(x)x = self.classifier(x[:, 0])return x

在创建视觉Transformer类时,我们首先需要确保输入图像可以均匀地分割成patch大小的块,并且模型的维度可以被注意力头的数量整除。

assert img_size[0] % patch_size[0] == 0 and img_size[1] % patch_size[1] == 0, "img_size的维度必须能被patch_size的维度整除"
assert d_model % n_heads == 0, "d_model必须能被n_heads整除"

我们还需要计算位置编码的最大序列长度,该长度等于patch的数量加一。可以通过将输入图像的高度和宽度的乘积除以patch大小的高度和宽度的乘积来找到patch的数量。

self.n_patches = (self.img_size[0] * self.img_size[1]) // (self.patch_size[0] * self.patch_size[1])
self.max_seq_length = self.n_patches + 1

视觉Transformer还需要能够包含多个编码器模块。这可以通过将一系列编码器层放入顺序包装器中实现。

self.encoder = nn.Sequential(*[TransformerEncoder(self.d_model, self.n_heads) for _ in range(n_layers)])

视觉Transformer模型的最后部分是MLP分类头。这由一个线性层和一个softmax层组成。

self.classifier = nn.Sequential(nn.Linear(self.d_model, self.n_classes),nn.Softmax(dim=-1)
)

在forward方法中,输入图像首先通过patch嵌入层,将图像分割成patch并获取这些patch的线性嵌入序列。然后,它们通过位置编码层添加分类令牌和位置编码,再通过编码器模块。分类令牌然后通过分类MLP确定图像的类别。

def forward(self, images):x = self.patch_embedding(images)x = self.positional_encoding(x)x = self.encoder(x)x = self.classifier(x[:, 0])return x

我们完成了模型的构建。现在我们需要训练和测试它。

训练参数

d_model = 9
n_classes = 10
img_size = (32, 32)
patch_size = (16, 16)
n_channels = 1
n_heads = 3
n_layers = 3
batch_size = 128
epochs = 5
alpha = 0.005

加载MNIST数据集

transform = T.Compose([T.Resize(img_size),T.ToTensor()
])train_set = MNIST(root="./../datasets", train=True, download=True, transform=transform
)
test_set = MNIST(root="./../datasets", train=False, download=True, transform=transform
)train_loader = DataLoader(train_set, shuffle=True, batch_size=batch_size)
test_loader = DataLoader(test_set, shuffle=False, batch_size=batch_size)

训练

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print("Using device: ", device, f"({torch.cuda.get_device_name(device)})" if torch.cuda.is_available() else "")transformer = VisionTransformer(d_model, n_classes, img_size, patch_size, n_channels, n_heads, n_layers).to(device)optimizer = Adam(transformer.parameters(), lr=alpha)
criterion = nn.CrossEntropyLoss()for epoch in range(epochs):training_loss = 0.0for i, data in enumerate(train_loader, 0):inputs, labels = datainputs, labels = inputs.to(device), labels.to(device)optimizer.zero_grad()outputs = transformer(inputs)loss = criterion(outputs, labels)loss.backward()optimizer.step()training_loss += loss.item()print(f'Epoch {epoch + 1}/{epochs} loss: {training_loss / len(train_loader) :.3f}')

测试

correct = 0
total = 0with torch.no_grad():for data in test_loader:images, labels = dataimages, labels = images.to(device), labels.to(device)outputs = transformer(images)_, predicted = torch.max(outputs.data, 1)total += labels.size(0)correct += (predicted == labels).sum().item()print(f'\nModel Accuracy: {100 * correct // total} %')

结果

使用此模型,我们在仅训练5个epoch后,在MNIST数据集上实现了约92%的准确率。这个示例展示了自注意力可以作为深度卷积网络的替代方案。

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

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

相关文章

延迟初始化和密封类

Kotlin 延迟初始化(Lazy Initialization) 定义 在 Kotlin 中,延迟初始化允许你延迟一个对象的初始化,直到首次访问该对象时才进行初始化。这通常用于那些初始化开销较大,或者只在程序运行的某个特定点才需要的对象。…

第十二届蓝桥杯物联网试题(国赛)

不得不说国赛相比较省赛而言确实,功能变得更加复杂,更加繁琐,特别是串口LORA通信相结合的更加频繁,且对收取的字符处理要求要更加复杂,处理判别起来会更加复杂。 对于收发数据本身来说,收发的数据本身是以…

springboot webservice接口一个配置文件配置两个接口路径

需求是我们的Java项目中需要和移动端对接接口 ,同时也需要和终端设备对接接口所以为了区分做了两个接口路径: http://localhost:994/pbh/api app接口路径 http://localhost:994/pbh/terminal 终端接口路径 在配置文件中配置两套 Endpoint package com.…

每日一题——Python实现PAT甲级1029 Median(举一反三+思想解读+逐步优化)

一个认为一切根源都是“自己不够强”的INTJ 个人主页:用哲学编程-CSDN博客专栏:每日一题——举一反三Python编程学习Python内置函数 Python-3.12.0文档解读 目录 我的方法 代码功能和结构点评 时间复杂度分析 空间复杂度分析 优化建议 我要更强…

深度学习环境安装教程-anaconda-python-pytorch

首先是anaconda的安装,可以从下面地址下载安装包 Index of /anaconda/archive/ | 清华大学开源软件镜像站 | Tsinghua Open Source Mirror 尽量选择最新的日期的anaconda进行安装,我这里是windows电脑,因此选择了windos-x86_64.exe&#xf…

Git相关命令介绍

Git是一个功能强大的版本控制系统,它提供了一系列的命令来帮助开发者进行日常的版本控制操作。以下是一些常用的Git命令及其简要介绍: 初始化和配置 git init: 初始化一个新的Git仓库。git config: 配置Git的设置,如用户名、邮箱等。 文件…

如何在anaconda的环境下安装langchain

1、安装anaconda; 2、在终端上,输入: conda install langchain -c conda-forge Proceed ([y]/n)? y 输入:Y 3、安装完成后,输入: python -c "import langchain; print(langchain.__version__)&…

Redisson集成SpringBoot

前言:Redisson集成SpringBoot主要有两种方式,一个是使用redisson-spring-boot-starter依赖(优先推荐),毕竟springboot主打的就是约定大于配置,这个依赖就是为springboot准备的。 再一种方式就是引入rediss…

JVM学习-javap解析Class文件

解析字节码的作用 通过反编译生成字节码文件,可以深入了解Java工作机制,但自己分析类文件结构太麻烦,除了第三方的jclasslib工具外,官方提供了javapjavap是jdk自带的反解析工具,它的作用是根据class字节码文件&#x…

JVM(9):虚拟机性能分析和故障解决工具之jmap工具

1 jmap(Memory Map for Java)作用 一个多功能的命令,它可以生成 java 程序的 dump 文件, 也可以查看堆内对象信息、查看 ClassLoader 的信息以及 finalizer 队列 2 命令格式 jmap [options] 参数解释: 第一个参数:options no…

一个投稿好方法让你的文章早日发表

作为一名单位信息宣传员,我初入此行时,满腔热情,怀揣着传播单位价值、展示团队风采的理想,一头扎进了稿件撰写的海洋。我的目标很简单,就是通过文字的力量,让外界听到我们的声音,感受到我们的活力。然而,理想很丰满,现实却给我上了生动的一课。 起初,我遵循传统路径,选择了一家…

QT安装和配置[安装注意点][QT找不到python27.dll][缩小空间]

安装注意点 本文摘录于:https://blog.csdn.net/Python_0011/article/details/131699443只是做学习备份之用,绝无抄袭之意,有疑惑请联系本人! 双击"qt-online-installer-windows-x64-4.8.0.exe"文件后输入账号选择如下控…

云原生架构内涵_1.云原生架构定义

1.云原生架构 从技术的角度,云原生架构是基于云原生技术的一组架构原则和设计模式的集合,旨在将云应用中的非业务代码部分进行最大化的剥离,从而让云设施接管应用中原有的大量非功能特性(如弹性、韧性、安全、可观测性、灰度等&a…

什么是Capto刀柄,一起来认识一下

大家好,今天咱们不聊齿轮,说一说一款刀柄的相关内容。目前,高速加工中心的主轴转速可以达到10,000——50,000r /min ,极大地提高了生产率。高速加工工具系统的主要作用是保证刀具在机床主轴中的精确定位,将主轴的运动和…

车辆工程计算机编程:深度探索与未来挑战

车辆工程计算机编程:深度探索与未来挑战 随着科技的不断进步,车辆工程领域与计算机编程的交融日益紧密,为行业发展注入了新的活力。然而,对于许多人来说,车辆工程计算机编程究竟学什么,仍是一个充满困惑和…

【C++刷题】优选算法——递归第四辑

记忆化搜索篇 什么是记忆化搜索&#xff1f; 带 备忘录 的递归 如何实现记忆化搜索&#xff1f; a.添加一个备忘录 <可变参数&#xff0c;返回值>b.每次递归返回的时候&#xff0c;把结果放到备忘录里c.每次递归进入的时候&#xff0c;先查看一下备忘录 记忆化搜索 vs 常…

对于个人而言,大数据时代如何更好地管理自己的信息?

在大数据时代&#xff0c;管理个人信息变得尤为重要。以下是几个建议来更好地管理个人信息&#xff1a; 认识和了解自己的数字足迹&#xff1a;了解自己在互联网上的活动&#xff0c;包括浏览历史、社交媒体和在线购物数据等。通过查阅自己的帐户设置和隐私选项&#xff0c;可以…

golang信号通知 signal.Notify NotifyContext完整示例

在看示例之前有必要先看看Go程序中信号的默认行为&#xff0c; go中信号的默认行为如下&#xff1a; SIGHUP、SIGINT或SIGTERM信号会导致程序退出。SIGQUIT、SIGILL、SIGTRAP、SIGABRT、SIGSTKFLT、SIGEMT或SIGSYS信号会导致程序退出并进行堆栈转储。SIGTSTP、SIGTTIN或SIGT…

使用Nginx作为反向代理实现MQTT内外网通信

使用Nginx作为反向代理实现MQTT内外网通信 步骤1: 安装Nginx 确保你的服务器上已安装Nginx。如果未安装&#xff0c;可以通过以下命令在Ubuntu上安装Nginx&#xff1a; sudo apt update sudo apt install nginx步骤2: 配置Nginx 编辑Nginx的配置文件&#xff0c;通常是/etc…

全面掌握Prompt提示词技巧

本文综合介绍了Prompt提示词的各种技巧&#xff0c;包括高级提示工程技术、设计提示的通用技巧、优化prompt的十个技巧、AI提示词网站合集、提示工程指南以及ChatGPT提示词技巧等&#xff0c;旨在帮助读者深入理解和应用这些技巧&#xff0c;提高与AI模型的交互效率和质量。 文…