NLP从零开始------13.文本中阶序列处理之语言模型(1)

news2024/11/15 5:17:14

        语言模型( language model) 用于计算一个文字序列的概率, 评估该序列作为一段文本出现在通用或者特定场景中的可能性。每个人的语言能力蕴涵了一个语言模型,当我们说出或写下一段话的时候,已经在不自觉地应用语言模型来帮助我们决定这段话中的每个词,使之通顺合理。语言模型在自然语言处理中也有诸多应用, 例如, 当我们使用拼音中文输入法输入“ ziranyuyan”, 输出的候选文字中“自然语言”比“孜然鱼雁”更靠前,这是因为中文输入法所使用的语言模型判断出前者的概率更高。类似地,在机器翻译、拼写检查、语音识别等应用中,语言模型也被用来在多项候选文字中选择更合理、更有可能出现的文字。
        本节将首先概述语言模型,然后介绍不同的方法来实现语言模型, 包括最简单的n元语法模型、更复杂但效果更好的循环神经网络、在循环神经网络的基础上引入的注意力机制,以及纯粹基于注意力机制的 Transformer模型。

1. 概述

        要想得到一个语言模型,最简单的想法是从一个大型语料库中直接统计不同文字序列出现的频率。然而由于文字序列的排列组合空间极大,不可能找到一个包含所有合理的文字序列的语料库,因此这个想法是不可行的。既然序列的概率无法通过经验频率来估计, 那么是否可以通过概率乘法公式将其转换为一系列条件概率的乘积,转而估算这些条件概率呢?
                                      P(w_{1},w_{2}, \cdots ,w_{n})= \prod \limits _{i}^{n}P(w_{i}|w_{1},w_{2}, \cdots ,w_{i-1})
        其中, w_{i}表示输入文字序列中的第i个词。那么这个序列“自然语言”的概率可以分解为
                                        P(自然语言)=P(自)×P(然|自)×P(语|自然)×P(言|自然语)
        这种分解方式的一个潜在好处在于,一旦能够成功估算所有可能的条件概率, 就可以用它们来生成文本。具体而言,首先根据第一个词的概率分布采样出第一个词,再根据给定第一个词时第二个词的条件概率分布采样出第二个词,然后根据给定前两个词时第三个词的条件概率分布采样出第三个词,以此类推。这种逐个词依次输出,每一步根据已输出的词决定下一个词的过程称为自回归 ( auto- regressive) 过程。

        那么如何估算这些条件概率呢? 最直接的想法是最大似然估计:

                                P(言|自然语) = count(自然语言)/ count(自然语)
        但这显然也是不可行的,同样因为我们无法找到一个足够大的包含所有合理文字序列的语料库来估算频率。因此,人们发展出了n元语法模型、循环神经网络、 Transformer模型等一系列方法来计算这些条件概率。

        (1) n元语法(n- gram)模型: 每个词的概率仅以前n-1个词为条件。
        (2) 循环神经网络( recurrent neural network, RNN): 每个词的概率以一个包含前置序列全部信息的稠密向量为条件。
        (3) Transformer模型: 每个词的概率通过对前置所有词使用注意力机制得到。

        本节将对这些方法的细节进行详细介绍。除了这些方法,还存在一些更加复杂的方法,如基于句法结构的生成式文法等,后面会介绍其中一些方法,这里不再展开。
        所有这些方法所共同面临的一个问题是,如何处理在模型训练时没有见过的词,即所谓未登录( out- of- vocabulary, OOV) 词。一个常用方法是引入一个特殊词“[UNK]”: 在训练时,创建一个固定的词表(如所有高频词),将训练语料库中所有未在词表中出现的词都替换为“[UNK]”, 并将“[UNK]”作为一个正常词估算概率; 在测试时, 使用“[UNK]”的概率来代替任何未登录词的概率。除这个方法之外,还可以在字符或者子词级建立语言模型。因为任何词都可以拆解为若干字符或子词的组合,而字符或子词的个数较少,所以这样的语言模型能够涵盖所有字符或子词,从而涵盖所有可能的文字序列。

        下面最后讨论如何评估一个语言模型的质量。一种方式是在下游任务(如机器翻译、语音识别等) 中检验语言模型的性能,但这往往比较费时费力, 并且不同下游任务的评估结果有可能大相径庭。因此,评估语言模型的通用方法是使用困惑度( perplexity), 即评估模型是否给训练语料库之外的真实测试语言语料库赋予较大的概率。对于测试语料库 \bar{x}_{1:m}(m个序列),使用待评估模型计算每个词的平均负对数概率:
                                                l=- \frac {1}{M} \sum \limits _{i=1}^{m} \log _{2}P( \overline {x}_{i})
        其中, M为测试语料库中的总词数。该评价指标相当于编码每个词平均所需的比特数,其二次幂2'就被称为测试数据的困惑度。困惑度越小则测试语料库的概率越大,因此可认为被评估模型的质量越高。困惑度的最小值是1,这仅当所有测试语料的概率均为1的极端情况下才能取得。需要特别注意的是,词表不同的两个语言模型,其困惑度是不可比较的,显然,词表较小的语言模型平均会给每个词更高的概率,因而更有可能具有较低的困惑度,极端情况下,如果词表只包含“[UNK]”这一个词, 那么模型的困惑度可以达到完美的1。但词表过小的语言模型会将过多的词当作“[UNK]”,缺乏区分度,因而不是一个好的语言模型。因此,要比较不同的语言模型方法时, 需要使用统一的词表。

