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

动手实现一遍Transformer

最近乘着ChatGpt的东风,关于NLP的研究又一次被推上了风口浪尖。在现阶段的NLP的里程碑中,无论如何无法绕过Transformer。《Attention is all you need》成了每个NLP入门者的必读论文。惭愧的是,我虽然使用过很多基于Transformer的模型,例如BERT,但是对于他们,我也仅仅是会调用而已,对于他们的结构并不熟悉,更不要提修改他们了。
对于Transformer,则更不了解Transformer的细节,直到最近才下定决心复现一遍Transformer。完整的项目链接,我放在GitHub这里了。

工具

我使用的国产的框架,PaddlePaddle。为什么不使用Pytorch呢?因为我的英文并不十分灵光,对于Pytorch的一些API不能准确的理解,有时候理解错一个字就会带来十分巨大的偏差,所以Paddle的中文文档帮了我很大的忙。同时Paddle与Pytorch十分近似的API,也可以帮助我理解Pytorch。

我需要掌握的是Transformer的思想,至于工具的选择,在这个项目上,Paddle与Pytorch并没有什么不同。

模型结构

这里就要祭出这个十分经典的图了。

Transformer架构图

对于这幅图的理解,网上也有很多的介绍,我要做的是复现它。在复现的过程中,我也参考哈佛NLP的Annotated Transformer。那是一篇写的很风骚的代码,但是我认为它并不适合我。

我们就先从输入部分开始说吧:

Embedding

import math
import paddle
import paddle.nn as nn
from paddle import Tensorclass TransformerEmbedding(nn.Layer):def __init__(self, vocab_size, d_model=512):super(TransformerEmbedding, self).__init__()self.d_model = d_modelself.embedding = nn.Embedding(vocab_size, d_model)self.positional_embedding = PositionalEncoding()def forward(self, x: Tensor):""":param x: tensor对象,疑问,这是什么时候转成Tensor的呢?原版的Transformer是使用Tensor生成的数字,所以他不用考虑这个问题。又因为Tensor是无法输入字符串的,所以只能输入字符串对应的数字。或许这就是BERT词表存在的意义。:return:"""return self.embedding(x) + math.sqrt(self.d_model)class PositionalEncoding(nn.Layer):def __init__(self, d_model: int = 512, max_seq_length: int = 1000):"""PE(pos,2i) = sin(pos/100002i/dmodel)通过公式可以知道,位置编码与原来的字信息毫无关系,独立门户的一套操作对于在一句话中的一个字对应的512个维度中,位于偶数位置的使用sin函数,位于基数位置的使用cos函数"""super(PositionalEncoding, self).__init__()self.pe = paddle.tensor.zeros([max_seq_length, d_model])position = paddle.tensor.arange(0, max_seq_length).unsqueeze(1)two_i = paddle.tensor.arange(0, d_model, 2)temp = paddle.exp(-1 * two_i * math.log(10000.0) / d_model)aab = position * temp# position 对应的是词的长度self.pe[:, 0::2] = paddle.sin(aab.cast('float32'))self.pe[:, 1::2] = paddle.cos(aab.cast('float32'))#     pe[max_seq_length, d_model]self.pe = self.pe.unsqueeze(0)#     pe[1,max_seq_length, d_model]def forward(self, x: Tensor):"""词向量+位置编码:param x: x应该是一个[bactch,seq_length,d_model]的数据"""self.pe.stop_gradient = Truereturn x + self.pe[:, x.shape[1]]

在这里的位置编码中,我使用了与哈佛nlp相同的处理,关于这个的理解可以参考The Annotated Transformer的中文注释版(1) - 知乎 (zhihu.com)
公式转换
是数学的力量产生了如此优美的代码。

因为Transformer有很多复用的层,这些复用的层拼接出来了EncoderLayer和DecoderLayer;EncoderLayer堆叠出来了Encoder,DecoderLayer堆叠出来了Decoder。

这些复用的层,我将一一展示:

FeedForward

这是一个很简单的层,就是将输入的结果512维扩展到2048维,然后使用Relu函数后,又降低到原来的512维。

import paddle
import paddle.nn as nnclass FeedForward(nn.Layer):def __init__(self, d_model: int = 512, d_ff=2048):super().__init__()self.lin_to_big = nn.Linear(d_model, d_ff)self.lin_to_small = nn.Linear(d_ff, d_model)def forward(self, x):return self.lin_to_small(paddle.nn.functional.relu(self.lin_to_big(x)))

