【深度学习】创建和训练Transformer神经网络模型,将葡萄牙语翻译成英语
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 前言
- 1. 安装
- 2. 数据处理
- 2.1 下载数据集
- 2.2 设置标记器
- 2.3 使用tf.data设置数据管道
- 3. 测试数据集
- 4. 定义组件
- 4.1 嵌入和位置编码层
- 4.2 添加并规范化
- 4.3 基础注意力层
- 4.4 交叉注意层
- 4.5 全局自注意力层
- 4.6 因果自注意力层
- 4.7 前馈网络
- 4.8 编码器层
- 4.9 编码器
- 4.10 解码器层
- 4.11 解码器
- 5. 变压器
- 5.1 超参数
- 5.2 尝试一下
- 6. 训练模型
- 6.1 设置优化器
- 6.2 设置损失和指标
- 6.3 训练模型
- 7. 运行推理
- 8. 创建注意力图
- 9. 导出模型
- 10. 总结
前言
Transformer 是一种深度神经网络,它利用自注意力机制取代了 CNN 和 RNN 。自注意力机制让 Transformer 能够轻松地在输入序列之间传递信息。
正如Google AI 博客文章中所解释的那样:
图 1:将 Transformer 应用于机器翻译。
用于机器翻译的神经网络通常包含一个编码器,用于读取输入句子并生成其表示。然后,解码器在参考编码器生成的表示的同时逐字生成输出句子。Transformer 首先为每个单词生成初始表示或嵌入…然后,它使用自注意力机制聚合来自所有其他单词的信息,根据整个上下文为每个单词生成一个新的表示,由填充的球表示。然后对所有单词并行重复此步骤多次,依次生成新的表示。
Transformer 是一种序列到序列的编码器-解码器模型,类似于NMT with Attention 教程中的模型。单层 Transformer 需要多写一点代码,但与编码器-解码器 RNN 模型几乎相同。唯一的区别是 RNN 层被替换为自注意力层。本教程构建了一个 4 层 Transformer,它更大、更强大,但从根本上来说并不更复杂。
在训练模型后,您将能够输入葡萄牙语句子并返回英语翻译。
Transformer 为何如此重要
- Transformer 擅长对序列数据(例如自然语言)进行建模。
- 与循环神经网络 (RNN)不同,Transformers 是可并行化的。这使得它们在 GPU 和 TPU 等硬件上非常高效。主要原因是 Transformers 用注意力机制取代了循环,计算可以同时进行。层输出可以并行计算,而不是像 RNN 那样串联计算。
- 与RNN(如seq2seq,2014)或卷积神经网络 (CNN)(例如ByteNet )不同,Transformer 能够捕获输入或输出序列中远距离位置之间的数据中的远距离或长距离上下文和依赖关系。因此,可以学习更长的连接。注意力机制允许每个位置在每一层访问整个输入,而在 RNN 和 CNN 中,信息需要经过许多处理步骤才能移动很长的距离,这使得学习变得更加困难。
- Transformers 不对数据中的时空关系做任何假设。这对于处理一组对象(例如星际争霸单位)非常理想。
图 3:在英语到法语翻译上训练的 Transformer 的第 5 层到第 6 层中,单词“it”的编码器自注意力分布(八个注意力头之一)。
1. 安装
首先安装TensorFlow Datasets用于加载数据集,以及安装TensorFlow Text用于文本预处理:
# Install the most re version of TensorFlow to use the improved
# masking support for `tf.keras.layers.MultiHeadAttention`.
apt install --allow-change-held-packages libcudnn8=8.1.0.77-1+cuda11.2
pip uninstall -y -q tensorflow keras tensorflow-estimator tensorflow-text
pip install protobuf~=3.20.3
pip install -q tensorflow_datasets
pip install -q -U tensorflow-text tensorflow
导入必要的模块:
import logging
import timeimport numpy as np
import matplotlib.pyplot as pltimport tensorflow_datasets as tfds
import tensorflow as tfimport tensorflow_text
2. 数据处理
下载数据集和子词标记器,然后将其全部包装在tf.data.Dataset训练中。
2.1 下载数据集
使用 TensorFlow Datasets
加载葡萄牙语-英语翻译数据集D Talks Open Translation Project。该数据集包含约 52,000
个训练样本、1,200
个验证样本和 1,800
个测试样本。
examples, metadata = tfds.load('ted_hrlr_translate/pt_to_en',with_info=True,as_supervised=True)train_examples, val_examples = examples['train'], examples['validation']
TensorFlow Datasets
返回的对象tf.data.Dataset产生文本示例对:
for pt_examples, en_examples in train_examples.batch(3).take(1):print('> Examples in Portuguese:')for pt in pt_examples.numpy():print(pt.decode('utf-8'))print()print('> Examples in English:')for en in en_examples.numpy():print(en.decode('utf-8'))
2.2 设置标记器
现在您已经加载了数据集,您需要对文本进行标记,以便每个元素都表示为一个标记或标记 ID(数字表示形式)。
标记化是将文本分解为“标记”的过程。根据标记器的不同,这些标记可以表示句子片段、单词、子单词或字符。要了解有关标记化的更多信息,请访问此指南。
本教程使用子词标记器教程中内置的标记器。该教程针对此数据集text.BertTokenizer优化了两个对象(一个用于英语,一个用于葡萄牙语),并以 TensorFlow格式导出它们。saved_model
下载、提取并导入saved_model:
model_name = 'ted_hrlr_translate_pt_en_converter'
tf.keras.utils.get_file(f'{model_name}.zip',f'https://storage.googleapis.com/download.tensorflow.org/models/{model_name}.zip',cache_dir='.', cache_subdir='', extract=True
)
tokenizers = tf.saved_model.load(model_name)
包含tf.saved_model两个文本标记器,一个用于英语,一个用于葡萄牙语。两者都具有相同的方法:
[item for item in dir(tokenizers.en) if not item.startswith('_')]
该tokenize方法将一批字符串转换为一批填充的标记 ID。此方法在标记之前拆分标点符号、小写字母并对输入进行 Unicode 规范化。该标准化在此处不可见,因为输入数据已经标准化。
print('> This is a batch of strings:')
for en in en_examples.numpy():print(en.decode('utf-8'))
encoded = tokenizers.en.tokenize(en_examples)print('> This is a padded-batch of token IDs:')
for row in encoded.to_list():print(row)
该detokenize方法尝试将这些令牌 ID 转换回人类可读的文本:
round_trip = tokenizers.en.detokenize(encoded)print('> This is human-readable text:')
for line in round_trip.numpy():print(line.decode('utf-8'))
较低级别的lookup方法将 token-ID 转换为 token 文本:
print('> This is the text split into tokens:')
tokens = tokenizers.en.lookup(encoded)
tokens
输出演示了子词标记化的“子词”方面。
例如将单词分解searchability
为search
和##ability
,将单词serendipity
分解为s
、##ere
、##nd
和##ip
、##ity
请注意,标记化的文本包含[START]
和[END]
标记。
数据集中每个示例的标记分布如下:
lengths = []for pt_examples, en_examples in train_examples.batch(1024):pt_tokens = tokenizers.pt.tokenize(pt_examples)lengths.append(pt_tokens.row_lengths())en_tokens = tokenizers.en.tokenize(en_examples)lengths.append(en_tokens.row_lengths())print('.', end='', flush=True)
all_lengths = np.concatenate(lengths)plt.hist(all_lengths, np.linspace(0, 500, 101))
plt.ylim(plt.ylim())
max_length = max(all_lengths)
plt.plot([max_length, max_length], plt.ylim())
plt.title(f'Maximum tokens per example: {max_length}');
2.3 使用tf.data设置数据管道
以下函数将批量文本作为输入,并将其转换为适合训练的格式。
- 它将它们标记为不规则的批次。
- 它将每个修剪得不超过MAX_TOKENS。
- 它将目标(英语)标记拆分为输入和标签。这些标记会移动一步,这样在每个输入位置上都有下label一个标记的 ID。
- 它将RaggedTensors 转换为填充的密集Tensors。
- 它返回一(inputs, labels)对。
MAX_TOKENS=128
def prepare_batch(pt, en):pt = tokenizers.pt.tokenize(pt) # Output is ragged.pt = pt[:, :MAX_TOKENS] # Trim to MAX_TOKENS.pt = pt.to_tensor() # Convert to 0-padded dense Tensoren = tokenizers.en.tokenize(en)en = en[:, :(MAX_TOKENS+1)]en_inputs = en[:, :-1].to_tensor() # Drop the [END] tokensen_labels = en[:, 1:].to_tensor() # Drop the [START] tokensreturn (pt, en_inputs), en_labels
下面的函数将文本示例数据集转换为批量数据以供训练。
- 它对文本进行标记,并过滤掉太长的序列。(之所以包含batch/,unbatch是因为标记器在处理大批量时效率更高)。
- 该cache方法确保该工作只执行一次。
- 然后shuffle,dense_to_ragged_batch随机化顺序并组装一批例子。
- 最后prefetch将数据集与模型并行运行,以确保在需要时数据可用。
BUFFER_SIZE = 20000
BATCH_SIZE = 64
def make_batches(ds):return (ds.shuffle(BUFFER_SIZE).batch(BATCH_SIZE).map(prepare_batch, tf.data.AUTOTUNE).prefetch(buffer_size=tf.data.AUTOTUNE))
3. 测试数据集
# Create training and validation set batches.
train_batches = make_batches(train_examples)
val_batches = make_batches(val_examples)
生成的tf.data.Dataset
对象已设置为使用 Keras 进行训练。KerasModel.fit
训练需要成对**(inputs, labels)的序列。inputs是标记化的葡萄牙语和英语序列对,(pt, en)是labels**相同的英语序列,但移位了 1。这种移位使得在每个位置输入en序列,label在下一个标记中。
下图:输入在底部,标签在顶部。
这与文本生成教程相同,只是这里有模型“条件化”的额外输入“上下文”(葡萄牙语序列)。
这种设置称为“教师强制”,因为无论模型在每个时间步的输出如何,它都会获得真实值作为下一个时间步的输入。这是一种简单而有效的文本生成模型训练方法。它之所以高效,是因为您不需要按顺序运行模型,不同序列位置的输出可以并行计算。
您可能以为input, output, 对只是序列Portuguese, English。给定葡萄牙语序列,模型将尝试生成英语序列。
可以用这种方式训练模型。您需要写出推理循环并将模型的输出传回输入。这种方式速度较慢(时间步骤不能并行运行),学习难度也较大(模型无法正确得出句子的结尾,除非它能正确得出句子的开头),但它可以提供更稳定的模型,因为模型必须在训练期间学会纠正自己的错误。
for (pt, en), en_labels in train_batches.take(1):breakprint(pt.shape)
print(en.shape)
print(en_labels.shape)
和en相同en_labels,只是移动了 1:
print(en[0][:10])
print(en_labels[0][:10])
4. 定义组件
Transformer 内部有很多事情要做。需要记住的重要事项是:
- 它遵循与带有编码器和解码器的标准序列到序列模型相同的一般模式。
- 如果你一步一步地努力,一切都会变得有意义。
原始 Transformer 图 ------------------------ 4 层 Transformer 的表示
随着您学习本教程的进度,我们将解释这两个图中的每个组件。
4.1 嵌入和位置编码层
编码器和解码器的输入使用相同的嵌入和位置编码逻辑。
给定一个标记序列,输入标记(葡萄牙语)和目标标记(英语)都必须使用一个tf.keras.layers.Embedding
层转换为向量。
整个模型中使用的注意层将其输入视为一组无序的向量。由于模型不包含任何循环层或卷积层。它需要某种方法来识别词序,否则它会将输入序列视为一个词袋,例如how are you,,,,等等,是无法区分的。how you areyou how are
Transformer 为嵌入向量添加了“位置编码”。它使用一组不同频率的正弦和余弦(跨序列)。根据定义,附近的元素将具有相似的位置编码。
原始论文采用以下公式来计算位置编码:
注意:下面的代码实现了它,但不是交错正弦和余弦,而是简单地连接正弦和余弦向量。像这样排列通道在功能上是等效的,而且更容易实现,并在下面的图中显示。
def positional_encoding(length, depth):depth = depth/2positions = np.arange(length)[:, np.newaxis] # (seq, 1)depths = np.arange(depth)[np.newaxis, :]/depth # (1, depth)angle_rates = 1 / (10000**depths) # (1, depth)angle_rads = positions * angle_rates # (pos, depth)pos_encoding = np.concatenate([np.sin(angle_rads), np.cos(angle_rads)],axis=-1) return tf.cast(pos_encoding, dtype=tf.float32)
位置编码函数是一堆正弦和余弦,它们根据沿嵌入向量深度的位置以不同的频率振动。它们在位置轴上振动。
pos_encoding = positional_encoding(length=2048, depth=512)# Check the shape.
print(pos_encoding.shape)# Plot the dimensions.
plt.pcolormesh(pos_encoding.numpy().T, cmap='RdBu')
plt.ylabel('Depth')
plt.xlabel('Position')
plt.colorbar()
plt.show()
根据定义,这些向量与位置轴上的邻近向量很好地对齐。下面对位置编码向量进行归一化,并1000通过点积将位置向量与所有其他向量进行比较:
pos_encoding/=tf.norm(pos_encoding, axis=1, keepdims=True)
p = pos_encoding[1000]
dots = tf.einsum('pd,d -> p', pos_encoding, p)
plt.subplot(2,1,1)
plt.plot(dots)
plt.ylim([0,1])
plt.plot([950, 950, float('nan'), 1050, 1050],[0,1,float('nan'),0,1], color='k', label='Zoom')
plt.legend()
plt.subplot(2,1,2)
plt.plot(dots)
plt.xlim([950, 1050])
plt.ylim([0,1])
因此,使用它来创建一个PositionEmbedding查找标记的嵌入向量并添加位置向量的层:
class PositionalEmbedding(tf.keras.layers.Layer):def __init__(self, vocab_size, d_model):super().__init__()self.d_model = d_modelself.embedding = tf.keras.layers.Embedding(vocab_size, d_model, mask_zero=True) self.pos_encoding = positional_encoding(length=2048, depth=d_model)def compute_mask(self, *args, **kwargs):return self.embedding.compute_mask(*args, **kwargs)def call(self, x):length = tf.shape(x)[1]x = self.embedding(x)# This factor sets the relative scale of the embedding and positonal_encoding.x *= tf.math.sqrt(tf.cast(self.d_model, tf.float32))x = x + self.pos_encoding[tf.newaxis, :length, :]return x
embed_pt = PositionalEmbedding(vocab_size=tokenizers.pt.get_vocab_size().numpy(), d_model=512)
embed_en = PositionalEmbedding(vocab_size=tokenizers.en.get_vocab_size().numpy(), d_model=512)pt_emb = embed_pt(pt)
en_emb = embed_en(en)
en_emb._keras_mask
4.2 添加并规范化
这些“Add & Norm
”块分散在整个模型中。每个块都加入一个残差连接,并通过一个LayerNormalization层运行结果。
组织代码的最简单方法是围绕这些残差块。以下部分将为每个残差块定义自定义层类。
残差“添加和规范”块包含在内,以便提高训练效率。残差连接为梯度提供了直接路径(并确保向量由注意层更新而不是替换),而规范化则为输出保持合理的比例。
4.3 基础注意力层
整个模型都使用了注意力层。除了注意力的配置方式外,这些层都是相同的。每个层都包含一个layers.MultiHeadAttention、一个layers.LayerNormalization和一个layers.Add。
要实现这些注意层,请从仅包含组件层的简单基类开始。每个用例都将作为子类实现。这样编写的代码会多一点,但意图却很明确。
class BaseAttention(tf.keras.layers.Layer):def __init__(self, **kwargs):super().__init__()self.mha = tf.keras.layers.MultiHeadAttention(**kwargs)self.layernorm = tf.keras.layers.LayerNormalization()self.add = tf.keras.layers.Add()
在了解每种用法的具体细节之前,先快速回顾一下注意力的工作原理:
有两个输入:
- 查询序列;正在处理的序列;执行关注的序列(底部)。
- 上下文序列;正在关注的序列(左)。
输出具有与查询序列相同的形状。
常见的比较是,此操作就像字典查找。模糊的、可微的、矢量化的字典查找。
这是一个常规的 Python 字典,其中有 3 个键和 3 个值,通过单个查询传递。
d = {'color': 'blue', 'age': 22, 'type': 'pickup'}
result = d['color']
- squery就是您要查找的内容。
- key字典里有什么样的信息。
- 这value就是那个信息。
当您在普通字典中查找 时query,字典会找到匹配的key,并返回其关联的value。query要么有匹配key,要么没有。您可以想象一个模糊字典,其中的键不必完全匹配。如果您d[“species”]在上面的字典中查找,也许您希望它返回,"pickup"因为这是查询的最佳匹配。
注意层会进行这样的模糊查找,但它不只是寻找最佳键。它values根据query每个匹配程度将 组合起来key。
它是如何工作的?在注意层中query、key和value分别是向量。注意层不会进行哈希查找,而是将query和key向量组合起来以确定它们的匹配程度,即“注意分数”。该层返回所有的平均数values,由“注意分数”加权。
查询序列的每个位置都提供一个query向量。上下文序列充当字典。上下文序列中的每个位置都提供一个key和value向量。输入向量不直接使用,该layers.MultiHeadAttention
层包括layers.Dense
在使用输入向量之前对其进行投影的层。
4.4 交叉注意层
Transformer 的核心是交叉注意力层。该层连接编码器和解码器。该层是模型中最直接使用注意力的层,它执行的任务与 NMT 中的注意力模块相同。
为了实现这一点,您在调用层时将目标序列x作为传递query,并将context序列作为传递:key/valuemha
class CrossAttention(BaseAttention):def call(self, x, context):attn_output, attn_scores = self.mha(query=x,key=context,value=context,return_attention_scores=True)# Cache the attention scores for plotting later.self.last_attn_scores = attn_scoresx = self.add([x, attn_output])x = self.layernorm(x)return x
下面的漫画展示了信息如何流经这一层。柱状图表示上下文序列的加权和。
为了简单起见,未显示残余连接。
输出长度是序列的长度query,而不是上下文序列的长度key/value。
下图进一步简化了。无需绘制整个“注意力权重”矩阵。重点是每个query位置都可以看到上下文中的所有key/value对,但查询之间不会交换任何信息。
在样本输入上进行测试运行:
sample_ca = CrossAttention(num_heads=2, key_dim=512)print(pt_emb.shape)
print(en_emb.shape)
print(sample_ca(en_emb, pt_emb).shape)
4.5 全局自注意力层
该层负责处理上下文序列,并沿其长度传播信息:
由于在生成翻译时上下文序列是固定的,因此信息可以双向流动。
在 Transformer 和自注意力出现之前,模型通常使用 RNN 或 CNN 来完成此任务:
RNN 和 CNN 都有其局限性。
- RNN 允许信息在整个序列中流动,但它需要经过许多处理步骤才能到达那里(限制梯度流)。这些 RNN 步骤必须按顺序运行,因此 RNN 不太能够利用现代并行设备。
- 在 CNN 中,每个位置都可以并行处理,但它仅提供有限的接受场。接受场仅随 CNN 层数线性增长,您需要堆叠多个卷积层才能在序列中传输信息(Wavenet通过使用扩张卷积来减少此问题)。
另一方面,全局自注意层允许每个序列元素直接访问每个其他序列元素,只需少量操作,并且所有输出都可以并行计算。
要实现这一层,您只需将目标序列x作为query和value参数传递给该mha层:
class GlobalSelfAttention(BaseAttention):def call(self, x):attn_output = self.mha(query=x,value=x,key=x)x = self.add([x, attn_output])x = self.layernorm(x)return x
sample_gsa = GlobalSelfAttention(num_heads=2, key_dim=512)print(pt_emb.shape)
print(sample_gsa(pt_emb).shape)
坚持与以前相同的风格,你可以像这样绘制:
再次,为了清楚起见,省略了残差连接。
像这样绘制更加紧凑,并且同样准确:
4.6 因果自注意力层
对于输出序列,该层的作用与全局自注意层类似:
这需要与编码器的全局自注意层以不同的方式处理。
与文本生成教程和带注意力机制的 NMT教程一样,Transformers 是一种“自回归”模型:它们一次生成一个标记的文本,并将该输出反馈给输入。为了提高效率,这些模型确保每个序列元素的输出仅取决于前一个序列元素;这些模型是“因果”的。
单向 RNN 从定义上讲是因果的。要进行因果卷积,您只需填充输入并移动输出,使其正确对齐(使用layers.Conv1D(padding='causal'
))。
因果模型在两个方面是有效的:
- 在训练中,它允许您仅执行一次模型即可计算输出序列中每个位置的损失。
- 在推理过程中,对于生成的每个新标记,您只需计算其输出,前一个序列元素的输出可以重复使用。
- 对于 RNN,您只需要 RNN 状态来考虑之前的计算(传递return_state=True给 RNN 层的构造函数)。
- 对于 CNN,你需要遵循Fast Wavenet的方法
要构建因果自注意力层,需要在计算注意力分数和求和注意力时使用适当的掩码value。
use_causal_mask = True如果你在调用它时传递给该MultiHeadAttention层,则会自动处理这个问题:
class CausalSelfAttention(BaseAttention):def call(self, x):attn_output = self.mha(query=x,value=x,key=x,use_causal_mask = True)x = self.add([x, attn_output])x = self.layernorm(x)return x
因果掩码确保每个位置只能访问它之前的位置:
再次,为了简单起见,省略了残差连接。
该层更紧凑的表示形式为:
测试该层:
sample_csa = CausalSelfAttention(num_heads=2, key_dim=512)print(en_emb.shape)
print(sample_csa(en_emb).shape)
早期序列元素的输出不依赖于后续元素,因此在应用层之前或之后修剪元素都无关紧要:
out1 = sample_csa(embed_en(en[:, :3]))
out2 = sample_csa(embed_en(en))[:, :3]tf.reduce_max(abs(out1 - out2)).numpy()
注意:使用 Keras 蒙版时,无效位置的输出值定义不明确。因此,上述内容可能不适用于蒙版区域。
4.7 前馈网络
Transformer 还在编码器和解码器中包含了这个逐点前馈网络:
该网络由两个线性层(tf.keras.layers.Dense
)组成,中间有一个 ReLU 激活,还有一个 dropout 层。与注意层一样,此处的代码还包括残差连接和规范化:
class FeedForward(tf.keras.layers.Layer):def __init__(self, d_model, dff, dropout_rate=0.1):super().__init__()self.seq = tf.keras.Sequential([tf.keras.layers.Dense(dff, activation='relu'),tf.keras.layers.Dense(d_model),tf.keras.layers.Dropout(dropout_rate)])self.add = tf.keras.layers.Add()self.layer_norm = tf.keras.layers.LayerNormalization()def call(self, x):x = self.add([x, self.seq(x)])x = self.layer_norm(x) return x
测试层,输出与输入形状相同:
sample_ffn = FeedForward(512, 2048)print(en_emb.shape)
print(sample_ffn(en_emb).shape)
4.8 编码器层
编码器包含一组N编码器层。其中每个编码器层EncoderLayer包含一个GlobalSelfAttention和FeedForward层:
这是的定义EncoderLayer:
class EncoderLayer(tf.keras.layers.Layer):def __init__(self,*, d_model, num_heads, dff, dropout_rate=0.1):super().__init__()self.self_attention = GlobalSelfAttention(num_heads=num_heads,key_dim=d_model,dropout=dropout_rate)self.ffn = FeedForward(d_model, dff)def call(self, x):x = self.self_attention(x)x = self.ffn(x)return x
快速测试一下,输出将具有与输入相同的形状:
sample_encoder_layer = EncoderLayer(d_model=512, num_heads=8, dff=2048)print(pt_emb.shape)
print(sample_encoder_layer(pt_emb).shape)
4.9 编码器
接下来构建编码器。
编码器由以下部分组成:
- PositionalEmbedding输入处的一层。
- 一层层的堆叠EncoderLayer。
class Encoder(tf.keras.layers.Layer):def __init__(self, *, num_layers, d_model, num_heads,dff, vocab_size, dropout_rate=0.1):super().__init__()self.d_model = d_modelself.num_layers = num_layersself.pos_embedding = PositionalEmbedding(vocab_size=vocab_size, d_model=d_model)self.enc_layers = [EncoderLayer(d_model=d_model,num_heads=num_heads,dff=dff,dropout_rate=dropout_rate)for _ in range(num_layers)]self.dropout = tf.keras.layers.Dropout(dropout_rate)def call(self, x):# `x` is token-IDs shape: (batch, seq_len)x = self.pos_embedding(x) # Shape `(batch_size, seq_len, d_model)`.# Add dropout.x = self.dropout(x)for i in range(self.num_layers):x = self.enc_layers[i](x)return x # Shape `(batch_size, seq_len, d_model)`.
测试编码器:
# Instantiate the encoder.
sample_encoder = Encoder(num_layers=4,d_model=512,num_heads=8,dff=2048,vocab_size=8500)sample_encoder_output = sample_encoder(pt, training=False)# Print the shape.
print(pt.shape)
print(sample_encoder_output.shape) # Shape `(batch_size, input_seq_len, d_model)`.
4.10 解码器层
解码器的堆栈稍微复杂一些,每个解码器DecoderLayer包含一个CausalSelfAttention、一个CrossAttention和一个FeedForward层:
class DecoderLayer(tf.keras.layers.Layer):def __init__(self,*,d_model,num_heads,dff,dropout_rate=0.1):super(DecoderLayer, self).__init__()self.causal_self_attention = CausalSelfAttention(num_heads=num_heads,key_dim=d_model,dropout=dropout_rate)self.cross_attention = CrossAttention(num_heads=num_heads,key_dim=d_model,dropout=dropout_rate)self.ffn = FeedForward(d_model, dff)def call(self, x, context):x = self.causal_self_attention(x=x)x = self.cross_attention(x=x, context=context)# Cache the last attention scores for plotting laterself.last_attn_scores = self.cross_attention.last_attn_scoresx = self.ffn(x) # Shape `(batch_size, seq_len, d_model)`.return x
测试解码器层:
sample_decoder_layer = DecoderLayer(d_model=512, num_heads=8, dff=2048)sample_decoder_layer_output = sample_decoder_layer(x=en_emb, context=pt_emb)print(en_emb.shape)
print(pt_emb.shape)
print(sample_decoder_layer_output.shape) # `(batch_size, seq_len, d_model)`
4.11 解码器
与 类似Encoder,Decoder由PositionalEmbedding和 堆栈组成DecoderLayer:
通过扩展来定义解码器tf.keras.layers.Layer
:
class Decoder(tf.keras.layers.Layer):def __init__(self, *, num_layers, d_model, num_heads, dff, vocab_size,dropout_rate=0.1):super(Decoder, self).__init__()self.d_model = d_modelself.num_layers = num_layersself.pos_embedding = PositionalEmbedding(vocab_size=vocab_size,d_model=d_model)self.dropout = tf.keras.layers.Dropout(dropout_rate)self.dec_layers = [DecoderLayer(d_model=d_model, num_heads=num_heads,dff=dff, dropout_rate=dropout_rate)for _ in range(num_layers)]self.last_attn_scores = Nonedef call(self, x, context):# `x` is token-IDs shape (batch, target_seq_len)x = self.pos_embedding(x) # (batch_size, target_seq_len, d_model)x = self.dropout(x)for i in range(self.num_layers):x = self.dec_layers[i](x, context)self.last_attn_scores = self.dec_layers[-1].last_attn_scores# The shape of x is (batch_size, target_seq_len, d_model).return x
测试解码器:
# Instantiate the decoder.
sample_decoder = Decoder(num_layers=4,d_model=512,num_heads=8,dff=2048,vocab_size=8000)output = sample_decoder(x=en,context=pt_emb)# Print the shapes.
print(en.shape)
print(pt_emb.shape)
print(output.shape)
sample_decoder.last_attn_scores.shape # (batch, heads, target_seq, input_seq)
创建了 Transformer 编码器和解码器后,就该构建 Transformer 模型并进行训练了。
5. 变压器
现在你有Encoder和Decoder。要完成模型Transformer,你需要将它们放在一起并添加最终的线性(Dense)层,该层将每个位置的结果向量转换为输出标记概率。
解码器的输出是该最终线性层的输入。
和Transformer中各有一层的模型看起来几乎与RNN+attention 教程中的模型完全相同。多层 Transformer 具有更多层,但本质上做的事情相同。EncoderDecoder
Transformer通过扩展来创建tf.keras.Model:
class Transformer(tf.keras.Model):def __init__(self, *, num_layers, d_model, num_heads, dff,input_vocab_size, target_vocab_size, dropout_rate=0.1):super().__init__()self.encoder = Encoder(num_layers=num_layers, d_model=d_model,num_heads=num_heads, dff=dff,vocab_size=input_vocab_size,dropout_rate=dropout_rate)self.decoder = Decoder(num_layers=num_layers, d_model=d_model,num_heads=num_heads, dff=dff,vocab_size=target_vocab_size,dropout_rate=dropout_rate)self.final_layer = tf.keras.layers.Dense(target_vocab_size)def call(self, inputs):# To use a Keras model with `.fit` you must pass all your inputs in the# first argument.context, x = inputscontext = self.encoder(context) # (batch_size, context_len, d_model)x = self.decoder(x, context) # (batch_size, target_len, d_model)# Final linear layer output.logits = self.final_layer(x) # (batch_size, target_len, target_vocab_size)try:# Drop the keras mask, so it doesn't scale the losses/metrics.# b/250038731del logits._keras_maskexcept AttributeError:pass# Return the final output and the attention weights.return logits
5.1 超参数
为了使这个例子保持较小并且相对较快,层数(num_layers)、嵌入的维数(d_model)和层的内部维数FeedForward(dff)都已经减少了。
原始 Transformer 论文中描述的基础模型使用了num_layers=6、d_model=512和dff=2048。
自注意力头的数量保持不变(num_heads=8)。
num_layers = 4
d_model = 128
dff = 512
num_heads = 8
dropout_rate = 0.1
5.2 尝试一下
实例化Transformer模型:
transformer = Transformer(num_layers=num_layers,d_model=d_model,num_heads=num_heads,dff=dff,input_vocab_size=tokenizers.pt.get_vocab_size().numpy(),target_vocab_size=tokenizers.en.get_vocab_size().numpy(),dropout_rate=dropout_rate)
测试一下:
output = transformer((pt, en))print(en.shape)
print(pt.shape)
print(output.shape)
attn_scores = transformer.decoder.dec_layers[-1].last_attn_scores
print(attn_scores.shape) # (batch, heads, target_seq, input_seq)
打印模型摘要:
transformer.summary()
6. 训练模型
现在是时候准备模型并开始训练它了。
6.1 设置优化器
根据原始 Transformer论文中的公式,使用带有自定义学习率调度器的 Adam 优化器。
class CustomSchedule(tf.keras.optimizers.schedules.LearningRateSchedule):def __init__(self, d_model, warmup_steps=4000):super().__init__()self.d_model = d_modelself.d_model = tf.cast(self.d_model, tf.float32)self.warmup_steps = warmup_stepsdef __call__(self, step):step = tf.cast(step, dtype=tf.float32)arg1 = tf.math.rsqrt(step)arg2 = step * (self.warmup_steps ** -1.5)return tf.math.rsqrt(self.d_model) * tf.math.minimum(arg1, arg2)
实例化优化器(在此示例中为tf.keras.optimizers.Adam):
learning_rate = CustomSchedule(d_model)optimizer = tf.keras.optimizers.Adam(learning_rate, beta_1=0.9, beta_2=0.98,epsilon=1e-9)
测试自定义学习率调度程序:
plt.plot(learning_rate(tf.range(40000, dtype=tf.float32)))
plt.ylabel('Learning Rate')
plt.xlabel('Train Step')
6.2 设置损失和指标
由于目标序列是填充的,因此在计算损失时应用填充掩码非常重要。使用交叉熵损失函数 ( tf.keras.losses.SparseCategoricalCrossentropy):
def masked_loss(label, pred):mask = label != 0loss_object = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True, reduction='none')loss = loss_object(label, pred)mask = tf.cast(mask, dtype=loss.dtype)loss *= maskloss = tf.reduce_sum(loss)/tf.reduce_sum(mask)return lossdef masked_accuracy(label, pred):pred = tf.argmax(pred, axis=2)label = tf.cast(label, pred.dtype)match = label == predmask = label != 0match = match & maskmatch = tf.cast(match, dtype=tf.float32)mask = tf.cast(mask, dtype=tf.float32)return tf.reduce_sum(match)/tf.reduce_sum(mask)
6.3 训练模型
所有组件准备就绪后,使用 配置训练过程model.compile,然后使用 运行它model.fit:
transformer.compile(loss=masked_loss,optimizer=optimizer,metrics=[masked_accuracy])
transformer.fit(train_batches,epochs=20,validation_data=val_batches)
7. 运行推理
现在,您可以通过执行翻译来测试模型。以下步骤用于推理:
- 使用葡萄牙语标记器 ( ) 对输入句子进行编码tokenizers.pt。这是编码器输入。
- 解码器输入被初始化为[START]令牌。
- 计算填充掩码和前瞻掩码。
- 然后decoder通过查看encoder output和其自身的输出(自我注意力)来输出预测。
- 将预测的标记连接到解码器输入并将其传递给解码器。
- 在这种方法中,解码器根据其预测的前一个标记来预测下一个标记。
注意:该模型针对高效训练进行了优化,并同时对输出中的每个标记进行下一个标记预测。这在推理过程中是多余的,并且只使用最后一个预测。如果在推理模式下运行时仅计算最后一个预测,则可以使该模型的推理效率更高()。training=False
Translator通过子类化来定义类tf.Module:
class Translator(tf.Module):def __init__(self, tokenizers, transformer):self.tokenizers = tokenizersself.transformer = transformerdef __call__(self, sentence, max_length=MAX_TOKENS):# The input sentence is Portuguese, hence adding the `[START]` and `[END]` tokens.assert isinstance(sentence, tf.Tensor)if len(sentence.shape) == 0:sentence = sentence[tf.newaxis]sentence = self.tokenizers.pt.tokenize(sentence).to_tensor()encoder_input = sentence# As the output language is English, initialize the output with the# English `[START]` token.start_end = self.tokenizers.en.tokenize([''])[0]start = start_end[0][tf.newaxis]end = start_end[1][tf.newaxis]# `tf.TensorArray` is required here (instead of a Python list), so that the# dynamic-loop can be traced by `tf.function`.output_array = tf.TensorArray(dtype=tf.int64, size=0, dynamic_size=True)output_array = output_array.write(0, start)for i in tf.range(max_length):output = tf.transpose(output_array.stack())predictions = self.transformer([encoder_input, output], training=False)# Select the last token from the `seq_len` dimension.predictions = predictions[:, -1:, :] # Shape `(batch_size, 1, vocab_size)`.predicted_id = tf.argmax(predictions, axis=-1)# Concatenate the `predicted_id` to the output which is given to the# decoder as its input.output_array = output_array.write(i+1, predicted_id[0])if predicted_id == end:breakoutput = tf.transpose(output_array.stack())# The output shape is `(1, tokens)`.text = tokenizers.en.detokenize(output)[0] # Shape: `()`.tokens = tokenizers.en.lookup(output)[0]# `tf.function` prevents us from using the attention_weights that were# calculated on the last iteration of the loop.# So, recalculate them outside the loop.self.transformer([encoder_input, output[:,:-1]], training=False)attention_weights = self.transformer.decoder.last_attn_scoresreturn text, tokens, attention_weights
注意:此函数使用展开循环,而不是动态循环。MAX_TOKENS每次调用时都会生成。请参阅NMT with Attention教程,获取使用动态循环的示例实现,这样效率会高得多。
创建此类的一个实例Translator,并尝试几次:
translator = Translator(tokenizers, transformer)
def print_translation(sentence, tokens, ground_truth):print(f'{"Input:":15s}: {sentence}')print(f'{"Prediction":15s}: {tokens.numpy().decode("utf-8")}')print(f'{"Ground truth":15s}: {ground_truth}')
示例 1:
sentence = 'este é um problema que temos que resolver.'
ground_truth = 'this is a problem we have to solve .'translated_text, translated_tokens, attention_weights = translator(tf.constant(sentence))
print_translation(sentence, translated_text, ground_truth)
示例 2:
sentence = 'os meus vizinhos ouviram sobre esta ideia.'
ground_truth = 'and my neighboring homes heard about this idea .'translated_text, translated_tokens, attention_weights = translator(tf.constant(sentence))
print_translation(sentence, translated_text, ground_truth)
示例 3:
sentence = 'vou então muito rapidamente partilhar convosco algumas histórias de algumas coisas mágicas que aconteceram.'
ground_truth = "so i'll just share with you some stories very quickly of some magical things that have happened."translated_text, translated_tokens, attention_weights = translator(tf.constant(sentence))
print_translation(sentence, translated_text, ground_truth)
8. 创建注意力图
您在上一节中创建的类Translator返回一个注意力热图词典,您可以使用它来可视化模型的内部工作。
例如:
sentence = 'este é o primeiro livro que eu fiz.'
ground_truth = "this is the first book i've ever done."translated_text, translated_tokens, attention_weights = translator(tf.constant(sentence))
print_translation(sentence, translated_text, ground_truth)
创建一个函数,在生成标记时绘制注意力:
def plot_attention_head(in_tokens, translated_tokens, attention):# The model didn't generate `<START>` in the output. Skip it.translated_tokens = translated_tokens[1:]ax = plt.gca()ax.matshow(attention)ax.set_xticks(range(len(in_tokens)))ax.set_yticks(range(len(translated_tokens)))labels = [label.decode('utf-8') for label in in_tokens.numpy()]ax.set_xticklabels(labels, rotation=90)labels = [label.decode('utf-8') for label in translated_tokens.numpy()]ax.set_yticklabels(labels)
head = 0
# Shape: `(batch=1, num_heads, seq_len_q, seq_len_k)`.
attention_heads = tf.squeeze(attention_weights, 0)
attention = attention_heads[head]
attention.shape
这些是输入(葡萄牙语)标记:
in_tokens = tf.convert_to_tensor([sentence])
in_tokens = tokenizers.pt.tokenize(in_tokens).to_tensor()
in_tokens = tokenizers.pt.lookup(in_tokens)[0]
in_tokens
这些是输出(英语翻译)标记:
translated_tokens
plot_attention_head(in_tokens, translated_tokens, attention)
def plot_attention_weights(sentence, translated_tokens, attention_heads):in_tokens = tf.convert_to_tensor([sentence])in_tokens = tokenizers.pt.tokenize(in_tokens).to_tensor()in_tokens = tokenizers.pt.lookup(in_tokens)[0]fig = plt.figure(figsize=(16, 8))for h, head in enumerate(attention_heads):ax = fig.add_subplot(2, 4, h+1)plot_attention_head(in_tokens, translated_tokens, head)ax.set_xlabel(f'Head {h+1}')plt.tight_layout()plt.show()plot_attention_weights(sentence,translated_tokens,attention_weights[0])
该模型可以处理不熟悉的单词。 和’triceratops’都不’encyclopédia’在输入数据集中,即使没有共享词汇表,模型也会尝试音译它们。例如:
sentence = 'Eu li sobre triceratops na enciclopédia.'
ground_truth = 'I read about triceratops in the encyclopedia.'translated_text, translated_tokens, attention_weights = translator(tf.constant(sentence))
print_translation(sentence, translated_text, ground_truth)plot_attention_weights(sentence, translated_tokens, attention_weights[0])
9. 导出模型
您已测试模型并且推理正常。接下来,您可以将其导出为tf.saved_model。要了解如何以 SavedModel 格式保存和加载模型,请使用本指南。
通过使用该方法对子类进行子ExportTranslator类化来创建一个调用的类:tf.Moduletf.function__call__
class ExportTranslator(tf.Module):def __init__(self, translator):self.translator = translator@tf.function(input_signature=[tf.TensorSpec(shape=[], dtype=tf.string)])def __call__(self, sentence):(result,tokens,attention_weights) = self.translator(sentence, max_length=MAX_TOKENS)return result
上面tf.function只返回了输出句子。由于执行不严格,所以tf.function不会计算任何不必要的值。
包裹translator新创建的ExportTranslator:
translator = ExportTranslator(translator)
由于模型正在解码预测,因此tf.argmax预测是确定性的。原始模型和从其重新加载的模型SavedModel应该给出相同的预测:
translator('este é o primeiro livro que eu fiz.').numpy()
tf.saved_model.save(translator, export_dir='translator')
reloaded = tf.saved_model.load('translator')
reloaded('este é o primeiro livro que eu fiz.').numpy()
10. 总结
在本教程中,您学习了:
- Transformer 及其在机器学习中的重要性
- 注意力、自注意力和多头注意力
- 嵌入位置编码
- 原始 Transformer 的编码器-解码器架构
- 自我注意力的掩蔽
- 如何将所有内容整合在一起来翻译文本
这种架构的缺点是:
- 对于时间序列,时间步长的输出是根据整个历史记录计算得出的,而不是仅根据输入和当前隐藏状态计算得出的。这可能效率较低。
- 如果输入具有时间/空间关系,如文本或图像,则必须添加一些位置编码,否则模型将有效地看到一个词袋。
如果你想练习,你可以尝试很多事情。例如:
- 使用不同的数据集来训练 Transformer。
- 通过更改超参数,从原始论文创建“Base Transformer”或“Transformer XL”配置。
- 使用此处定义的层来创建BERT的实现
- 使用 Beam search 来获得更好的预测。
基于 Transformer 的模型种类繁多,其中许多模型在 2017 年版原始 Transformer 的基础上进行了改进,采用了编码器-解码器、仅编码器和仅解码器架构。
相关文章:

