昇思25天学习打卡营第11天|NLP-LSTM-CRF序列标注

news2024/11/25 16:31:40

打卡

目录

打卡

序列标注

条件随机场(Conditional Random Field, CRF)

SCORE计算

SCORE 计算的代码

Normalizer计算

Normalizer 计算的代码实现

Viterbi算法

CRF层组装

BiLSTM+CRF模型

模型实例化 

模型训练

模型预测

新造一个句子重新预测看效果


序列标注

序列标注指给定输入序列,给序列中每个Token进行标注标签的过程。序列标注问题通常用于从文本中进行信息抽取,包括分词(Word Segmentation)、词性标注(Position Tagging)、命名实体识别(Named Entity Recognition, NER)等。

一种常见的命名实体识别的标注方法——“BIOE”标注,将一个实体(Entity)的开头标注为B,其他部分标注为I,非实体标注为O。

例子:

输入序列
输出标注BIIIOOOOOBI

如上表所示,`清华大学` 和 `北京`是地名,需要将其识别,我们对每个输入的单词预测其标签,最后根据标签来识别实体。

条件随机场(Conditional Random Field, CRF)

对序列进行标注,实际上是对序列中每个Token进行标签预测,可以直接视作简单的多分类问题。但是序列标注不仅仅需要对单个Token进行分类预测,同时相邻Token直接有关联关系。即,对于“BIOE”标注方法,I前必须是B或I。

将命名实体识别视为多分类问题,则每个词的预测概率都是独立的,易产生类似的问题,因此需要引入一种能够学习到此种关联关系的算法来保证预测结果的正确性。而条件随机场是适合此类场景的一种概率图模型(概率图模型利用图的结构来表达变量间的概率关系,简化了复杂系统的概率计算。在概率图模型中,节点通常代表随机变量,边则代表变量间的依赖关系。)。

下面对条件随机场的定义和参数化形式进行简析。

考虑到序列标注问题的线性序列特点,本节所述的条件随机场特指线性链条件随机场(Linear Chain CRF)。


设 $x=\{x_0, ..., x_n\}$ 为输入序列,$y=\{ y_0, ..., y_n\}$$y \in Y$为输出的标注序列,其中 $n$ 为序列的最大长度,$Y$ 表示 $x$ 对应的所有可能的输出序列集合。则输出序列 $y$ 的概率为:

$ P(y|x) = \frac{\exp{(\text{Score}(x, y)})}{\sum_{y' \in Y} \exp{(\text{Score}(x, y')})} \qquad (1) $

$ x_i $ , $ y_i $ 为序列的第 i 个Token和对应的标签,则Score 需要能够在计算$ x_i $ 和 $ y_i $ 的映射的同时,捕获相邻标签 $ y_{i-1} $ 和 $ y_i $ 之间的关系,因此我们定义两个概率函数:

  1. 发射概率函数 $ \psi _{EMIT} $ :表示 $ x_{i} \rightarrow y_i $ 的概率。
  2. 转移概率函数 $ \psi _{TRANS} $:表示 $ y_{i-1} \rightarrow y_i $ 的概率。

则可以得到 Score 的计算公式:

$ \text{Score}(x,y) = \sum_i \log \psi_\text{EMIT}(x_i \rightarrow y_i) + \log \psi_\text{TRANS}(y_{i-1} \rightarrow y_i) \qquad (2) $

标签集合$T$,构造大小为 $|T| \times |T|$ 的矩阵 $\textbf{P}$,用于存储标签间的转移概率;由编码层(如Dense、LSTM等) 输出的隐状态 $h$ 可以直接视作发射概率,此时 $\text{Score}$ 的计算公式可以转化为:

$ \text{Score}(x,y) = \sum_i h_i[y_i] + \textbf{P}_{y_{i-1}, y_{i}} \qquad (3) $

根据上述公式,使用 MindSpore 来实现 CRF 的参数化形式。

首先实现CRF层的前向训练部分,将CRF和损失函数做合并,选择分类问题常用的负对数似然函数(Negative Log Likelihood, NLL),则有: $ \text{Loss} = -log(P(y|x)) \qquad (4) $

由公式(1)与(4)可得,

$ \text{Loss} = -log(\frac{\exp{(\text{Score}(x, y)})}{\sum_{y' \in Y} \exp{(\text{Score}(x, y')})}) = log(\sum_{y' \in Y} \exp{(\text{Score}(x, y')}) - \text{Score}(x, y) \qquad (5) $