2. n元语法模型

        在基础篇我们曾经谈论过n元语法模型,这里我们更深入讲解一番。下面是上文提到原处。

NLP从零开始------4基础文本处理之分词(1)_nlp 分词 词性标注-CSDN博客

        在上面,我们使用概率乘法公式分解文字序列的概率, 但无法对分解得到的条件概率进行估算。为了估算这些条件概率,可以引入马尔可夫假设,即假设每个词只依赖它前面的n-1个词。
                ​​​​​​​        P(w_{i}|w_{1},w_{2}, \cdots ,w_{i-1})=P(w_{i}|w_{i-n+1}, \cdots ,w_{i-1})
        上述方法被称为n元语法模型。所谓n元语法是指文本中的连续n个词。最简单的情况为一元语法 ( unigram) 模型:
        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​P(w_{1},w_{2}, \cdots ,w_{n})= \prod \limits _{i}P(w_{i})
        一元语法模型假设每个词出现的概率独立于其他词,这类似于 4.2.1节中朴素贝叶斯模型所做的假设。二元语法( bigram)模型则假设每个词只与上一个词有关,而和其他词无关:
        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        P(w_{i}|w_{1},w_{2}, \cdots ,w_{l-1})=P(w_{l}|w_{l-1})
        类似地,可以定义三元语法模型、四元语法模型等。由于n元语法模型对于条件概率的限制条件是只包含n-1个词的序列,因此当n较小时,可能的条件序列也相对较少,可以从语料库中通过统计频率来估算。

        n元语法模型的一个缺点在于无法建模所谓的长距离依赖(即距离大于n的两个词之间的依赖关系)。长距离依赖在自然语言中很常见,例如英语中动词所采用的单复数形式取决于主语,但动词与其主语之间可能间隔任意多个词(如对于一个很长的定语从句)。n元语法模型的另一个缺点是需要存储大量的条件概率,当n较大时模型会非常巨大。尽管有这些缺点, n元语法模型仍有很不错的性能,在神经网络语言模型出现之前是最为成功的语言模型。
        接下来讨论如何从语料库中估算n元语法模型的条件概率。最简单的方式是最大似然估计:
        ​​​​​​​        ​​​​​​​        P(w_{i}|w_{i-n+1}, \cdots ,w_{i-1})= \frac {count(w_{i-n+1}, \cdots ,w_{i-1},w_{i})}{count(w_{i-n+1}, \cdots ,w_{i-1})}
        但这样做的一个常见问题是数据稀疏性问题: 尽管限制了序列的长度,但是不同的n元语法仍然是非常多的,因此一些合理的n元语法可能不会在训练语料库中出现,从而导致相应的条件概率估算为0。这样一来,模型会将包含这些n元语法的文本的概率计算为0,这是不合理的。
        常见的处理数据稀疏性问题的方法有以下几类:平滑法(拉普拉斯平滑)、回退法(找不到对应的n元语法时使用n-1元语法,仍找不到时使用n-2元语法, 以此类推)、插值法(将n元语法模型、n-1元语法模型、n-2元语法模型等一系列模型加权平均)。其中最为成功的方法是改良型 Kneser- Ney 平滑。

        一个著名的n元语法数据集和可视化界面是 Google Ngram Viewer, 包含几百年来多种语言的公开文献中n元语法的出现频率统计, 可以查询其官网了解详细信息。


 