【深度学习】创建和训练Transformer神经网络模型,将葡萄牙语翻译成英语
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 前言1. 安装2. 数据处理2.1 下载数据集2.2 设置标记器2.3 使用tf.data设置数据管道 3. 测试数据集4. 定义组件4.1 嵌入和位置编码层4.2 添加并规范化4.3 基础注意力…...

[Qt][多元素控件]详细讲解
目录 0.前言1.List Widget2.Table Widget3.Tree Widget 0.前言 Qt中提供的多元素控件有: 列表: QListWidgetQListView 表格: QTableWidgetQTableView 树形: QTreeWidgetQTreeView Widget和View之间的区别,以QTableWi…...

/var/log/里面的文件具体是什么?linux的登录文件
1,什么是登录文件? linux系统官方对登录文件的定义解释我就不说了,我个人理解登录文件其实就是记录系统活动信息的几个文件,登录文件其实就是系统的日志文件。 比如linux系统默认是不会安装nginx的,nginx的日志为/var…...

JVM知识总结(双亲委派机制)
文章收录在网站:http://hardyfish.top/ 文章收录在网站:http://hardyfish.top/ 文章收录在网站:http://hardyfish.top/ 文章收录在网站:http://hardyfish.top/ 双亲委派机制 双亲委派类加载过程 当App尝试加载一个类时&#x…...

