记录学习《手动学习深度学习》这本书的笔记(五)

这一章是循环神经网络,太难了太难了,有很多卡壳的地方理解了好久,比如隐藏层和隐状态的区别、代码的含义(为此专门另写了一篇【笔记】记录对自主实现一个神经网络的步骤的理解)、梯度计算相关(【笔记】记录对python中.grad()的一些理解)。

第八章:循环神经网络

8.1 序列模型

之前介绍了卷积神经网络,了解了怎么将空间结构融入模型,这一章的循环神经网络就是介绍如何将时间序列结构融入模型当中。

通常,在一个有序列结构的模型预测中,对于下一个时间步我们是按:x_{t} \sim P\left \{ x_{t} | x_{t-1} , ... x_{1}\right \}来预测的。

但是这样的话,随着预测步数的推进,复杂度会上升得非常快,所以我们需要想出一些解决方法。

我们想出的方法(自回归模型)有两种:

  • 一种叫自回归模型,是规定取预测步数之前的 \tau 个步数对当前预测步数进行预测,也就是说不需要从 t 步取回第 1 步了,只需要从t步取到第 t-\tau 步,x_{t} \sim P\left \{ x_{t} | x_{t-1} , ... x_{t-\tau }\right \}
  • 一种叫隐变量自回归模型,是我们后面会一直介绍的方法,将第 t 步之前步数的数据用一个隐状态 h_{t} 表示然后预测下一步 t+1 时只要将 h_{t} 和 x_{t+1} 进行线性变化组合得到 h_{t+1} 再用 h_{t+1} 预测第 t+1 步。

 这一章初步实现了一个简单的自回归模型,利用sin函数加一些随机误差生成数据,再利用自回归模型进行训练预测。

内插法是根据现有数据预测单个步的数据,预测数据来自现有数据。

外推法是根据现有数据不断推出后面的数据,每步根据前几步预测出来的数据对未来进行预测。

实验结果表明内推法难度更小,准确率也更高。而外推法得到的结果非常容易偏离正确值,因为预测的错误容易“累计”,不断预测非常容易偏离实际结果。

实验使用k步预测,由k步之前的数据预测第k步的数据,跨度为k,实验结果表明随着k的增加,错误会积累得越多,预测质量急剧下降。

8.2 文本预处理

我们选取《时光机器》中的文本数据。

提取文本步骤包括:

  1. 将文本作为字符串加载到内存中。
  2. 将字符串拆分为词元
  3. 建立词表将词元映射为数字索引
  4. 将文本转换为数字索引

第1步:

#@save
d2l.DATA_HUB['time_machine'] = (d2l.DATA_URL + 'timemachine.txt','090b5e7e70c295757f55df93cb0a180b9691891a')def read_time_machine():  #@save"""将时间机器数据集加载到文本行的列表中"""with open(d2l.download('time_machine'), 'r') as f:lines = f.readlines()return [re.sub('[^A-Za-z]+', ' ', line).strip().lower() for line in lines]lines = read_time_machine()

读取数据,将除了字母以外的字符转为空格(re.sub()正则匹配),去除首位空格(.strip()),将大写转为小写(.lower())

第2步:

将文本行继续拆分为单个词元。

def tokenize(lines, token='word'):  #@save"""将文本行拆分为单词或字符词元"""if token == 'word':return [line.split() for line in lines]elif token == 'char':return [list(line) for line in lines]else:print('错误:未知词元类型:' + token)tokens = tokenize(lines)
for i in range(11):print(tokens[i])

这个函数实现了拆分单词和拆分字母两种方式,默认拆分单词。

返回一些列表,每个列表代表一行,每一行有若干个单词(或字母)。

第3步:

这一步要将词元构成字典的形式,建立一个class类,需要实现将输入的词元匹配上一个对应的数字索引。