3. 循环神经网络(RNN)

        基于神经网络的语言模型可以避免n元语法模型的各种缺点。神经网络语言模型中最基础的模型之一是循环神经网络( recurrent neural network, RNN), 它使用隐状态 ( hidden state)来保存历史信息,并使用循环结构逐一处理输入序列中的每一个元素。接下来首先介绍最基本的循环神经网络语言模型,然后介绍循环神经网络的两个著名变体——长短期记忆( long short- term memory, LSTM)和门控循环单元( gated recurrent unit, GRU)(门控循环单元可以看作长短期记忆的一种简化变体), 最后介绍多层双向循环神经网络。


3.1 循环神经网络

        循环神经网络的基本思想是在计算文字序列中每个词的条件概率时,计算一个稠密向量来表示条件(也就是这个词的前置序列)所包含的信息,然后用该向量来计算条件概率分布。循环神经网络的架构如下图所示。        

用于语言模型的循环神经网络

        

        在这个模型结构中,每一步网络的输入包含两部分,一部分是历史输入h^{t-1},是前置序列的总结, 称为隐状态,另一部分是新的输入x^{t},此处为词w_{t}所对应的词嵌入, 模型的输出经过 softmax函数处理得到下一步的词的分布,例如“自然语言”的下一个词可能是“处(理)”“生(成)”等。计算公式如下。
        隐状态:
                                                        h⁽ᵗ⁾=σ(Wₓx⁽ᵗ⁾+Wₕh⁽ᵗ⁻¹⁾+bₕ)

        输出:

        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​\hat {y}^{(t)}=soft \max (W_{o}h^{(t)}+b_{o})
        其中,Wx为输入的参数,W_{h}b_{n}为隐藏层的参数,W_{o}b_{o}为输出的参数,σ()在此处表示激活函数。初始隐状态h^{0}在没有其他信息的情况下一般设为全0。

        循环神经网络的显著好处在于,解除了n元语法模型中对条件序列长度的限制,即不再使用马尔可夫假设,因而每一时刻的输出(即条件概率分布)都基于整个前置序列作为条件。并且,由于不需要像n元语法模型那样存储大量的条件概率,循环神经网络往往比典型的n元语法模型小, 即具有更少的模型参数。

        下面讨论循环神经网络的训练。在每一步t, 训练损失函数为下一个词的预测分布\dot{y}^{t}与真实分布y^{*t}(即下一个词w_{t+1}的独热编码) 的交叉熵( cross entropy):

        ​​​​​​​        ​​​​​​​        ​​​​​​​        J^{(t)}( \theta )=H(y^{ \ast (t)}, \hat {y}^{(t)})=- \sum \limits _{w \in \nu }y^{ \ast (t)}_{ w } \log \hat {y}_{ w }^{(t)}=- \log \hat {y}_{w_{i+1}}^{(t)}

        其中, θ为模型参数,H(P,Q)=-∑ₓP(x)logQ(x)为两个概率分布P、Q的交叉熵, V为词表。注意, 真实分布y^{t}只有在词w_{t+1}上概率为1, 在其余词上的概率为0, 因此可以去除求和符号而仅保留非零项, 即预测分布在词w_{t+1}上的概率。
总损失函数即为每一步交叉熵的平均值:
        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​J( \theta )= \frac {1}{T} \sum \limits _{t=1}^{T}J^{(t)}( \theta )= \frac {1}{T} \sum \limits _{t=1}^{T}- \log \hat {y}_{w_{i+1}}^{(t)}
        其中, T为总词数。该损失函数可以使用随机梯度下降进行优化。

下面用代码实现循环神经网络。首先读取数据,使用《小王子》这本书作为训练语料库。

import os
import sys

# 导入前面实现的小王子数据集
sys.path.append('./code')
from utils import TheLittlePrinceDataset

dataset = TheLittlePrinceDataset()

# 统计每句话的长度
sent_lens = []
max_len = -1
for sentence in dataset.tokens:
    sent_len = len(sentence)
    sent_lens.append(sent_len)
    if sent_len > max_len:
        max_len = sent_len
        longest = sentence
print(max_len)