YOLOv2:更快更准的目标检测
目录 前言 2.1 简介 2.2 网络结构 2.3 改进方法 2.4 性能表现 前言 自从 You Only Look Once (YOLO) 系列算法问世以来,就以其独特的设计和高效的性能在目标检测领域占据了重要地位。YOLOv1 开创了单阶段检测的新纪元,通过将整个检测过程简化为一个端到端…...
硬件工程师笔面试真题汇总
目录 1、电阻 1)上拉电阻的作用 2)PTC热敏电阻作为电源电路保险丝的工作原理 2、电容 1)电容的特性 2) 电容的特性曲线 3) 1uf的电容通常来滤除什么频率的信号 3、电感 4、二极管 1)二极管特性 2)二极管伏安…...

【vue+marked】marked
一、使用marked 第一步:下载marked和代码块高亮highlight.js npm i markednpm i highlight.jsnpm i markdown-loadernpm i github-markdown-css 第二步:注册并使用 main.js import hljs from "highlight.js"; import "github-markdow…...

无人机之热成像篇
一、定义 无人机热成像技术是指将热成像相机安装在无人机云台上,通过无人机的高空飞行能力和云台的稳定性,结合红外热成像技术对目标区域进行非接触式的温度测量和图像采集。该技术利用物体发出的红外辐射来生成图像,通过测量物体表面温度分布…...