LayerNorm

这里的代码我是完全copy哈佛nlp的,LayerNorm的思想不是Transformer论文提出的,各大框架也都有自己的实现。我觉得LayerNorm的与Relu这些函数一样,属于基础件,直接调用框架的代码也可以。

import paddle.nn as nn
import paddleclass LayerNorm(nn.Layer):def __init__(self, d_model: int = 512, eps=1e-6):super(LayerNorm, self).__init__()self.a_2 = self.create_parameter(shape=[d_model], dtype='float32',default_initializer=nn.initializer.Constant(1.0))self.b_2 = self.create_parameter(shape=[d_model], dtype='float32',default_initializer=nn.initializer.Constant(0.0))self.eps = epsdef forward(self, x):# 就是在统计每个样本所有维度的值,求均值和方差,所以就是在hidden dim上操作# 相当于变成[bsz*max_len, hidden_dim], 然后再转回来, 保持是三维mean = x.mean(-1, keepdim=True)  # mean: [bsz, max_len, 1]std = x.std(-1, keepdim=True)  # std: [bsz, max_len, 1]# 注意这里也在最后一个维度发生了广播return self.a_2 * (x - mean) / (std + self.eps) + self.b_2

MultiHeadAttention

这是最重要的部分,也是Transformer的精华,讲Transformer其实就是在讲多头注意力机制,我曾经在毕业论文上见过利用注意力机制水论文,但是当时我被唬住了,直到亲手实现过一遍后,我更加确定他们就是在水论文。

相关的解释,我全部加在代码中了。