# 简单看一下语料中序列长度的分布
import matplotlib.pyplot as plt
plt.hist(sent_lens, bins=20)
plt.show()

        然后建立词表,截断过长的序列, 将序列填充( padding) 为相同长度。实践中容易遇到极长的句子,这样的句子数量稀少但可能包含大量字符,如果不截断的话会极大拖慢模型的运行效率、占用大量显存甚至导致运行出错。因此在预处理阶段,可以对训练数据中过长的句子进行截断,即把预先确定的最大长度之外的词删去。注意, 一般不对测试数据进行截断,以便更完整地评估模型,并确保与其他工作的评估结果可比。为了使模型能够并行处理一个批次中不同长度的输入序列,可以使用填充将不同长度的输入对齐,常见做法是在长度不足的序列末尾持续添加特殊字符(如“[PAD]”)直到序列达到设定长度。填充可以在预处理阶段进行,这样会把所有输入序列填充为相同长度,也可以在训练过程中进行, 根据每个批次的序列长度动态设置填充长度。预处理阶段进行填充的好处是只需处理一次数据,并且处理完成的数据可以保存下来多次使用。训练过程中进行填充的好处是插入的特殊字符更少,减少了计算量。
 

import numpy as np

dataset.build_vocab()
sent_tokens = dataset.convert_tokens_to_ids()
# 截断和填充
max_len=40
for i, tokens in enumerate(sent_tokens):
    tokens = tokens[:max_len]
    tokens += [dataset.token2id['<pad>']] * (max_len - len(tokens))
    sent_tokens[i] = tokens
sent_tokens = np.array(sent_tokens)

print(len(dataset.tokens), max([len(x) for x in dataset.tokens]))
print(sent_tokens.shape)
print(sent_tokens[0])
1105 115
(1104, 40)
[  4  17  20 742 743 744 742 743 744   2  62  19   9   1   1   2   1  10
 745 746   4  17  20  21   1   2  30 335 194  33 299   3   0   0   0   0
   0   0   0   0]
import torch
from torch import nn
import torch.nn.functional as F

# 定义一个正态分布的函数用于初始化参数
def normal(shape):
    return torch.randn(size=shape) * 0.01

class RNN(nn.Module):
    def __init__(self, input_size, hidden_size):
        super(RNN, self).__init__()
        self.input_size = input_size
        self.hidden_size = hidden_size
        # 将输入与隐状态分别经过线性变化后相加
        self.W_xh = nn.Parameter(normal((input_size, hidden_size)))
        self.W_hh = nn.Parameter(normal((hidden_size, hidden_size)))
        self.b_h = nn.Parameter(torch.zeros(hidden_size))
    
    def init_rnn_state(self, batch_size, hidden_size):
        return (torch.zeros((batch_size, hidden_size), dtype=torch.float),)
    
    def forward(self, inputs, states):
        seq_len, batch_size, _ = inputs.shape
        hidden_state, = states
        hiddens = []
        for step in range(seq_len):
            # 输入hidden_state与inputs经过线性变换后相加,
            # 输出的hidden_state也是下一时刻输入的hidden_state
            xh = torch.mm(inputs[step], self.W_xh)
            hh = torch.mm(hidden_state, self.W_hh)
            hidden_state = xh + hh + self.b_h
            hidden_state = torch.tanh(hidden_state)
            hiddens.append(hidden_state)
        # 返回所有时刻的hidden_state: seq_len * batch_size * hidden_size
        # 以及最后时刻的hidden_state(可能用于后续输入): 
        # batch_size * hidden_size
        return torch.stack(hiddens, dim=0), (hidden_state,)