浅谈C/C++指针和引用在Linux和Windows不同环境下的编码风格
目录 0. 前言 1. 代码块、函数体上的 { } 的规范 2. 指针和引用中的 * 和 & 符号的位置 1. Linux 环境下编码风格(gcc) 2. Windows 环境下编码风格(Visual Studio) 3. 简单总结 0. 前言 C/C因为高度的自由性,并没有对一些常见的编码风格进行限制&#…...

【C#】一个项目移动了位置,或者换到其他电脑上,编译报错 Files 的值“IGEF,解决方法
文章目录 1 问题分析2 本文解决方法 一个项目可以正常运行编译的项目,所有路径均为相对路径。 移动了位置,或者换到其他电脑上,编译报错 Files 的值“IGEF, 1 问题分析 这个错误信息表明在处理文件时,Files 的值出…...

代码随想录算法训练营第五十八天|拓扑排序精讲 、dijkstra(朴素版)精讲
拓扑排序 117. 软件构建 from collections import deque, defaultdictdef topological_sort(n, edges):inDegree [0] * n # inDegree 记录每个文件的入度umap defaultdict(list) # 记录文件依赖关系# 构建图和入度表for s, t in edges:inDegree[t] 1umap[s].append(t)# 初…...

【ARM】ULINK Pro如何和SWD接口进行连接调试
【更多软件使用问题请点击亿道电子官方网站】 1、 文档目标 解决ULINK Pro和JTAR接口进行连接问题。 2、 问题场景 因为ULINK Pro本身自带的接口是Cortex-M ETM Interface 20-pin Connector。所以无法和JTAR接口直接进行连接。 图2-1 3、软硬件环境 1)、软件版…...