import copy
import math
from typing import Optionalimport paddle
import paddle.nn as nn
from paddle import Tensorclass MultiHeadAttention(nn.Layer):def __init__(self, d_model: int = 512, head: int = 8):super().__init__()self.head = head"""MultiHeadAttention在
论文中一共出现在了3个地方。在EncoderLayer中一处,在DecoderLay中两处。论文中设置了头的数量为8。其实是分别使用网络为q,k,v进行了8次变换。这个网络映射过程就是论文中提到的权重变换。哈佛论文提出的方法很巧妙,与论文有些出入,所以我并不能理解。于是完全按照论文的思路来实现。为q,k,v分别进行8次变换,那就是需要有24个网络。"""self.linear_list = [copy.deepcopy(nn.Linear(d_model, d_model)) for _ in range(head * 3)]# 这是经过多头注意力的拼接后,将他们恢复到512维。self.linear_output = nn.Linear(d_model * head, d_model)def forward(self, query, encoder_output: Optional[Tensor] = None, mask=False,src_mask: Optional[Tensor] = None,tgt_mask: Optional[Tensor] = None):""":param query: query:param encoder_output: encoder的输出:param mask: 是否是论文中的MASK-multiheadAttention:param src_mask: 来自encoder编码层的掩码,或者是encoder输出的掩码。具体如何判读就是tgt_mask是不是None:param tgt_mask: 来自decoder的掩码:return:"""attention_list = []# 在论文中,self.linear_list的数量是24。for index, linear in enumerate(self.linear_list):if index % 3 == 0:# query永远来自于自家query = linear(query)elif index % 3 == 1:# 对于key来说,编码器没什么好说的;解码器中间的多头注意力,key和value都来自编码器的输出# 在编码器中,都是使用query进行权重变换的。z = query if encoder_output is None else encoder_outputkey = linear(z)else:z = query if encoder_output is None else encoder_outputvalue = linear(z)attention_list.append(attention(query, key, value, self.head, src_mask, tgt_mask, mask=mask))query = paddle.concat(attention_list, axis=-1)return self.linear_output(query)def attention(query: Tensor, key: Tensor, value: Tensor, head: int,src_mask=None,tgt_mask=None,mask=False) -> Tensor:"""计算 Attention 的函数。在函数中,计算出来的scale是矩阵乘法的结果,我们为了“不让解码器看到未来的结果”计算出scale后将相关的部位置设置为一个极小的数字,这样经过softmax后就几乎为0了,达成了“不让解码器看到未来的结果”的效果。这个是用一个下三角矩阵做到的。除此之外,其他的矩阵都是遮掩padding的矩阵,不需要“不让解码器看到未来的结果”:param src_mask::param tgt_mask::return::param query: shape [batch,seq_length,d_model]:param key:同上:param value:同上:param mask:是否开启掩码矩阵。我们要防止模型看到未来的信息,那么未来的信息来自哪里,当然是解码器的输入啦。所以掩码矩阵的shape为[seq_length,seq_length]:param head:头数"""assert query.shape[-1] % head == 0dk = query.shape[-1] // head# paddle的转置操作真奇葩,好像tf也是这样子scale = paddle.matmul(query, paddle.transpose(key, [0, 2, 1]))scale = scale / math.sqrt(dk)if src_mask is not None and tgt_mask is not None:# 这说明是在 DecoderLayer 的第二个多头注意力中。q_sen_length = scale.shape[-2]k_sen_length = scale.shape[-1]batch_size = scale.shape[0]result = []# 这个需要根据src_mask和tgt_mask生成掩码矩阵# src_mask是一个[batch,input_seq_length,input_seq_length]的矩阵,tgt_mask同理,不够这两个矩阵的长度可能会不一样。#比如我爱中国,4个字翻译成英语 i love china 就是3个字。for index in range(batch_size):s = paddle.count_nonzero(src_mask[index])lie = int(math.sqrt(s.item()))p = paddle.count_nonzero(tgt_mask[index])row = int(math.sqrt(p.item()))temp = paddle.zeros([q_sen_length, k_sen_length])temp[:row, :lie] = 1result.append(temp)result_mask = paddle.to_tensor(result)scale = masked_fill(scale, result_mask, -1e9)elif src_mask is not None:# Encoderlayer中的mask,也就是为了遮掩住padding的部分scale = masked_fill(scale, src_mask, -1e9)elif tgt_mask is not None:# decoderlayer中的mask,也就是为了遮掩住padding的部分scale = masked_fill(scale, tgt_mask, -1e9)if mask:# 这里有一个下三角,只有decoderlayerr才会进入,但是我们这里的scale是一个[batch,tgt_length,tgt_length]seq_length = query.shape[-2]down_metric = (paddle.triu(paddle.ones([seq_length, seq_length]), diagonal=1) == 0)scale = masked_fill(scale, down_metric, -1e9)if tgt_mask is not None:assert tgt_mask.shape == scale.shape# tgt_mask也是一个[batch,tgt_length,tgt_length]的矩阵scale = masked_fill(scale, tgt_mask, -1e9)return paddle.matmul(nn.functional.softmax(scale), value)def masked_fill(x, mask, value):"""从paddle官方抄的代码,哈哈:param x::param mask::param value::return:"""mask = paddle.cast(mask, 'bool')y = paddle.full(x.shape, value, x.dtype)return paddle.where(mask, x, y)

接下来就开始拼接了

EncoderLayer

import paddle.nn as nnfrom FeedForward import FeedForward
from LayerNorm import LayerNorm
from MultiHeadAttention import MultiHeadAttentionclass EncoderLayer(nn.Layer):def __init__(self):"""编码器的组成部分,一个多头注意力机制+残差+Norm,一个前馈神经网路+残差+Norm,"""super(EncoderLayer, self).__init__()self.multi_head = MultiHeadAttention()self.feed_forward = FeedForward()self.norm = LayerNorm()def forward(self, x, src_mask=None):""":param x: shape [batch,max_length,d_model]:return:"""y = self.multi_head(x, src_mask=src_mask)y = x + self.norm(y)z = self.feed_forward(y)z = y + self.norm(z)return z

DecoderLayer

import paddle.nn as nn
from paddle import Tensorfrom FeedForward import FeedForward
from LayerNorm import LayerNorm
from MultiHeadAttention import MultiHeadAttentionclass DecoderLayer(nn.Layer):def __init__(self):"""解码器部分,一个带掩码的多头注意力+norm+残差一个不带掩码的多头注意力+norm+残差一个前馈神经网络+norm+残差"""super(DecoderLayer, self).__init__()self.mask_multi_head_attention = MultiHeadAttention()self.multi_head_attention = MultiHeadAttention()self.feed_forward = FeedForward()self.norm = LayerNorm()def forward(self, x, encoder_output: Tensor, src_mask: None, tgt_mask: None):""":param x: decoder 的输入,他的初始输入应该只有一个标记,但是shape依然是[batch,seq_length,d_model]:param encoder_output:编码器的输出"""y = self.mask_multi_head_attention(x, mask=True, tgt_mask=tgt_mask)query = x + self.norm(y)z = self.multi_head_attention(query, encoder_output, src_mask=src_mask, tgt_mask=tgt_mask)z = query + self.norm(z)p = self.feed_forward(z)output = self.norm(p) + zreturn output