# 在循环神经网络的基础上添加语言模型的输入输出、损失计算等
class RNNLM(nn.Module):
    def __init__(self, model, vocab_size, hidden_size):
        super(RNNLM, self).__init__()
        self.vocab_size = vocab_size
        self.hidden_size = hidden_size
        self.embedding = nn.Embedding(vocab_size, hidden_size)
        self.model = model
        self.W_hq = nn.Parameter(normal((hidden_size, vocab_size)))
        self.b_q = nn.Parameter(torch.zeros(vocab_size))
        
    def forward(self, input_ids):
        batch_size, seq_len = input_ids.shape
        # input_ids形状为batch_size * seq_len,翻转为seq_len * batch_size,
        # 将seq_len放在第一维方便计算
        input_ids = torch.permute(input_ids, (1, 0))
        # seq_len * batch_size * embed_size
        embed = self.embedding(input_ids)
        # batch_size * hidden_size
        states = self.model.init_rnn_state(batch_size, self.hidden_size)
        hiddens, _ = self.model(embed, states)
    
        hiddens = torch.flatten(hiddens[:-1], start_dim=0, end_dim=1)
        output_states = torch.mm(hiddens, self.W_hq) + self.b_q
        labels = torch.flatten(input_ids[1:], start_dim=0, end_dim=1)
        loss_fct = nn.CrossEntropyLoss(ignore_index=0)
        loss = loss_fct(output_states, labels)
        return loss

        接下来讨论循环神经网络训练过程中经常出现的一个问题。假设第t步的损失为J^{t},隐状态为h^{t}。由于总损失函数是各时刻损失函数的平均值, 因此训练过程中涉及各时刻的损失函数对历史时刻的求导, 这里以J^{t}h^{t}求导为例, 根据链式法则展开梯度:
        ​​​​​​​        ​​​​​​​        ​​​​​​​        \frac { \partial J^{(t)}}{ \partial h^{(1)}}= \frac { \partial J^{(t)}}{ \partial h^{(t)}} \times \frac { \partial h^{(t)}}{ \partial h^{(1)}}= \frac { \partial J^{(t)}}{ \partial h^{(t)}} \times \prod \limits _{ \tau =2}^{t} \frac { \partial h^{( \tau )}}{ \partial h^{( \tau -1)}}
        可以看到此梯度中包含连乘项\prod \limits _{ \tau =2}' \frac { \partial h^{( \tau )}}{ \partial h^{( \tau -1)}},即每一时刻隐状态对上一时刻隐状态梯度的乘积。
        在某些条件下,连乘项中的大部分因子小于1, 这会导致梯度随t的增加呈指数级衰减,这就是循环神经网络训练中著名的梯度消失问题。由此带来的后果是,距离远的梯度信号比距离近的梯度信号小得非常多,所以模型的参数实际上只能根据近距离的损失函数进行优化,从而破坏了长距离依赖。

        反过来,如果大部分连乘项大于1, 又会导致梯度随t的增加呈指数级增长, 这就是循环神经网络训练中的梯度爆炸问题。梯度爆炸会导致参数更新幅度过大,从而可能使训练过程不稳定或是使参数落入损失函数很大的区域, 甚至导致 inf或NaN。
        事实上, 梯度消失或梯度爆炸是神经网络训练中普遍存在的问题。对于循环神经网络,当输入序列较长时, 更容易发生梯度消失或梯度爆炸的问题。

        对于梯度爆炸,一个非常直接的解决方案是梯度裁剪( gradient clipping), 即当梯度的模( norm) 超过某个阈值的时候,手动将其缩小到合理的范围内再使用梯度下降。那么, 梯度裁剪的思想是否也可以用于解决梯度消失问题,即当梯度的模低于某个阈值时手动将其增加到合理的范围呢?答案是否定的。这是因为当梯度的模较低时, 我们并不能确定是发生了梯度消失还是到达了损失函数的局部最优点,如果是后者, 那么增加梯度是不合理的, 这会导致梯度下降无法收敛。
        下面展示使用梯度裁剪的循环神经网络语言模型的训练代码。

# 梯度裁剪
def grad_clipping(model, theta=1):
    params = [p for p in model.parameters() if p.requires_grad]
    norm = torch.sqrt(sum(torch.sum((p.grad ** 2)) for p in params))
    if norm > theta:
        for param in params:
            param.grad[:] *= theta / norm
    

# 训练
from torch.utils.data import DataLoader
from torch.optim import SGD, Adam
import numpy as np
from tqdm import tqdm, trange
import matplotlib.pyplot as plt

def train_rnn_lm(data_loader, rnn, vocab_size, hidden_size=128, 
                 epochs=200, learning_rate=1e-3):
    # 准备模型、优化器等
    rnn_lm = RNNLM(rnn, vocab_size, hidden_size)
    optimizer = Adam(rnn_lm.parameters(), lr=learning_rate)
    rnn_lm.zero_grad()
    rnn_lm.train()

    epoch_loss = []
    with trange(epochs, desc='epoch', ncols=60) as pbar:
        for epoch in pbar:
            for step, batch in enumerate(data_loader):
                loss = rnn_lm(batch)
                pbar.set_description(f'epoch-{epoch}, ' + \
                    f'loss={loss.item():.4f}')
                loss.backward()
                grad_clipping(rnn_lm)
                optimizer.step()
                rnn_lm.zero_grad()
            epoch_loss.append(loss.item())

    epoch_loss = np.array(epoch_loss)
    # 打印损失曲线
    plt.plot(range(len(epoch_loss)), epoch_loss)
    plt.xlabel('training epoch')
    plt.ylabel('loss')
    plt.show()