react框架安全设计
react框架安全设计 1、易受攻击的React版本 React库在过去有一些严重性很高的漏洞,因此最好保持稳定版中的最新版本。 2、数据绑定 使用默认的{}进行数据绑定,React会自动对值进行转义以防止XSS攻击。但注意这种保护只在渲染textContent时候有用,渲染 HTML attributes的…...

Kafka生产调优实践。Kafka消息安全性、消息丢失、消息积压、保证消息顺序性
文章目录 搭建Kafka监控平台合理规划Kafka部署环境合理优化Kafka集群配置优化Kafka客户端使用方式合理保证消息安全消费者防止消息重复消费 生产环境常见问题分析消息零丢失方案消息积压如何处理如何保证消息顺序 搭建Kafka监控平台 官网地址 下载efak-web-3.0.2-bin.tar.gz安…...

DDColor部署安装,在服务器Ubuntu22.04系统——点动科技
DDColor图片上色项目的部署安装,在服务器Ubuntu22.04系统——点动科技 一、ubuntu22.04基本环境配置1.1 更换清华Ubuntu镜像源1.2 更新包列表:2. 安装英伟达显卡驱动2.1 使用wget在命令行下载驱动包2.2 更新软件列表和安装必要软件、依赖2.2 卸载原有驱动…...