Endoder

import copyimport paddle.nn as nnfrom EncoderLayer import EncoderLayerclass Encoder(nn.Layer):def __init__(self, num_layers: int):super(Encoder, self).__init__()self.layers = nn.LayerList([copy.deepcopy(EncoderLayer()) for _ in range(num_layers)])def forward(self, x,src_mask:None):for encoder_layer in self.layers:x = encoder_layer(x,src_mask)return x
.norm(p) + zreturn output

Decoder

import copyimport paddle.nn as nnfrom DecoderLayer import DecoderLayerclass Decoder(nn.Layer):def __init__(self, num_layers: int = 6):super(Decoder, self).__init__()self.decoder_layers = nn.LayerList([copy.deepcopy(DecoderLayer()) for _ in range(num_layers)])def forward(self, x, encoder_output, src_mask, tgt_mask):""":param x: shape [batch,seq_legth,d_model]"""for layer in self.decoder_layers:x = layer(x, encoder_output, src_mask, tgt_mask)return x

最后集成为Transformer,它就是一个编码器,解码器工程。

EncoderDecoder

from typing import Optionalimport paddle
import paddle.nn as nn
from paddle import Tensorfrom Decoder import Decoder
from Embedding import TransformerEmbedding, PositionalEncoding
from Encoder import Encoderclass EncoderDecoder(nn.Layer):def __init__(self, vocab_size: int, d_model: int = 512):super(EncoderDecoder, self).__init__()self.layers_nums = 3self.embedding = nn.Sequential(TransformerEmbedding(vocab_size),PositionalEncoding())self.encoder = Encoder(self.layers_nums)self.decoder = Decoder(self.layers_nums)self.linear = nn.Linear(d_model, vocab_size)self.soft_max = nn.Softmax()self.loss_fct = nn.CrossEntropyLoss()def forward(self, x, label, true_label: Optional[Tensor] = None, src_mask=None, tgt_mask=None):input_embedding = self.embedding(x)label_embedding = self.embedding(label)encoder_output = self.encoder(input_embedding, src_mask)decoder_output = self.decoder(label_embedding, encoder_output, src_mask, tgt_mask)logits = self.linear(decoder_output)res_dict = {}if true_label is not None:loss = self.loss_fct(logits.reshape((-1, logits.shape[-1])),true_label.reshape((-1,)))res_dict['loss'] = lossresult = self.soft_max(logits)max_index = paddle.argmax(result, axis=-1)res_dict['logits'] = resultres_dict['index'] = max_indexreturn res_dict

然后是一个工具类,用于生成词表以及将输入转化为向量。

from typing import Listimport paddle
from paddle import Tensordef convert():chinese = ['你好吗', "我爱你", "中国是一个伟大的国家"]english = ['how are you', 'i love you', 'china is a great country']cc = []for item in chinese:for word in item:# 中文一个字一个字的加入listcc.append(word)for item in english:cc.extend(item.split())word_list = list(set(cc))word_list.sort(key=cc.index)word_list.insert(0, 0)word_list.append(-1)word2id = {item: index for index, item in enumerate(word_list)}id2word = {index: item for index, item in enumerate(word_list)}return word2id, id2worddef convert_list_to_tensor(str_list: List[str], endlish=True) -> (Tensor, Tensor):""":param str_list::return: 原始的id矩阵;处理好了的掩码矩阵"""batch = len(str_list)max_length = 0if endlish:for item in str_list:ll = item.split(' ')max_length = len(ll) if len(ll) > max_length else max_lengthelse:max_length = len(max(str_list, key=len))max_length += 2word2id, id2word, = convert()result = []padding_metric = []pad = -1mask_seq_seq = []for sentence in str_list:ids = [0, ]  # 开始的标志padding_mask = []if endlish:word_list = sentence.split(' ')for word in word_list:ids.append(word2id[word])else:for word in sentence:ids.append(word2id[word])padding_mask.extend([1] * len(ids))ids.append(0)  # 结束的标志pad_nums = max_length - len(ids)ids.extend([word2id[pad]] * pad_nums)padding_mask.extend([0] * (len(ids) - len(padding_mask) - 1))result.append(ids)count = padding_mask.count(1)metric_mask = paddle.zeros([len(padding_mask), len(padding_mask)])metric_mask[:count, :count] = 1mask_seq_seq.append(metric_mask)padding_metric.append(padding_mask)return paddle.to_tensor(result).reshape([batch, -1]), \paddle.to_tensor(padding_metric).reshape([batch, -1]), \paddle.to_tensor(mask_seq_seq).reshape([batch, len(padding_mask), -1]),