class Vocab:  #@save"""文本词表"""def __init__(self, tokens=None, min_freq=0, reserved_tokens=None):if tokens is None:tokens = []if reserved_tokens is None:reserved_tokens = []# 按出现频率排序counter = count_corpus(tokens)self._token_freqs = sorted(counter.items(), key=lambda x: x[1],reverse=True)# 未知词元的索引为0self.idx_to_token = ['<unk>'] + reserved_tokensself.token_to_idx = {token: idxfor idx, token in enumerate(self.idx_to_token)}for token, freq in self._token_freqs:if freq < min_freq:breakif token not in self.token_to_idx:self.idx_to_token.append(token)self.token_to_idx[token] = len(self.idx_to_token) - 1def __len__(self):return len(self.idx_to_token)def __getitem__(self, tokens):if not isinstance(tokens, (list, tuple)):return self.token_to_idx.get(tokens, self.unk)return [self.__getitem__(token) for token in tokens]def to_tokens(self, indices):if not isinstance(indices, (list, tuple)):return self.idx_to_token[indices]return [self.idx_to_token[index] for index in indices]@propertydef unk(self):  # 未知词元的索引为0return 0@propertydef token_freqs(self):return self._token_freqsdef count_corpus(tokens):  #@save"""统计词元的频率"""# 这里的tokens是1D列表或2D列表if len(tokens) == 0 or isinstance(tokens[0], list):# 将词元列表展平成一个列表tokens = [token for line in tokens for token in line]return collections.Counter(tokens)

这个类大概是根据词元出现的频率排序,得出索引。

第4步:

执行:

vocab = Vocab(tokens)
print(list(vocab.token_to_idx.items())[:10])

就可以得到:

[('<unk>', 0), ('the', 1), ('i', 2), ('and', 3), ('of', 4), ('a', 5), ('to', 6), ('was', 7), ('in', 8), ('that', 9)]

执行:

for i in [0, 10]:print('文本:', tokens[i])print('索引:', vocab[tokens[i]])

就可以得到:

文本: ['the', 'time', 'machine', 'by', 'h', 'g', 'wells']
索引: [1, 19, 50, 40, 2183, 2184, 400]
文本: ['twinkled', 'and', 'his', 'usually', 'pale', 'face', 'was', 'flushed', 'and', 'animated', 'the']
索引: [2186, 3, 25, 1044, 362, 113, 7, 1421, 3, 1045, 1]

整合以上功能:

def load_corpus_time_machine(max_tokens=-1):  #@save"""返回时光机器数据集的词元索引列表和词表"""lines = read_time_machine()tokens = tokenize(lines, 'char')vocab = Vocab(tokens)# 因为时光机器数据集中的每个文本行不一定是一个句子或一个段落,# 所以将所有文本行展平到一个列表中corpus = [vocab[token] for line in tokens for token in line]if max_tokens > 0:corpus = corpus[:max_tokens]return corpus, vocabcorpus, vocab = load_corpus_time_machine()
len(corpus), len(vocab)

加载数据,切分为字母,构造词典,整合成一个大列表,可选择是否取前max_tokens个字母。

8.3 语言模型和数据集

在构建模型之前,我们要了解一些关于自然语言处理的相关知识。

基本概率规则:比如 P(我想你) = P(你|我,想)P(想|我)P(我) ,为了得出一个句子的概率,我们需要知道单词出现的概率,以及单词在前几个单词出现的情况下出现的概率。

假设我们有一个非常大的语料库,对于上面式子的条件概率,我们可以通过 P(想|我) = P(我想) / P(我) 得到。

但是对于一些不常见的组合,语料库中出现的概率可能是零,于是就要利用拉普拉斯平滑,使所有语料库中没有出现过的组合概率不为零。

进行自然语言统计时,我们将之前的数据中频率最高的词汇打印出来,发现很多都是the、of、and之类的停用词,并且前几个出现的概率比后面的概率要高很多。

打印出图像可以看出词频从某个临界点开始就下降得特别快。

这意味着单词频率符合齐鲁夫定律。

如果采用之前的平滑方法,尾部数量就会大增。

我们再统计两个词汇组成的词组出现的频率、三个词汇组成的词组出现的频率:

可以看出都是这种情况。

说明拉普拉斯平滑并不适合语言建模,很多n元组很少出现,所以我们使用深度学习模型。

我们读取长序列数据,生成小批量数据作为特征,然后移动生成它的标签。

1.随机抽样:

def seq_data_iter_random(corpus, batch_size, num_steps):  #@save"""使用随机抽样生成一个小批量子序列"""# 从随机偏移量开始对序列进行分区,随机范围包括num_steps-1corpus = corpus[random.randint(0, num_steps - 1):]# 减去1,是因为我们需要考虑标签num_subseqs = (len(corpus) - 1) // num_steps# 长度为num_steps的子序列的起始索引initial_indices = list(range(0, num_subseqs * num_steps, num_steps))# 在随机抽样的迭代过程中,# 来自两个相邻的、随机的、小批量中的子序列不一定在原始序列上相邻random.shuffle(initial_indices)def data(pos):# 返回从pos位置开始的长度为num_steps的序列return corpus[pos: pos + num_steps]num_batches = num_subseqs // batch_sizefor i in range(0, batch_size * num_batches, batch_size):# 在这里,initial_indices包含子序列的随机起始索引initial_indices_per_batch = initial_indices[i: i + batch_size]X = [data(j) for j in initial_indices_per_batch]Y = [data(j + 1) for j in initial_indices_per_batch]yield np.array(X), np.array(Y)my_seq = list(range(35))
for X, Y in seq_data_iter_random(my_seq, batch_size=2, num_steps=5):print('X: ', X, '\nY:', Y)

生成特征和标签:

X:  [[22. 23. 24. 25. 26.][27. 28. 29. 30. 31.]]
Y: [[23. 24. 25. 26. 27.][28. 29. 30. 31. 32.]]
X:  [[ 7.  8.  9. 10. 11.][12. 13. 14. 15. 16.]]
Y: [[ 8.  9. 10. 11. 12.][13. 14. 15. 16. 17.]]
X:  [[17. 18. 19. 20. 21.][ 2.  3.  4.  5.  6.]]
Y: [[18. 19. 20. 21. 22.][ 3.  4.  5.  6.  7.]]

2.顺序分区:

def seq_data_iter_sequential(corpus, batch_size, num_steps):  #@save"""使用顺序分区生成一个小批量子序列"""# 从随机偏移量开始划分序列offset = random.randint(0, num_steps)num_tokens = ((len(corpus) - offset - 1) // batch_size) * batch_sizeXs = np.array(corpus[offset: offset + num_tokens])Ys = np.array(corpus[offset + 1: offset + 1 + num_tokens])Xs, Ys = Xs.reshape(batch_size, -1), Ys.reshape(batch_size, -1)num_batches = Xs.shape[1] // num_stepsfor i in range(0, num_steps * num_batches, num_steps):X = Xs[:, i: i + num_steps]Y = Ys[:, i: i + num_steps]yield X, Yfor X, Y in seq_data_iter_sequential(my_seq, batch_size=2, num_steps=5):print('X: ', X, '\nY:', Y)

生成特征和标签:

X:  [[ 0.  1.  2.  3.  4.][17. 18. 19. 20. 21.]]
Y: [[ 1.  2.  3.  4.  5.][18. 19. 20. 21. 22.]]
X:  [[ 5.  6.  7.  8.  9.][22. 23. 24. 25. 26.]]
Y: [[ 6.  7.  8.  9. 10.][23. 24. 25. 26. 27.]]
X:  [[10. 11. 12. 13. 14.][27. 28. 29. 30. 31.]]
Y: [[11. 12. 13. 14. 15.][28. 29. 30. 31. 32.]]

将两种方法打包:

class SeqDataLoader:  #@save"""加载序列数据的迭代器"""def __init__(self, batch_size, num_steps, use_random_iter, max_tokens):if use_random_iter:self.data_iter_fn = d2l.seq_data_iter_randomelse:self.data_iter_fn = d2l.seq_data_iter_sequentialself.corpus, self.vocab = d2l.load_corpus_time_machine(max_tokens)self.batch_size, self.num_steps = batch_size, num_stepsdef __iter__(self):return self.data_iter_fn(self.corpus, self.batch_size, self.num_steps)
def load_data_time_machine(batch_size, num_steps,  #@saveuse_random_iter=False, max_tokens=10000):"""返回时光机器数据集的迭代器和词表"""data_iter = SeqDataLoader(batch_size, num_steps, use_random_iter, max_tokens)return data_iter, data_iter.vocab

8.4 循环神经网络

循环神经网络将按照之前说的隐状态自回归模型,每一步根据上一步的状态h和当前读取数据进行预测。

隐状态H的作用是保留前面步的历史信息,相当于记忆一样。

这里书中提示了隐状态和隐藏层是不同的,想了老半天为什么,直到看到老师的ppt才懂:

其实两者指的是差不多的东西,只不过隐藏层是指那个层(箭头),而隐状态是指那个层输出的状态(h)。

没有隐状态就是普通神经网络,输入x输出y,中间的隐藏层不和前面的输入有关。

有隐状态就是隐藏层结合了前一个h,输出的是隐状态h,记录的前面的历史数据。

然后值得一提的是所有时间步是共享参数的,也就是h_{2}的计算和h_{3}是类似的这使得循环神经网络的开销不会随着预测步数的增大而增大。

预测值y的计算如下:

y_{t} = H_{t} W_{hq} + b_{q}

隐状态h的计算如下:

H_{t} = \phi (X_{t} W_{xh} + H_{t-1}W_{hh} + b_{h})

循环神经网络对于每个批量(比如you ar)和它的标签(ou are),对每个小批量(一个或多个字母)执行上述计算操作,这样就可以推出下一个字母,如此反复。

由上述隐状态的公式可以看出,我们可以将上一个隐状态和这一个输入横向合并(物理),将权重W纵向合并(物理),两者相乘加b得到的也是同样结果。

最后来看评判模型好坏的困惑度,就是判断这个字母在这个位置出现的合理性(概率),可以用如下方法计算:

\frac{1}{n}\prod_{n}^{t = 1}P(x_{t} | x_{t-1}, x_{t-2} ... x_{1})

累乘会导致数字过大或者过小,于是将其取对数:

exp(\frac{1}{n}\sum_{n}^{t = 1}-log P(x_{t} | x_{t-1}, x_{t-2} ... x_{1}))

这就是困惑度。最好情况为1,最坏情况为正无穷大。

8.5 循环神经网络的从零开始实现

这一节单独放在【笔记】记录对自主实现一个神经网络的步骤的理解里了,只需再介绍一个独热编码的实现:

F.one_hot(torch.tensor([0, 2]), len(vocab))

对0,1两个数字独热编码,长度为字母表长度。

运行结果是:

tensor([[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,0, 0, 0, 0],[0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,0, 0, 0, 0]])

如果传入的是数组,也可以给出对应独热编码,结果上升一个维度。

8.6 循环神经网络的简洁实现

这一章我们构建一个隐藏单元为256的单隐藏层循环神经网络。

像之前那样读取数据:

batch_size, num_steps = 32, 35
train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps)

定义模型:

num_hiddens = 256
rnn_layer = nn.RNN(len(vocab), num_hiddens)

要注意的是,nn.RNN不是整个循环神经网络,而是只是隐藏层这部分,输出的是隐藏层的预测数据,我们还需要实现从隐状态到最后输出层的代码。

初始化隐状态:

state = torch.zeros((1, batch_size, num_hiddens))
state.shape

由于nn.RNN只包含隐藏层,我们还需要建输出层,并将两个层合并成一个完整的循环神经网络:

#@save
class RNNModel(nn.Module):"""循环神经网络模型"""def __init__(self, rnn_layer, vocab_size, **kwargs):super(RNNModel, self).__init__(**kwargs)self.rnn = rnn_layerself.vocab_size = vocab_sizeself.num_hiddens = self.rnn.hidden_size# 如果RNN是双向的(之后将介绍),num_directions应该是2,否则应该是1if not self.rnn.bidirectional:self.num_directions = 1self.linear = nn.Linear(self.num_hiddens, self.vocab_size)else:self.num_directions = 2self.linear = nn.Linear(self.num_hiddens * 2, self.vocab_size)def forward(self, inputs, state):X = F.one_hot(inputs.T.long(), self.vocab_size)X = X.to(torch.float32)Y, state = self.rnn(X, state)# 全连接层首先将Y的形状改为(时间步数*批量大小,隐藏单元数)# 它的输出形状是(时间步数*批量大小,词表大小)。output = self.linear(Y.reshape((-1, Y.shape[-1])))return output, statedef begin_state(self, device, batch_size=1):if not isinstance(self.rnn, nn.LSTM):# nn.GRU以张量作为隐状态return  torch.zeros((self.num_directions * self.rnn.num_layers,batch_size, self.num_hiddens),device=device)else:# nn.LSTM以元组作为隐状态return (torch.zeros((self.num_directions * self.rnn.num_layers,batch_size, self.num_hiddens), device=device),torch.zeros((self.num_directions * self.rnn.num_layers,batch_size, self.num_hiddens), device=device))

类接收一个隐藏层,词汇表大小作为参数,初始化传入参数,并且设置线性层作为由隐状态到输出的函数。

前向传播连接两个函数,先对输入进行独热编码,然后依次放入隐藏层和线性层中计算输出,输出为当前状态和当前输出。