使用 SSL/TLS 加密保障 RocketMQ 的安全传输
引言 在现代分布式系统中,数据传输的安全性至关重要。Apache RocketMQ作为一款高性能、高吞吐量的消息中间件,在许多关键应用场景中被广泛使用。为了确保消息传输的安全性,SSL/TLS 加密提供了一种可靠的解决方案。本文将详细介绍如何在 Rock…...

uni-app开发
参考帖 uniapp官方文档 组件库 项目中肯定需要使用第三方组件库,因为现有的这些不够方便我们去使用 uview: 演示 | uView 2.0 - 全面兼容 nvue 的 uni-app 生态框架 - uni-app UI 框架 ThorUI: 介绍 | ThorUI文档 创建uni-app项目 有HBuilder…...

2024社招面经_存储DB广告架构方向
总结 第一次社招,主要是三四月份面的,offer的有高德、拼多多、腾讯、美团、快手、携程,后面面的比较累了,因为美团定级和涨幅都还行就去了美团,没再继续面别的。 因为时间比较久了,只在这里贴一下当时有记…...

android10 系统定制:增加应用锁功能
实现效果如下,上锁应用在桌面或最近任务打开弹出解锁界面,需要解锁成功才能打开应用。解锁界面可点击返回或Home键关闭,非上锁应用可直接打开。 基本思路:拦截系统应用启动,判断应用是否在锁住状态,弹出解锁Window。解锁完成后再正常启动应用。分为从桌面启动和最近任务…...