接下来这里简单说一下,用到了 Teaching Force 思想。
我们的数据是这样的格式 < begin>内容< end> ,在这个程序中,begin和end都是0。这样的数据,喂给输入端时候去掉最开始的< begin>,在训练时去掉末尾的< /end>喂给 Decoder 。这样做的目的是训练 Decoder 根据自己已经有的信息预测下一个字符的能力。这样做的目的,是因为在测试阶段我们只会给 Decoder 一个< begin> 字符,让 Decoder 根据这个 < begin> 字符和 Encoder 的输出来输出内容。

import paddle
import paddle.nn as nn# 不知道这个有没有用。。
nn.initializer.set_global_initializer(nn.initializer.Uniform(), nn.initializer.Constant())from EncoderDecoder import EncoderDecoder
from utils import convert_list_to_tensordef train():english = ['i love you', 'china is a great country', 'i love china', 'china is a country']chinese = ['我爱你', '中国是一个伟大的国家', '我爱中国', '中国是一个国家']input_ids, _, input_metric = convert_list_to_tensor(english)encod_ids, _, encod_metric = convert_list_to_tensor(chinese, endlish=False)input_ids = input_ids[:, 1:]true_labels = encod_ids[:, 1:]encod_ids = encod_ids[:, :-1]transformer = EncoderDecoder(vocab_size=26, d_model=512)adamw = paddle.optimizer.AdamW(learning_rate=0.001, parameters=transformer.parameters())for epoch in range(700):output_dict = transformer(input_ids, encod_ids, true_labels, src_mask=input_metric, tgt_mask=encod_metric)loss = output_dict['loss']print(f"第{epoch + 1}次训练,loss是{loss.item()},logits是{paddle.tolist(output_dict['index'])}")adamw.clear_gradients()loss.backward()adamw.step()evaluate(transformer)@paddle.no_grad()
def evaluate(model: EncoderDecoder, MAX_LENGTH=6):model.eval()str_list = ['china']enput_ids, _, enput_mask = convert_list_to_tensor(str_list)enput_ids = enput_ids[:, 1:]de_ids = [[0]]de_ids = paddle.to_tensor(de_ids)for i in range(MAX_LENGTH):tgt_mask = paddle.ones([i + 1, i + 1]).unsqueeze(0)output_dict = model(enput_ids, de_ids, src_mask=enput_mask, tgt_mask=tgt_mask)result = output_dict['index']# temp = result[:, -1].item()# if temp == 0:#     print("结束了")#     returng = result[:, -1].unsqueeze(0)de_ids = paddle.concat((de_ids, g), axis=1)print(paddle.tolist(de_ids))if __name__ == '__main__':train()# vocab_size = 11# original = [0, 1, 2, 3, 4, 5, 6, 8, 0]# encode_input = original[1:]# decode_input = original[0:-1]# encode_input = paddle.to_tensor(encode_input).unsqueeze(0)# decode_input = paddle.to_tensor(decode_input).unsqueeze(0)## transformer = EncoderDecoder(vocab_size=vocab_size, d_model=512)# adamw = paddle.optimizer.AdamW(learning_rate=0.001, parameters=transformer.parameters())# for epoch in range(400):#     output_dict = transformer(encode_input, label=decode_input, true_label=encode_input)#     loss = output_dict['loss']#     print(f"第{epoch + 1}次训练,logits是{paddle.tolist(output_dict['index'])},loss是{loss.item()}")#     adamw.clear_gradients()#     loss.backward()#     adamw.step()# evaluate(transformer)

总结

在这个过程中,我深刻的理解了这里的Decoder是串行的,刚开始不知道如何实现,看了TensorFlow的官方实现后才领悟到。