begin_state函数初始化状态,每次新一轮预测就调用这个函数。

实现完整个模型,然后就可以将模型代入之前实现的预测和训练函数。

预测:

device = d2l.try_gpu()
net = RNNModel(rnn_layer, vocab_size=len(vocab))
net = net.to(device)
d2l.predict_ch8('time traveller', 10, net, vocab, device)

训练:

num_epochs, lr = 500, 1
d2l.train_ch8(net, train_iter, vocab, lr, num_epochs, device)

8.7 通过时间反向传播

这一节分析了循环神经网络的梯度传播。

画出流程图可以看出,越后面的步需要回溯越多,这也是循环神经网络容易梯度爆炸和梯度消失的原因。

这一节主要是在用链式计算第t步的梯度公式。

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

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

相关文章

人大金仓数据linux安装注意事项

人大金仓数据linux安装注意事项 本次是个人搭建虚拟机安装centos7的环境下进行安装。 1、安装流程参照https://help.kingbase.com.cn/v9/install-updata/install-linux/preface.html。 2、mount安装文件报错 操作手册提供mount的命令如下&#xff1a; mount KingbaseES_V009R0…

【GIS教程】使用GDAL-Python将tif转为COG并在ArcGIS Js前端加载-附完整代码

目录 一、数据格式 二、COG特点 三、使用GDAL生成COG格式的数据 四、使用ArcGIS Maps SDK for JavaScript加载COG格式数据 一、数据格式 COG&#xff08;Cloud optimized GeoTIFF&#xff09;是一种GeoTiff格式的数据。托管在 HTTP 文件服务器上&#xff0c;可以代替geose…

探索智能时代:如何利用AI一键生成PPT改变演示文稿的制作方式

在这个科技飞速发展的时代&#xff0c;信息的传递方式发生了翻天覆地的变化。曾几何时&#xff0c;我们还在为制作PPT而熬夜&#xff0c;手动选择模板、调整布局&#xff0c;甚至为每一张幻灯片的内容苦思冥想。然而&#xff0c;随着人工智能技术的不断进步&#xff0c;制作PPT…

【LDAP】LDAP概念和原理介绍

目录 一、前言 二、什么是LDAP&#xff1f; 2.1 什么是目录服务&#xff1f; 2.2 LDAP的介绍 2.3 为什么要使用LDAP 三、LDAP的主要产品线 四、LDAP的基本模型 4.1 目录树概念 4.2 LDAP常用关键字列表 4.3 objectClass介绍 五、JXplorer工具使用 一、前言 对于许多的…

nginx模板文件

nginx模板配置 背景模板文件nginx.conf容器验证 背景 nginx通过读取环境变量完成对nginx.conf的相关代理设置&#xff0c;但是nginx.conf不支持直接读取环境变量、所以使用nginx的模板功能达到相应目的 带有环境变量的 Nginx 配置文件模板。在 Docker 化部署 Nginx 时&#x…

善于运用指针(四)--指针数组和多重指针

一个数组的元素均为指针类型&#xff0c;称为指针数组。指针数组中的每一个元素应都有地址&#xff0c;相当于一个变量。 文章目录 前言 一、指针数组的定义 二、指向指针数组的指针变量 1.指针数组 2.多重指针 三、main函数的参数 1.argc argv envp 总结 前言 主要是指针数组…

12篇--图像轮廓绘制与最小外接问题

何为轮廓&#xff1f; 轮廓是一系列相连的点组成的曲线&#xff0c;代表了物体的基本外形。与边缘有什么区别与联系呢&#xff1f; 相对于边缘&#xff0c;轮廓是连续的&#xff0c;边缘不一定连续&#xff0c;如下图所示。其实边缘主要是作为图像的特征使用&#xff0c;比如…

3.8 路由选择器协议

欢迎大家订阅【计算机网络】学习专栏&#xff0c;开启你的计算机网络学习之旅&#xff01; 文章目录 前言1 静态路由选择2 动态路由选择3 自治系统&#xff08;AS&#xff09;4 域内路由选择5 域间路由选择7 路由器基本结构 前言 在计算机网络中&#xff0c;路由选择协议起着至…

#渗透测试#漏洞挖掘#红蓝攻防#SRC漏洞挖掘02之逻辑漏洞技巧