数据结构----队列
一、队列 1)队列定义 队列(Queue)是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。 允许插入的端是队尾,允许删除的端是队头。队列是一个先进先出(FIFO)的线性表,相应 的也有顺序存储和链式存储两种方式。 2&#…...

【python】实现对文件夹中的图像连续重命名方法
import os import shutildef rename_images(input_folder):# 获取输入文件夹下的所有图片文件(假设都是.jpg格式)image_files [f for f in os.listdir(input_folder) if os.path.isfile(os.path.join(input_folder, f)) and f.endswith(".jpg"…...

【nginx 第一篇章】认识一下 NGINX 服务器
一、简介 Nginx (engine x) 是一个高性能的 HTTP 和反向代理服务器,也是一个 IMAP/POP3/SMTP 代理服务器。由俄罗斯程序员 Igor Sysoev 开发,并在2004年首次公开发布。Nginx 以其高并发处理能力、低内存消耗、稳定性、丰富的功能集、简单的配置以及低学…...

【物联网】(防水篇)哪些电子产品需要通过 IPX7 防水级别测试?
哪些电子产品需要通过 IPX7 防水级别测试? 举例一些可能需要通过 IPX7 防水级别测试的产品 - 电子产品:如智能手机、平板电脑、智能手表、运动手环等,以满足用户在不同场景下的使用需求,例如在潮湿环境或意外沾水时仍能正常工作。…...