实际上的效果并不是很好,我也不知道是哪里的问题。再使用哈佛nlp的Transformer中,他们的重复数字的例子效果也不好,有可能是数据量太少的原因?

我觉得在亲自动手实现架构的过程,学到的东西要比纸上谈兵多的多。在复现的过程中,也遇到了一些细节问题,有些是框架的,有些是模型的,文章可能也有遗漏错误。欢迎大家提出,我们一起讨论学习。

相关文章:

动手实现一遍Transformer

最近乘着ChatGpt的东风&#xff0c;关于NLP的研究又一次被推上了风口浪尖。在现阶段的NLP的里程碑中&#xff0c;无论如何无法绕过Transformer。《Attention is all you need》成了每个NLP入门者的必读论文。惭愧的是&#xff0c;我虽然使用过很多基于Transformer的模型&#x…...

【Flutter入门到进阶】Flutter基础篇---弹窗Dialog

1 AlertDialog 1.1 说明 最简单的方案是利用AlertDialog组件构建一个弹框 1.2 示例 void alertDialog(BuildContext context) async {var result await showDialog(barrierDismissible: false, //表示点击灰色背景的时候是否消失弹出框context: context,builder: (context)…...

【操作系统】进程和线程的区别

文章目录1. 概述2. 进程3. 线程4. 协程5. 进程与线程区别1. 概述 进程和线程这两个名词天天听&#xff0c;但是对于它们的含义和关系其实还有点懵的&#xff0c;其实除了进程和线程&#xff0c;还存在一个协程&#xff0c;它们的关系如下&#xff1a; 首先&#xff0c;我们需要…...

Linux开发环境配置--正点原子阿尔法开发板

Linux开发环境配置–正点原子阿尔法开发板 文章目录Linux开发环境配置--正点原子阿尔法开发板1.网络环境设置1.1添加网络适配器1.2虚拟网络编辑器设置1.3Ubuntu和Windows网络信息设置Ubuntu网络信息配置方式&#xff1a;1.系统设置->网络->选项2.配置网络文件2源码准备2.…...

Android ThreadPoolExecutor的基本使用

ThreadPoolExecutor是Java中的一个线程池类&#xff0c;Android中也可以使用该类来管理自己的线程池&#xff0c;它为我们管理线程提供了很多方便。 线程池是一种能够帮助我们管理和复用线程的机制&#xff0c;它可以有效地降低线程创建和销毁的开销。使用线程池可以避免不必要…...

基于区域生长和形态学处理的图像融合方法——Matlab图像处理

✅ 大三下时弄的 文章目录最终效果图摘要1 研究背景及意义2 基本原理描述3 实验数据来源3.1 原始图像的来源3.2 天空背景图像的来源4 实验步骤及相应处理结果4.1 原始图像的预处理4.2 区域生长法分割图像4.3 形态学处理填充孔洞4.4 边缘检测根据二值图像构造RGB图像4.5 图像拼接…...

三个案例场景带你掌握Cisco交换机VLAN互通

VLAN间路由的方式现在主流的组网主要是依靠三层交换机通过配置SVI接口【有的厂商叫VLANIF接口】&#xff0c;当然也有比较小型的网络&#xff0c;它就一个出口路由器可管理的二层交换机&#xff0c;还有一种更加差的&#xff0c;就是出口路由一个可管理的二层交换机&#xff0c…...

小白入门之持久连接与非持久连接的差别

对比 HTTP 0.9 已过时 HTTP1.0&#xff1a;非持续连接&#xff0c;每个连接只处理一个请求响应事务&#xff0c;有些服务器端甚至还在用此&#xff0c;可以在一定时间内复用连接&#xff0c;具体复用时间的长短可以由服务器控制&#xff0c;一般在15s左右。 HTTP 1.1 默认使用持…...

TypeScript篇.01-简介,类,接口,基础类型

1.简介(1)安装及编译安装: npm install -g typescript创建 .ts 后缀名的文件编译: tsc 文件名.ts 编译后会生成同名 .js 的文件查看: 在html文件中script引入js文件,运行查看控制台即可(2)类型注解TypeScript里的类型注解是一种轻量级的为函数或变量添加约束的方式 变量或函数声…...

分享几种WordPress怎么实现相关文章功能

一淘模板&#xff08;56admin.com&#xff09;给大家介绍一下WordPress代码实现相关文章的几种方法&#xff0c;希望对大家有所帮助&#xff01; WordPress很多插件可以实现相关文章的功能&#xff0c;插件的优点是配置简单&#xff0c;但是可能会对网站的速度造成一些小的影响…...

PANGO的IOB的电平能力那些事

LVCMOS33 如果要使用33电平&#xff0c;VCCIO则必须供电3V3. 在此制式下&#xff0c;VILMAX为0.8V&#xff0c;VIHMIN为2.0V&#xff0c;即&#xff0c;电平处于0.8V到2.0V之间时&#xff0c;处于浮游态。 VOLMAX是0.4V&#xff0c;VOHMIN是VCCIO-0.4V&#xff0c;折算下来&am…...

scrpy学习-02

新浪微博[Scrapy 教程] 3. 利用 scrapy 爬取网站中的详细信息 - YouTubedef parse(self,response):soup BeautifulSoup(response.body,html.parser)tags soup.find_all(a,hrefre.compile(r"sina.*\d{4}-\d{2}-\d{2}.*shtmls"))#匹配日期for tag in tags:url tag.get(…...

MySQL运维篇之Mycat分片规则

3.5.3、Mycat分片规则 3.5.3.1、范围分片 根据指定的字段及其配置的范围与数据节点的对应情况&#xff0c;来决定该数据属于哪一个分片。 示例&#xff1a; 可以通过修改autopartition-long.txt自定义分片范围。 注意&#xff1a; 范围分片针对于数字类型的字段&#xff0c;…...

vue router elementui template CDN模式实现多个页面跳转

文章目录前言一、elementui Tabs标签页和NavMenu 导航菜单是什么&#xff1f;二、使用方式1.代码如下2.页面效果总结前言 写上一篇bloghttps://blog.csdn.net/jianyuwuyi/article/details/128959803的时候因为整个前端都写在一个index.html页面里&#xff0c;为了写更少的代码…...

ElasticSearch - ElasticSearch基本概念及集群内部原理

文章目录1. ElasticSearch的应用场景01. Elasticsearch 是什么&#xff1f;02. 为何使用 Elasticsearch&#xff1f;03. Elasticsearch 的用途是什么&#xff1f;04. Elasticsearch 的工作原理是什么&#xff1f;05. Elasticsearch 索引是什么&#xff1f;06. Logstash 的用途是…...

【反射中,Class.forName和ClassLoader区别】

在Java中&#xff0c;可以使用反射机制来获取类的信息并动态地创建对象。其中&#xff0c;Class是Java反射机制中的重要类&#xff0c;表示一个类的信息。 Class.forName()和ClassLoader都可以用于获取类的Class对象&#xff0c;但它们之间存在一些差别&#xff1a; 1、是否会…...

2023了为什么还有人在问:女生适合做跨境电商吗?

女生适合做跨境电商吗&#xff1f;这是东哥最近咨询里面问最多的&#xff0c;今天东哥就给大家解答一下你们内心的疑惑&#xff0c;虽然代表的是东哥我自己的观点&#xff0c;但我觉得还是很值得深思的。 女生适合做跨境电商吗&#xff1f; 性别并不是决定一个人是否适合从事跨…...

磁盘分区和挂载

磁盘分区和挂载一、linux分区1.原理介绍2.分区和文件关系示意图&#xff1a;3.硬盘说明二、linux分区1.查看所有设备挂载情况三、挂载案例1.使用lsblk命令查看2. 虚拟机硬盘分区3.虚拟机硬盘分区格式化4.mount挂载 重启挂载失效4.1挂载名词解释4.2注意事项4.3挂载4.4挂载非空目…...

电子技术——晶体管尺寸

电子技术——晶体管尺寸 在本节我们介绍关于IC设计的一个重要的参数晶体管尺寸&#xff08;例如长度和长宽比&#xff09;。我们首先考虑MOS反相器。 反相器尺寸 为了说明 (W/L)(W/L)(W/L) 的尺寸大小以及 (W/L)p(W/L)_p(W/L)p​ 和 (W/L)n(W/L)_n(W/L)n​ 的比例问题对于MO…...

Tuxera NTFS2023MacOS读写软件功能介绍使用

当我们遇到磁盘不能正常使用的情况时本能的会以为是磁盘损坏了&#xff0c;但某些情况下却并非如此。对于mac操作系统来说&#xff0c;软件无法使用设备无法正常读写似乎是很常见的事&#xff0c;毕竟现在的mac电脑对PC机上的产品无法完全适应使用&#xff0c;经常会存在兼容方…...

2022年数维杯国际大学生数学建模挑战赛A题自动地震地平线跟踪解题全过程论文及程序

2022年数维杯国际大学生数学建模挑战赛 A题 自动地震地平线跟踪 原题再现&#xff1a; 随着我国经济社会发展&#xff0c;地质工作的重要性也日益提高。地震资料解释是地震勘探工程的一个重要阶段&#xff0c;可以明确油气勘探的地下构造特征&#xff0c;为油气勘探提供良好和…...

推荐系统[八]:推荐系统常遇到问题和解决方案[物品冷启动问题、多目标平衡问题、数据实时性问题等]

相关文章推荐: 推荐系统[一]:超详细知识介绍,一份完整的入门指南,解答推荐系统相关算法流程、衡量指标和应用,以及如何使用jieba分词库进行相似推荐,业界广告推荐技术最新进展 推荐系统[二]:召回算法超详细讲解[召回模型演化过程、召回模型主流常见算法(DeepMF/TDM/Ai…...

shutil.copyfile PermissionError: [Errno 13] Permission denied

File "G:/od15/调试/翻译文件更换/更新翻译po文件.py", line 42, in <module> shutil.copyfile(gxpath,dir_file_path) File "E:\odsoft\python\lib\shutil.py", line 120, in copyfile with open(src, rb) as fsrc: PermissionError: [Er…...

07react+echart,大屏代码开发

react框架引入第三方插件原链接gitHub:GitHub - hustcc/echarts-for-react: ⛳ Apache ECharts components for React wrapper. 一个简单的 Apache echarts 的 React 封装。import ReactECharts from echarts-for-react;import * as echarts from echarts;一、软件简介echarts-…...

【数据库原理复习】ch2 SQL语句(主要基于sql server)

这里写目录标题基本知识常用基本数据类型字符型数据类型二进制数据类型日期类型数字类型约束条件表SQL语句创建语句修改基本表 & 删除基本表数据查询基本知识 常用基本数据类型 字符型数据类型 名称大小说明char(n)占n个字节只能显示英文字符nchar(n)2n字节2字节额外开销…...

Cadence Allegro 导出Component Pin Report详解

⏪《上一篇》   🏡《上级目录》   ⏩《下一篇》 目录 1,概述2,Component Pin Report作用3,Component Pin Report示例4,Component Pin Report导出方法4.1,方法14.2,方法2B站关注“硬小二”浏览更多演示视频 1,概述...

PAT甲级 1110 Complete Binary Tree

题目链接 PAT甲级 1110 Complete Binary Tree 思路 第一次的写法不是很好。 对于这种完全二叉树的层序遍历&#xff0c;比较烦人的就是空孩子使得处理很麻烦。 思来想去还是把空位置也入队比较好。 这样的话&#xff0c;访问到空指针的时机被推迟了一个level 而完全二叉树的…...

【JavaSE】逻辑控制语句

文章目录一. 顺序结构二. 分支结构1. if 语句2. switch 语句3、循环结构3.1 while 循环3.2 do while 循环3.3 for 循环3.4 break 和 continue三. 输入输出1. 输出到控制台2. 从键盘输入一. 顺序结构 顺序结构比较简单&#xff0c;即程序按照代码书写的顺序一行一行执行下去。 …...

Motionbuilder系统文件说明

安装路径 Motionbuilder 默认的安装路径在 C:\Program Files\Autodesk\MotionBuilder\ 用户数据(user data) 位于安装路径下的 bin\config 非管理员用户的配置文件路径 Motionbuilder会将配置文件备份到 \Users[user]\AppData\Local\Autodesk[MotionBuilder] 当用户第一次打开…...

【我的Android开发】AMS中Activity栈管理

概述 Activity栈管理是AMS的另一个重要功能&#xff0c;栈管理又和Activity的启动模式和startActivity时所设置的Flag息息相关&#xff0c;Activity栈管理的主要处理逻辑是在ActivityStarter#startActivityUnchecked方法中&#xff0c;本文也会围绕着这个方法进进出出&#xf…...