sent_tokens = np.array(sent_tokens)
print(sent_tokens.shape)
vocab_size = len(dataset.token2id)

data_loader = DataLoader(torch.tensor(sent_tokens, dtype=torch.long),\
    batch_size=16, shuffle=True)
rnn = RNN(128, 128)
train_rnn_lm(data_loader, rnn, vocab_size, hidden_size=128,\
    epochs=200, learning_rate=1e-3)
(1104, 40)
epoch-199, loss=0.3491: 100%|█| 200/200 [04:42<00:00,  1.41s

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

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

相关文章

viscode 自定义片段,快速生成自己的开发模板

设置 ---> 代码片段 2.选择新建全局代码片段文件 3.根据示例自定义配置代码片段 4.示例:vue prefix:内容--> 代表用于触发代码片段的内容 $1&#xff0c; $2 用于制表位,如 $1 代表生成后第一个输入的位置,$2代表第二个,不用自己移动鼠标 {// Place your snippets f…

Sac格式

本文章只作为自己学习时的用法&#xff0c;不通用&#xff0c;大家可不用参考。 sac格式 0.01000000 -1.569280 1.520640 -12345.00 -12345.009.459999 19.45000 -41.43000 10.46400 -12345.00-12345.00 -12345.00 -12…

SQL注入漏洞的基础知识

目录 SQL注入漏洞的定义和原理 SQL注入的类型和攻击方法 SQL注入的防御措施 示例代码 深入研究 SQL注入漏洞的常见攻击场景有哪些&#xff1f; 如何有效防范SQL注入攻击&#xff1f; SQL注入与跨站脚本攻击&#xff08;XSS&#xff09;之间有什么区别&#xff1f; 主要…

每日一练【最大连续1的个数 III】

一、题目描述 给定一个二进制数组 nums 和一个整数 k&#xff0c;如果可以翻转最多 k 个 0 &#xff0c;则返回 数组中连续 1 的最大个数 。 二、题目解析 本题同样是利用滑动窗口的解法。 首先进入窗口&#xff0c;如果是1&#xff0c;就直接让right&#xff0c;但是如果是…

【软考】树、森林和二叉树之间的相互转换

目录 1. 说明2. 树、森林转换为二叉树2.1 树转成二叉树2.1 森林转成二叉树 3. 二叉树转换为树和森林 1. 说明 1.树、森林和二叉树之间可以互相进行转换&#xff0c;即任何一个森林或一棵树可以对应表示为一棵叉树&#xff0c;而任何一棵二叉树也能对应到一个森林或一棵树上。 …

SSRF复现

目录 环境 分析测试 写入shell 环境 web-ssrfme docker环境 拉取运行 分析测试 进入网站会显示源码 可以看到过滤了file&#xff0c;dict等&#xff0c;但get传参info会执行phpinfo() 可以发现这里网站ip是172.18.0.3&#xff0c;可以使用这个地址绕过waf 测试看是否存在…

如何实现一次搭建 多平台适配的小程序

如何实现一次搭建 多平台适配的小程序 什么是小程序小程序的优势有什么&#xff1f;如果构建小程序&#xff0c;会用在什么领域和场景&#xff1f;如何实现一站式开发多平台的小程序&#xff1f;你希望了解小程序上哪些功能模块的集成能力&#xff1f; 随着微信、支付宝、百度、…

七、Centos安装LDAP--Docker版--已失败

参考博客&#xff1a; docker 安装 OpenLDAP 及 LdapAdmin桌面版、页面版(osixia/openldap)_docker安装ldap-CSDN博客 LDAP使用docker安装部署与使用_memberof ldap docker-CSDN博客 目录 一、安装Docker Docker基本使用 管理镜像 二、拉取LDAP镜像 配置docker国内的镜像…

Java 入门指南:初识 Java 异常(Exception)

初识Java异常 何为异常 在Java中&#xff0c;异常是一个不需要的或意外的事件&#xff0c;它发生在程序执行期间&#xff0c;即在运行时&#xff0c;破坏了程序指令的正常流程。异常可以被程序捕获&#xff08;catch&#xff09;和处理&#xff08;handle&#xff09;。 方法…

C/C++逆向:寻找main函数(Debug-x86)

在程序的逆向分析中&#xff0c;寻找main函数在逆向分析中是非常重要的&#xff0c;它是程序的核心执行点&#xff0c;从这里开始&#xff0c;程序的主要逻辑开始展开&#xff1b;在这边我们需要明确两个概念&#xff1a;用户入口&#xff08;User Entry Point&#xff09; 和 …

【C语言进阶】深入C语言指针:基础到进阶的跨越

&#x1f4dd;个人主页&#x1f339;&#xff1a;Eternity._ ⏩收录专栏⏪&#xff1a;C语言 “ 登神长阶 ” &#x1f921;往期回顾&#x1f921;&#xff1a;C语言数据在内存中的存储 &#x1f339;&#x1f339;期待您的关注 &#x1f339;&#x1f339; ❀C语言指针进阶 &a…

Linux中安装java和tomcat(保姆级教程)

java 篇 JDK是用于开发Java应用程序的软件开发工具包。它包含了编译器、调试器、运行时环境和其他一些开发工具&#xff0c;可以帮助开发人员创建、编译、调试和部署Java应用程序。JDK提供了Java编程语言的开发工具和运行时库&#xff0c;使开发人员能够编写和执行Java代码。 …

TypeScript教程(一)之我们为什么要学TypeScript

根据软件开发设计公司 The Software House 针对 2022 年前端市场状态的调查显示&#xff0c;84% 的受访者都在使用 TypeScript&#xff0c;43% 的受访者甚至认为 TypeScript 将超越 JavaScript 成为前端开发的主要语言。TypeScript 这些年越来越火&#xff0c;可以说是前端工程…

影视会员官方渠道api对接

API对接是指两个不同的软件系统或应用程序之间通过API&#xff08;应用程序编程接口&#xff09;进行交互的过程。这种交互允许数据和功能的共享&#xff0c;而不必暴露系统的内部工作原理。在影视会员充值场景中&#xff0c;API对接具有以下几个关键特点和优势&#xff1a; 数…

【Linux系列】AWK命令使用

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

Java面试题--JVM大厂篇之JVM大厂面试题及答案解析(4)

&#x1f496;&#x1f496;&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎你们来到青云交的博客&#xff01;能与你们在此邂逅&#xff0c;我满心欢喜&#xff0c;深感无比荣幸。在这个瞬息万变的时代&#xff0c;我们每个人都在苦苦追寻一处能让心灵安然栖息的港湾。而我的博…

使用VBA快速创建非规则数据图表

实例需求&#xff1a;工作表中共有4组数据&#xff0c;第一组数据涵盖所有日期&#xff0c;其他3组均为断续数据。 现在需要创建如下图所示的线图&#xff0c;由于数据区域是非连续的&#xff0c;因此无法直接创建图表。 需要先将数据表按照日期对齐&#xff0c;如下图所示&…

《深度学习》 OpenCV 计算机视觉入门 (中篇)

目录 一、OpenCV函数使用 1、改变像素值 2、图像切片合并 1&#xff09;直接截取相加 2&#xff09;使用cv2.add合并 3&#xff09;此时亮度太高了&#xff0c;需要降低亮度 3、边界填充 1&#xff09;常数填充 2&#xff09;镜面反射填充&#xff08;复制交界处&…

论文仍在苦恼?分享最实用6款AI论文工具网站的汇总!

论文写作是学术研究中的重要环节&#xff0c;然而&#xff0c;面对繁重的写作任务&#xff0c;许多学者和学生常常感到力不从心。幸运的是&#xff0c;随着人工智能技术的飞速发展&#xff0c;一系列AI论文写作工具应运而生&#xff0c;极大地简化了论文写作流程&#xff0c;提…

【Mybatis】介绍+搭建+参数传递+增删改查操作+事务与连接池

目录 一. Mybatis介绍 二. Mybatis搭建 1. 导入Mybatis依赖的jar包 2. 创建Mybatis全局配置文件 3. 定义一个接口 4. 创建sql映射文件 5. 测试 * MyBatisX插件安装 三. 数据库连接池 四. 参数传递 1. 将数据封装到对象中 2. 使用Param注解 五. 数据库事务 六.…