高级java每日一道面试题-2024年8月09日-网络篇-什么是XSS攻击如何避免?
如果有遗漏,评论区告诉我进行补充 面试官: 什么是XSS攻击如何避免? 我回答: XSS(Cross-Site Scripting,跨站脚本攻击)是一种常见的Web应用程序安全漏洞,攻击者通过在网页中注入恶意脚本,当其他用户浏览这些网页时&…...

Linux时间管理:命令与脚本的完美结合
目录 前言 Linux时间管理命令 date命令 cron定时任务 at命令 sleep命令 脚本与时间命令的结合使用 备份脚本示例 设置cron任务 监控脚本执行时间 结论 致谢 前言 在Linux系统中,时间管理是一项基础而关键的任务。无论是安排周期性的备份、监控任务的执…...

基于ssm+vue+uniapp的微信外卖小程序
开发语言:Java框架:ssmuniappJDK版本:JDK1.8服务器:tomcat7数据库:mysql 5.7(一定要5.7版本)数据库工具:Navicat11开发软件:eclipse/myeclipse/ideaMaven包:M…...

lvs(linux virtual server)实例
一.lvs概述 1.1什么是lvs LVS(Linux Virtual Server)是一个基于Linux操作系统的虚拟服务器技术,用于实现负载均衡和高可用性。LVS通过将客户端的请求分发到多台后端服务器上,从而提高整体服务的处理能力和可靠性。LVS主要有两个组…...

Unity游戏开发
Unity游戏开发 系列文章的目录: 第一章:Hello,Unity! “好读书,不求甚解;每有会意,便欣然忘食。” 本文目录: Unity游戏开发 Unity游戏开发前言今天我们来体验一下unity开发创建第一…...

5. MQTT消息类型详解(三)
9 SUBACK消息 9.1 消息结构 SUBACK消息是订阅确认消息,格式如下: ------------------------------- | 消息类型(1字节) | ------------------------------- | 保留标志(1字节) …...

TypeScript JSX
介绍 在线的免费的代码转换器工具:可以把HTML 代码移植到 JSX 语法。还支持很多其他的转换。 JSX 是 ECMAScript 的一种类似 XML 的语法扩展,没有任何定义的语义。它不打算由引擎或浏览器实现。它并不是将 JSX 纳入 ECMAScript 规范本身的提议。它旨在供…...