免责声明 本教程仅为合法的教学目的而准备,严禁用于任何形式的违法犯罪活动及其他商业行为,在使用本教程前,您应确保该行为符合当地的法律法规,继续阅读即表示您需自行承担所有操作的后果,如有异议,请立即停止本文章读。 目录 逻辑漏洞技巧 1、任意用户 1.1 验证码可爆…

CVE-2024-38819:Spring 框架路径遍历 PoC 漏洞复现

操作&#xff1a; 根据CVE-2024-38819&#xff1a;Spring 框架路径遍历 PoC 漏洞搭建复现的靶场环境 拿到环境的源码使用docker搭建 cd vuln 创建容器并启动 docker build -t cve-2024-38819-poc .docker run -d -p 8080:8080 --name cve-2024-38819-poc cve-2024-38819-po…

C#调用C++接口时,如何使用结构体参数

在C#中调用C接口时&#xff0c;通常使用平台调用服务&#xff08;P/Invoke&#xff09;或通过C/CLI创建托管包装器来实现。当涉及到结构体参数时&#xff0c;处理方式取决于几个因素&#xff0c;包括结构体的复杂度、是否需要在C和C#之间传递结构体、以及性能考虑。 以下是几种…

在pycharm2024.3.1中配置anaconda3-2024-06环境

version: anaconda3-2024.06-1 pycharm-community-2024.3.1 1、安装anaconda和pycharm 最新版最详细Anaconda新手安装配置环境创建教程_anaconda配置-CSDN博客 【2024最新版】超详细Pycharm安装保姆级教程&#xff0c;Pycharm环境配置和使用指南&#xff0c;看完这一篇就够了…

5.日常算法

1. 面试题 17.14. 最小K个数 题目来源 设计一个算法&#xff0c;找出数组中最小的k个数。以任意顺序返回这k个数均可。 示例&#xff1a; 输入&#xff1a; arr [1,3,5,7,2,4,6,8], k 4 输出&#xff1a; [1,2,3,4] 方法一&#xff1a;堆 class Solution { public:vecto…

数据挖掘与机器学习(part 9) 规则挖掘Rules Mining关联规则(Association Rules) Apriori算法

基于规则的分类器&#xff1a;Classification using rule based classifier 互斥规则&#xff08;Mutually exclusive rules&#xff09;&#xff1a; 分类器包含互斥规则&#xff0c;如果这些规则彼此独立。 每条记录最多被一条规则覆盖。 穷尽规则&#xff08;Exhaustive …

Java Http 接口对接太繁琐?试试 UniHttp 框架吧

前言 从企业级项目来说&#xff0c;如果你项目里还在用传统的编程式Http客户端比如HttpClient、Okhttp去直接对接第三方Http接口&#xff0c; 那么你项目一定充斥着大量的对接逻辑和代码&#xff0c;并且针对不同的对接渠道方需要每次封装一次调用的简化&#xff0c;一旦封装不…

Laravel vs Symfony:哪个框架更适合你?

Laravel vs Symfony&#xff1a;哪个框架更适合你&#xff1f; 在当今的Web开发领域&#xff0c;PHP框架扮演着至关重要的角色。Laravel和Symfony是最受欢迎的两个PHP框架&#xff0c;各自拥有独特的特性和优势。本文将从多个方面对这两个框架进行比较&#xff0c;帮助开发者选…

Java基于SpringBoot的企业OA管理系统,附源码

博主介绍&#xff1a;✌Java老徐、7年大厂程序员经历。全网粉丝12w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;&…

Linux shell的七大功能 --- history

1.直接输入“history” 这个命令可以显示出曾经使用过的命令&#xff08;最近时间的500条&#xff09; history 2.“history”命令也可以搭配其他命令一起使用。 例&#xff1a;history | grep "vim"&#xff0c;找出所有包含“vim”的记录&#xff1b; 也可以搭配…

【PHP】部署和发布PHP网站到IIS服务器

欢迎来到《小5讲堂》 这是《PHP》系列文章&#xff0c;每篇文章将以博主理解的角度展开讲解。 温馨提示&#xff1a;博主能力有限&#xff0c;理解水平有限&#xff0c;若有不对之处望指正&#xff01; 目录 前言安装PHP稳定版本线程安全版解压使用 PHP配置配置文件扩展文件路径…

腾讯云COS跨域访问CORS配置

腾讯云COS跨域访问CORS配置方法如下&#xff0c;参考以下截图&#xff1a; 参考文章&#xff1a; 跨域及CORS-Nginx配置CORS