根据公式(5),被减数为 Normalizer,减数为 Score,分别实现后相减得到最终 Loss

注意上面两个概率函数的逻辑:(两个逻辑:输入决定输出的发射概率 + 上一个输出决定下一个输出 的转移概率

上面推到的结论:计算出 Normalizer 和 Score,就可获得最终 Loss = Normalizer - Score。

CRF完整推导的论文:​​​​​​https://www.cs.columbia.edu/~mcollins/crf.pdf

SCORE计算

首先根据公式(3),计算正确标签序列所对应的得分,需要注意,除了转移概率矩阵 P 外,还需要维护两个大小为 |𝑇| 的向量,分别作为序列开始和结束时的转移概率。同时引入了一个掩码矩阵𝑚𝑎𝑠𝑘 ,将多个序列打包为一个Batch时填充的值忽略,使得 Score 计算仅包含有效的Token。
 

SCORE 计算的代码
def compute_score(emissions, tags, seq_ends, mask, trans, start_trans, end_trans):
    """
    emissions: 表示每个时间步(序列位置)和每个批次中每个可能标签的发射概率。形状为(seq_length, batch_size, num_tags)。
    tags: 表示预测的标签序列,形状为(seq_length, batch_size)。
    seq_ends: 表示每个批次序列结束的位置。
    mask: 表示有效的时间步(忽略填充部分),形状为(seq_length, batch_size)。
    trans: 表示标签之间的转移概率矩阵,形状为(num_tags, num_tags)。
    start_trans: 表示初始状态到每个标签的转移概率,形状为(num_tags,)。
    end_trans: 表示每个标签到结束状态的转移概率,形状为(num_tags,)。

    """
    # emissions: (seq_length, batch_size, num_tags) 
    # tags: (seq_length, batch_size)
    # mask: (seq_length, batch_size)
    
    # 获取序列长度seq_length和批次大小batch_size。
    seq_length, batch_size = tags.shape
    # 将mask转换为与emissions相同的数据类型。
    mask = mask.astype(emissions.dtype)  

    # 将score设置为初始转移概率
    # shape: (batch_size,) 初始化得分score为初始状态到序列第一个标签的转移概率,并加上第一个时间步的发射概率。
    score = start_trans[tags[0]] # 选择每个批次第一个时间步的标签对应的初始转移概率。
    # score += 第一次发射概率
    # shape: (batch_size,)
    score += emissions[0, mnp.arange(batch_size), tags[0]]  ## 加上第一个时间步的发射概率。
    
    ## 遍历序列的其余时间步(从第二个时间步开始):
    for i in range(1, seq_length):
        # 标签由i-1转移至i的转移概率(当mask == 1时有效)
        # shape: (batch_size,) 在每个时间步,加上前一个标签到当前标签的转移概率,乘以mask确保只计算有效时间步。
        score += trans[tags[i - 1], tags[i]] * mask[i]

        # 预测tags[i]的发射概率(当mask == 1时有效)
        # shape: (batch_size,) 加上当前时间步的发射概率。
        score += emissions[i, mnp.arange(batch_size), tags[i]] * mask[i]

    # 结束转移---对于每个批次,计算最后一个有效时间步的标签到结束状态的转移概率,并加到score上。
    # shape: (batch_size,)
    last_tags = tags[seq_ends, mnp.arange(batch_size)]
    # score += 结束转移概率, 返回计算得到的总得分score。
    # shape: (batch_size,)  加上最后一个有效时间步的标签到结束状态的转移概率。
    score += end_trans[last_tags]

    return score

Normalizer计算

根据公式 (5) ,Normalizer是 x 对应的所有可能的输出序列的 Score 的对数指数和(Log-Sum-Exp)。若按穷举法进行计算,需要将每个可能的输出序列Score都计算一遍,共有 $ |T|^{n}$个结果,此处采用动态规划算法,通过复用计算结果来提高效率。

假设需要计算从第 0 至第 i 个Token所有可能的输出序列得分 $\text{Score}_{i}$,则可以先计算出从第 0 至第$i-1$ 个Token所有可能的输出序列得分 $\text{Score}_{i-1}$。因此,Normalizer 可以改写为以下形式:

$log(\sum_{y'_{0,i} \in Y} \exp{(\text{Score}_i})) = log(\sum_{y'_{0,i-1} \in Y} \exp{(\text{Score}_{i-1} + h_{i} + \textbf{P}})) \qquad (6)$

其中$h_i$ 为第i 个Token的发射概率,$\textbf{P}$是转移矩阵。由于发射概率矩阵 $h$ 和转移概率矩阵 P 独立于 y 的序列路径计算,可以将其提出,可得:

$ log(\sum_{y'_{0,i} \in Y} \exp{(\text{Score}_i})) = log(\sum_{y'_{0,i-1} \in Y} \exp{(\text{Score}_{i-1}})) + h_{i} + \textbf{P} \qquad (7)$

Normalizer 计算的代码实现
def compute_normalizer(emissions, mask, trans, start_trans, end_trans):
    """给定发射概率、转移概率、初始和结束转移概率下,所有可能标签序列的总概率的对数。
    emissions: 表示每个时间步(序列位置)和每个批次中每个可能标签的发射概率。形状为(seq_length, batch_size, num_tags)。
    mask: 表示有效的时间步(忽略填充部分),形状为(seq_length, batch_size)。
    trans: 表示标签之间的转移概率矩阵,形状为(num_tags, num_tags)。
    start_trans: 表示初始状态到每个标签的转移概率,形状为(num_tags,)。
    end_trans: 表示每个标签到结束状态的转移概率,形状为(num_tags,)。
    """
    # emissions: (seq_length, batch_size, num_tags)
    # mask: (seq_length, batch_size)

    seq_length = emissions.shape[0]

    # 将score设置为初始转移概率,并加上第一次发射概率
    # shape: (batch_size, num_tags) 初始化得分score为初始状态到每个标签的转移概率加上第一个时间步的发射概率。
    score = start_trans + emissions[0]

    # 遍历序列的其余时间步(从第二个时间步开始)
    for i in range(1, seq_length):
        # 扩展score的维度用于总score的计算
        # shape: (batch_size, num_tags, 1) 将得分score扩展一个维度,以便进行广播操作。
        broadcast_score = score.expand_dims(2)

        # 扩展emission的维度用于总score的计算
        # shape: (batch_size, 1, num_tags) 将当前时间步的发射概率emissions[i]也扩展一个维度,以便进行广播操作。
        broadcast_emissions = emissions[i].expand_dims(1)

        # 根据公式(7),计算score_i
        # 此时broadcast_score是由第0个到当前Token所有可能路径
        # 对应score的log_sum_exp
        # shape: (batch_size, num_tags, num_tags) 计算下一个时间步的所有可能标签的得分,这包括前一个时间步的得分、转移概率和当前时间步的发射概率。
        next_score = broadcast_score + trans + broadcast_emissions

        # 对score_i做log_sum_exp运算,用于下一个Token的score计算
        # shape: (batch_size, num_tags)
        next_score = ops.logsumexp(next_score, axis=1)

        # 当mask == 1时,score才会变化
        # shape: (batch_size, num_tags)
        score = mnp.where(mask[i].expand_dims(1), next_score, score)

    # 最后加结束转移概率
    # shape: (batch_size, num_tags)
    score += end_trans
    # 对所有可能的路径得分求log_sum_exp
    # shape: (batch_size,)
    return ops.logsumexp(score, axis=1)

Viterbi算法

在完成前向训练部分后,需要实现解码部分。这里我们选择适合求解序列最优路径的Viterbi算法。与计算Normalizer类似,使用动态规划求解所有可能的预测序列得分。不同的是在解码时同时需要将第𝑖个Token对应的score取值最大的标签保存,供后续使用Viterbi算法求解最优预测序列使用。

取得最大概率得分Score ,以及每个Token对应的标签历史History 后,根据Viterbi算法可以得到公式:$P_{0,i} = max(P_{0, i-1}) + P_{i-1, i}$

从第0个至第i 个Token对应概率最大的序列,只需要考虑从第0个至第(i-1) 个Token对应概率最大的序列,以及从第i 个至第 (i-1) 个概率最大的标签即可。因此我们逆序求解每一个概率最大的标签,构成最佳的预测序列。

由于mindSpore静态图语法限制,我们将Viterbi算法求解最佳预测序列的部分作为后处理函数,不纳入后续CRF层的实现。

def viterbi_decode(emissions, mask, trans, start_trans, end_trans):
    # emissions: (seq_length, batch_size, num_tags)
    # mask: (seq_length, batch_size)

    seq_length = mask.shape[0]

    score = start_trans + emissions[0]
    history = ()

    for i in range(1, seq_length):
        broadcast_score = score.expand_dims(2)
        broadcast_emission = emissions[i].expand_dims(1)
        next_score = broadcast_score + trans + broadcast_emission

        # 求当前Token对应score取值最大的标签,并保存
        indices = next_score.argmax(axis=1)
        history += (indices,)

        next_score = next_score.max(axis=1)
        score = mnp.where(mask[i].expand_dims(1), next_score, score)

    score += end_trans

    return score, history

def post_decode(score, history, seq_length):
    # 使用Score和History计算最佳预测序列
    batch_size = seq_length.shape[0]
    seq_ends = seq_length - 1
    # shape: (batch_size,)
    best_tags_list = []

    # 依次对一个Batch中每个样例进行解码
    for idx in range(batch_size):
        # 查找使最后一个Token对应的预测概率最大的标签,
        # 并将其添加至最佳预测序列存储的列表中
        best_last_tag = score[idx].argmax(axis=0)
        best_tags = [int(best_last_tag.asnumpy())]

        # 重复查找每个Token对应的预测概率最大的标签,加入列表
        for hist in reversed(history[:seq_ends[idx]]):
            best_last_tag = hist[idx][best_tags[-1]]
            best_tags.append(int(best_last_tag.asnumpy()))

        # 将逆序求解的序列标签重置为正序
        best_tags.reverse()
        best_tags_list.append(best_tags)

    return best_tags_list

CRF层组装

将上述前向训练和解码部分的代码组装完整的CRF层。使用nn.Cell进行封装。

考虑到输入序列可能存在 Padding 的情况,CRF的输入需要考虑输入序列的真实长度,因此除发射矩阵和标签外,加入 seq_length 参数传入序列 Padding 前的长度,并实现生成mask矩阵的sequence_mask方法。

import mindspore as ms
import mindspore.nn as nn
import mindspore.ops as ops
import mindspore.numpy as mnp
from mindspore.common.initializer import initializer, Uniform

def sequence_mask(seq_length, max_length, batch_first=False):
    """根据序列实际长度和最大长度生成mask矩阵"""
    range_vector = mnp.arange(0, max_length, 1, seq_length.dtype)
    result = range_vector < seq_length.view(seq_length.shape + (1,))
    if batch_first:
        return result.astype(ms.int64)
    return result.astype(ms.int64).swapaxes(0, 1)

class CRF(nn.Cell):
    def __init__(self, num_tags: int, batch_first: bool = False, reduction: str = 'sum') -> None:
        if num_tags <= 0:
            raise ValueError(f'invalid number of tags: {num_tags}')
        super().__init__()
        if reduction not in ('none', 'sum', 'mean', 'token_mean'):
            raise ValueError(f'invalid reduction: {reduction}')
        self.num_tags = num_tags
        self.batch_first = batch_first
        self.reduction = reduction
        self.start_transitions = ms.Parameter(initializer(Uniform(0.1), (num_tags,)), name='start_transitions')
        self.end_transitions = ms.Parameter(initializer(Uniform(0.1), (num_tags,)), name='end_transitions')
        self.transitions = ms.Parameter(initializer(Uniform(0.1), (num_tags, num_tags)), name='transitions')

    def construct(self, emissions, tags=None, seq_length=None):
        if tags is None:
            return self._decode(emissions, seq_length)
        return self._forward(emissions, tags, seq_length)

    def _forward(self, emissions, tags=None, seq_length=None):
        if self.batch_first:
            batch_size, max_length = tags.shape
            emissions = emissions.swapaxes(0, 1)
            tags = tags.swapaxes(0, 1)
        else:
            max_length, batch_size = tags.shape

        if seq_length is None:
            seq_length = mnp.full((batch_size,), max_length, ms.int64)

        mask = sequence_mask(seq_length, max_length)

        # shape: (batch_size,)
        numerator = compute_score(emissions, tags, seq_length-1, mask, self.transitions, self.start_transitions, self.end_transitions)
        # shape: (batch_size,)
        denominator = compute_normalizer(emissions, mask, self.transitions, self.start_transitions, self.end_transitions)
        # shape: (batch_size,)
        llh = denominator - numerator

        if self.reduction == 'none':
            return llh
        if self.reduction == 'sum':
            return llh.sum()
        if self.reduction == 'mean':
            return llh.mean()
        return llh.sum() / mask.astype(emissions.dtype).sum()

    def _decode(self, emissions, seq_length=None):
        if self.batch_first:
            batch_size, max_length = emissions.shape[:2]
            emissions = emissions.swapaxes(0, 1)
        else:
            batch_size, max_length = emissions.shape[:2]

        if seq_length is None:
            seq_length = mnp.full((batch_size,), max_length, ms.int64)

        mask = sequence_mask(seq_length, max_length)
        
        ### viterbi_decode 前述算法
        return viterbi_decode(emissions, mask, self.transitions, self.start_transitions, self.end_transitions)

BiLSTM+CRF模型

设计一个双向 LSTM+CRF 的模型来进行命名实体识别任务的训练。

  • 模型结构如下:nn.Embedding -> nn.LSTM -> nn.Dense -> CRF
  • 其中 LSTM 提取序列特征,经过 Dense 层变换获得发射概率矩阵,最后送入CRF层
class BiLSTM_CRF(nn.Cell):
    def __init__(self, 
                 vocab_size, embedding_dim, 
                 hidden_dim, num_tags, 
                 padding_idx=0):
        super().__init__()
        self.embedding = nn.Embedding(
                            vocab_size, ## 词表长度
                            embedding_dim, ##  每个嵌入向量的大小。
                            padding_idx=padding_idx)
        self.lstm = nn.LSTM(embedding_dim, 
                            hidden_dim // 2, 
                            bidirectional=True, 
                            batch_first=True)
        self.hidden2tag = nn.Dense( hidden_dim, 
                                    num_tags, ### 标签集长度
                                    'he_uniform')
        self.crf = CRF( num_tags, 
                        batch_first=True)

    def construct(self, inputs, seq_length, tags=None):
        embeds = self.embedding(inputs)
        outputs, _ = self.lstm(embeds, seq_length=seq_length)
        feats = self.hidden2tag(outputs)

        crf_outs = self.crf(feats, tags, seq_length)
        return crf_outs


### 生成两句例子和对应的标签,并构造词表和标签表。
embedding_dim = 16
hidden_dim = 32

training_data = [(
    "清 华 大 学 坐 落 于 首 都 北 京".split(),
    "B I I I O O O O O B I".split()
), (
    "重 庆 是 一 个 魔 幻 城 市".split(),
    "B I O O O O O O O".split()
)]

word_to_idx = {}
word_to_idx['<pad>'] = 0
for sentence, tags in training_data:
    for word in sentence:
        if word not in word_to_idx:
            word_to_idx[word] = len(word_to_idx)

tag_to_idx = {"B": 0, "I": 1, "O": 2}

模型实例化 

由于CRF层已经进行了NLLLoss的计算,因此不需要再设置Loss。

model = BiLSTM_CRF( len(word_to_idx), ## 词表长度
                    embedding_dim,    ## embedding大小
                    hidden_dim,       ## 隐藏层大小
                    len(tag_to_idx))  ## 标签集长度
optimizer = nn.SGD( model.trainable_params(), 
                    learning_rate=0.01, 
                     weight_decay=1e-4)


### 前向传播和训练步骤定义

grad_fn = ms.value_and_grad(model, None, optimizer.parameters)

def train_step(data, seq_length, label):
    loss, grads = grad_fn(data, seq_length, label)
    optimizer(grads)
    return loss


def prepare_sequence(seqs, word_to_idx, tag_to_idx):
    # 将生成的数据打包成Batch,按照序列最大长度,对长度不足的序列进行填充,分别返回输入序列、输出标签和序列长度构成的Tensor。
    seq_outputs, label_outputs, seq_length = [], [], []
    max_len = max([len(i[0]) for i in seqs])

    for seq, tag in seqs:
        seq_length.append(len(seq))
        idxs = [word_to_idx[w] for w in seq]
        labels = [tag_to_idx[t] for t in tag]
        idxs.extend([word_to_idx['<pad>'] for i in range(max_len - len(seq))])
        labels.extend([tag_to_idx['O'] for i in range(max_len - len(seq))])
        seq_outputs.append(idxs)
        label_outputs.append(labels)

    return ms.Tensor(seq_outputs, ms.int64), \
            ms.Tensor(label_outputs, ms.int64), \
            ms.Tensor(seq_length, ms.int64)

设计的模型查看:

输出的模型实例化例子查看:

模型训练

from tqdm import tqdm

steps = 500
with tqdm(total=steps) as t:
    for i in range(steps):
        loss = train_step(data, seq_length, label)
        t.set_postfix(loss=loss)
        t.update(1)

500步长的训练过程查看:

模型预测

score, history = model(data, seq_length)
print(score)

predict = post_decode(score, history, seq_length) 
print(predict)

 
#########################
## 最后将预测的index序列转换为标签序列,打印输出结果,查看效果。
idx_to_tag = {idx: tag for tag, idx in tag_to_idx.items()}

def sequence_to_tag(sequences, idx_to_tag):
    outputs = []
    for seq in sequences:
        outputs.append([idx_to_tag[i] for i in seq])
    return outputs

新造一个句子重新预测看效果

从预测输出结果查看,没有什么误差,毕竟训练集就那两句话,我们新造一个句子重新识别,看效果怎么样。

首先,新造的句子两个地点关键字不变,词表做一个扩充,如下,词表由20个增加到26个。新造的句子 new_data 是 20个字符组成。

由此看到,效果不行,可以理解,毕竟训练样本少,复杂一点就不能识别了。

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

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

相关文章

安装adb和常用命令

下载ADB安装包 https://dl.google.com/android/repository/platform-tools-latest-windows.zip 解压安装包 解压如上下载的安装包&#xff0c;然后复制adb.exe所在的文件地址 配置环境变量 我的电脑——>右键属性——>高级系统设置——>环境变量——>系统变量—…

solidity基础语法(以太坊solidity合约)

solidity基础语法&#xff08;以太坊solidity合约&#xff09; 1-值类型和取值范围2-引用类型3-引用类型高阶4-固定数组和动态数组 1-值类型和取值范围 https://learnblockchain.cn/docs/solidity/introduction-to-smart-contracts.html#subcurrency https://learnblockchain…

R语言包AMORE安装报错问题以及RStudio与Rtools环境配置

在使用R语言进行AMORE安装时会遇到报错&#xff0c;这时候需要采用解决办法&#xff1a; AMORE包安装&#xff0c;需要离线官网下载安装包&#xff1a; Index of /src/contrib/Archive/AMORE (r-project.org)https://cran.r-project.org/src/contrib/Archive/AMORE/ 一、出现…

MySQL第九次作业

1、安装redis&#xff0c;启动客户端、验证。 首先选择版本 进去之后是这样&#xff0c;一个是压缩包&#xff0c;一个是安装包 点击下载好的安装包&#xff0c;开始安装 选择“添加Redis目录到环境变量PATH中”&#xff0c;这样方便系统自动识别Redis执行文件在哪里。 其他一…

新版本 idea 创建不了 spring boot 2 【没有jkd8选项】

创建新项目 将地址换成如下 https://start.aliyun.com/

厂家置换电费如何达到最大化收益

新能源行业知识体系-------主目录-----持续更新https://blog.csdn.net/grd_java/article/details/140004020 文章目录 一、电能电费二、同时刻不同厂家置换&#xff0c;不会影响最终电能电费结果三、风险防范补偿和回收机制四、我们的数据如何考虑补偿和回收五、如何利用补偿和…

元宇宙深入解析

元宇宙&#xff08;Metaverse&#xff09;是一个新兴的概念&#xff0c;它激发了技术专家、艺术家和商业领袖的无限想象。它代表着数字互动的新前沿&#xff0c;提供了一个平行的数字宇宙&#xff0c;用户可以在其中实时互动&#xff0c;超越物理世界的限制。 元宇宙是什么&am…

计算机毕业设计django+hadoop+scrapy租房可视化 租房推荐系统 租房大屏可视化 租房爬虫 spark 58同城租房爬虫 房源推荐系统

python scrapy bootstrap jquery css javascript html 租房信息数据展示 租房地址数量分布 租房类型统计 租房价格统计分析 租房面积分析 房屋朝向分析 房屋户型平均价格统计分析 房屋楼层统计分析 房屋楼层与价格统计分析 房屋地址与价格统计分析 房屋相关信息词云展示 租房…

计算机网络——网络层(路由选择协议、路由器工作原理、IP多播、虚拟专用网和网络地址转换)

目录 路由选择协议 因特网的路由选择协议特点 路由信息协议RIP RIP衡量目的网络距离 RIP选择路由器的方式 RIP具有以下三个重要特点 RIP的基本工作流程 RIP的距离向量算法 ​编辑 ​编辑 RIP存在的问题——“坏消息传播得慢” RIP的封装 开放最短路径优先协议OSPF…

关于window配置gitlab和gitee平台共存

今天使用gitlab拉取代码突然提示 gitgitlab.xxx.com: Permission denied (publickey,gssapi-keyex,gssapi-with-mic,password). 以为是ssh公钥没有配置好&#xff0c;遂又进行了一番配置&#xff0c;实际上并不是这个问题造成的&#xff0c;但还是想记录一下步骤&#xff0c;以…

【内网穿透】打洞笔记

文章目录 前言原理阐述公网sshfrp转发服务 实现前提第一步&#xff1a;第二步第三步第四步 补充第五步&#xff08;希望隧道一直开着&#xff09;sftp传数据&#xff08;嫌云服务器上的网太慢&#xff09; 前言 租了一个云服务器&#xff0c;想用vscode的ssh远程连接&#xff…

迅为3A5000_7A2000ATX标准DIY国产龙芯电脑

性能强 采用全国产龙芯3A5000处理器&#xff0c;基于龙芯自主指令系统 (LoongArch)的LA464微结构&#xff0c;并进一步提升频率&#xff0c;降低功耗&#xff0c;优化性能。 桥片 采用龙芯 7A2000&#xff0c;支持PCIE 3.0、USB 3.0和 SATA 3.0.显示接口2 路、HDMI 和1路 VGA&a…

蓝桥杯14小白月赛题解

直接输出pi/ti,for遍历 #include <iostream> using namespace std; #define int long long int a,b,c ; double t1.00; signed main() {cin>>a;int an0;for(int i1;i<a;i){cin>>b>>c;if(t>c*1.00/b){tc*1.00/b;ani;} }cout<<an<<e…

基于JAVA+SpringBoot+Vue+uniapp的微信小程序点餐平台

✌全网粉丝20W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取项目下载方式&#x1f345; 一、项目背景介绍&#xff1a; 点餐小程序主要为小个…

LabVIEW设备检修信息管理系统

开发了基于LabVIEW设计平台开发的设备检修信息管理系统。该系统应用于各种设备的检修基地&#xff0c;通过与基地管理信息系统的连接和数据交换&#xff0c;实现了本地检修工位数据的远程自动化管理&#xff0c;提高了设备的检修效率和安全性。 项目背景 现代设备运维过程中信…

AV1技术学习:Affine Motion Compensation

一、Affine Model Parameter 除了传统的平移运动补偿&#xff0c;AV1 还支持仿射变换模型&#xff0c;将当前像素点 (x, y) 通过以下方式投影到参考帧中的预测像素点 (x, y). 参数 (h13, h23) 对应于平移模型中使用的常规运动向量。 参数 h11 和 h22 控制垂直和水平轴上的比例…

“SelectDB 实时数据仓库解决方案”入围工信部“信息技术应用创新典型解决方案”

7 月 11 日&#xff0c;由工业和信息化部网络安全产业发展中心&#xff08;工业和信息化部信息中心&#xff09;主办的 2024 信息技术应用创新发展大会暨解决方案应用推广大会在天津落下帷幕&#xff0c;会上集中发布了一系列技术水平先进、应用效果突出、产业带动性强的信息技…

MySQL(5)表的查询

目录 1.表的查询 1.表的查询: 1.1创建表: 语法: create table 表名( 字段 类型 约束, 字段 类型 约束, ... 字段 类型 约束 ); 1.2 单行数据全行插入: &#x1f330; 表的字段都插入数据: insert into student values(100, 1000, 唐三藏, null); 1.3 多行数据 指定列插入:…

【Java--数据结构】二叉树

欢迎关注个人主页&#xff1a;逸狼 创造不易&#xff0c;可以点点赞吗~ 如有错误&#xff0c;欢迎指出~ 树结构 树是一种非线性的数据结构&#xff0c;它是由n&#xff08;n>0&#xff09;个有限结点组成一个具有层次关系的集合 注意&#xff1a;树形结构中&#xff0c;子…

Java爬虫安全策略:防止TikTok音频抓取过程中的请求被拦截

摘要 在当今互联网时代&#xff0c;数据采集已成为获取信息的重要手段。然而&#xff0c;随着反爬虫技术的不断进步&#xff0c;爬虫开发者面临着越来越多的挑战。本文将探讨Java爬虫在抓取TikTok音频时的安全策略&#xff0c;包括如何防止请求被拦截&#xff0c;以及如何提高…