NLP深度学习 DAY5:Sequence-to-sequence 模型详解

Seq2Seq(Sequence-to-Sequence)模型是一种用于处理输入和输出均为序列任务的深度学习模型。它最初被设计用于机器翻译,但后来广泛应用于其他任务,如文本摘要、对话系统、语音识别、问答系统等。


核心思想

Seq2Seq 模型的目标是将一个序列(如一句话)转换为另一个序列,例如:

  • 输入:英文句子 "Hello, how are you?"

  • 输出:法语句子 "Bonjour, comment ça va ?"


模型结构

Seq2Seq 模型通常由两部分组成:

  1. 编码器(Encoder)

    • 将输入序列(如源语言句子)编码为一个固定长度的上下文向量(Context Vector),用于捕捉输入序列的语义信息。

    • 常用循环神经网络(RNN、LSTM、GRU)或 Transformer 作为编码器。

  2. 解码器(Decoder)

    • 基于编码器生成的上下文向量,逐步生成输出序列(如目标语言句子)。

    • 解码器通常也是一个循环神经网络(或 Transformer),每一步生成一个输出词,直到生成结束符(如 <EOS>)。

首先,来简单介绍下RNN(循环神经网络)结构:

1. RNN 简介

                 

                                                                RNN结构  

RNN中,每个单元接受两个输入,一个是当前时间步输入的信息 X_{t} ,另一个是上一个单元的隐藏层状态 H_{t-1}。为什么这种结构的RNN适合用于做文本等序列型数据的任务,主要是因为隐藏状态的存在使得模型具有记忆性。针对不同的任务,根据输入和输出的数量,通常对RNN结构进行调整。

RNN的常见几种结构

1.1 N to N

该模型处理的一般是输入和输出序列长度相等的任务,如

  • 词性标注
  • 语言模型(Language Modeling)

                   

1.2 1 to N

此类结构的输入长度为1,输出长度为N,一般又可以分为两种:一种是将输入只输入到第一个神经元,另一种将输入输入到所有神经元。

一般用于以下任务:

图像生成文字,一般输入 X 为图片,输出为一段图片描述性的文字;
输入音乐类别,生成对应的音乐
根据小说(新闻情感)类别,生成对应的文字

                         

                         

1.3 N to 1

和1 to N相反,一般常见任务有:

  • 序列分类任务,如给定一段文本或语音序列,归类(情感分类,主题分类等)

              

2. Seq2Seq 模型

经过上面对几种RNN结构的分析,不难发现RNN结构大多对序列的长度比较局限,对于类似于机器翻译的任务,输入和输出长度并不对等,为N to M的结构,简单的RNN束手无策,因此便有了新的模型,Encoder-Decoder模型,也就是Seq2Seq模型。

模型一般由两部分组成:第一部分是Encoder部分,用于对输入的N长度的序列进行表征;第二部分是Decoder部分,用于将Encoder提取出的表征建立起到输出的M长度序列的映射。

2.1 编码器 Encoder 

                            

Encoder部分一般使用了普通RNN的结构。其将一个序列表征为一个定长的上下文向量c,计算方式有多种,如下:

2.2 解码器 Decoder

相对于编码器而言,解码器的结构更多,下面介绍三种:

第一种

                                     

这种结构直接Encoder得到的上下文向量作为RNN的初始隐藏状态输入到RNN结构中,后续单元不接受 c 的输入,计算公式如下:

  • 隐藏状态的更新:

                                                      

   

 第二种

                              

第二种将Encoder得到的上下文向量作为每个神经单元的输入,不再是只作为第一个单元的初始隐藏状态。计算公式如下:

第三种

                             

第三种在 c 的处理上和第二种类似,但是区别在于将前一个神经单元的输出作为当前神经单元的输出。计算公式如下:

3. Seq2Seq中的Trick

3.1 Teacher Forcing

主要针对第三种Decoder应用。当某一个单元输出出错时,如果将其输出传递给下一个单元,可能导致错误传递下去。这时候,需要在一定程度上减少这种传递,就采用按一定的比例决定是否对神经单元采用上一个上一个单元的输出作为输入。即:

3.2 Attention 机制(很重要)

提出Attention机制之前,我们先来看下之前的结构有什么问题:

核心问题是当序列过长时,上述的Encoder输出的上下文向量 c 无法记住所有信息,会出现长序列梯度消失的问题。比如句子有100个词,那么c里面可能丢失了前几个词的信息。

Attention 机制是怎样的?

Attention 机制其实是参考了人在翻译文章时候的注意力机制,它会将模型的注意力放在当前翻译的单词上,换句话说,它并不像前面提到的Encoder的结构一样对整个句子用一个表征,它对于每个单词都有一个以单词为中心的表征。Encoder结构如下:

                                  

3.3 束搜索(Beam Search)

注意:Beam Search只用于测试,不用于训练过程。
当模型训练好后,给其输入一段话,其输出的每个单元的 y 给的是各个词的概率,我们如何根据概率选词且如何判断是否句子终止呢?

采取的方法是在每个时间步,选取当前时间步条件概率最大的k个词,作为该时间步的候选输出序列。如下图,k选择2,第一步p(A|c)和p(C|c)最大;第二步 P(AB|c),P(CE|c)最大;第三步P(ABD|c),P(CED|c)最大。

这样,得到的最终候选序列就是各个时间步的得到的序列的集合,下图种即为6个 {A, C, AB, CE, ABD, CED}。那么最终预测结果就是要从这6个中选出分最高的。 

这时候,可能有小伙伴会发现,那按概率算的话,序列越长的概率肯定越小呀,所以一般最后分数计算会有一个和序列长度有关的惩罚系数,如下: 

其中L为候选序列的长度,α 一般选0.75. 这样一来,序列长的对应的系数更小,而由于取了对数,概率的对数是负数,如此变化后会使得长序列和短序列处于一个可比的情形。 

4. 详细代码对比(RNN VS Seq2Seq)

4.1 基础 RNN 示例(以文本分类为例)

4.1.1 模型结构

  • 输入:一个批次的句子(或序列),已被转化为数字索引表示(如词向量)。
  • RNN:使用 nn.RNN  来提取序列特征。
  • 输出:可以是序列最后一个时间步(或平均池化后的序列向量)接一个线性层,最终输出分类结果。
import torch
import torch.nn as nn

class BasicOnlyRNNModel(nn.Module):
    def __init__(self, vocab_size, embedding_dim, hidden_dim, output_dim,
                 num_layers=1, bidirectional=False):
        super(BasicOnlyRNNModel, self).__init__()
        
        # 词向量层
        self.embedding = nn.Embedding(vocab_size, embedding_dim)
        
        # 仅使用RNN
        self.rnn = nn.RNN(
            input_size=embedding_dim, 
            hidden_size=hidden_dim, 
            num_layers=num_layers,
            batch_first=True,
            bidirectional=bidirectional
        )
        
        # 双向时需要 *2
        self.num_directions = 2 if bidirectional else 1
        rnn_output_dim = hidden_dim * self.num_directions

        # 全连接层, 用于将 RNN 的输出映射到分类空间
        self.fc = nn.Linear(rnn_output_dim, output_dim)
        
    def forward(self, text):
        """
        text: [batch_size, seq_len]
        """
        # 嵌入: [batch_size, seq_len] -> [batch_size, seq_len, embedding_dim]
        embedded = self.embedding(text)
        
        # 前向传播 RNN
        # rnn_out: [batch_size, seq_len, hidden_dim * num_directions]
        # hidden : [num_layers * num_directions, batch_size, hidden_dim]
        rnn_out, hidden = self.rnn(embedded)  
        
        # 取最后一层的 hidden state
        # hidden 的形状: [num_layers * num_directions, batch_size, hidden_dim]
        # 若是单向 RNN,hidden[-1] 即可
        # 若是双向 RNN,需拼接正向和反向最后时刻
        if self.num_directions == 1:
            out = hidden[-1, :, :]  # [batch_size, hidden_dim]
        else:
            # 对双向的情况,hidden[-2] 与 hidden[-1] 分别是正向/反向最后时刻
            out = torch.cat((hidden[-2, :, :], hidden[-1, :, :]), dim=1)
            # [batch_size, hidden_dim * 2]
        
        # 全连接映射至分类空间
        logits = self.fc(out)  # [batch_size, output_dim]
        return logits

# ================ 以下是训练/推理时的简单示例 ================
if __name__ == "__main__":
    # 假设我们有一些超参数
    vocab_size = 10000      # 词典大小
    embedding_dim = 128     # 词向量维度
    hidden_dim = 256        # RNN 隐藏层维度
    output_dim = 2          # 假设做二分类
    num_layers = 1          # RNN 层数
    bidirectional = True    # 是否使用双向

    model = BasicOnlyRNNModel(
        vocab_size=vocab_size,
        embedding_dim=embedding_dim,
        hidden_dim=hidden_dim,
        output_dim=output_dim,
        num_layers=num_layers,
        bidirectional=bidirectional
    )

    # 假设当前批次的输入
    batch_size = 32
    seq_len = 50
    batch_text = torch.randint(0, vocab_size, (batch_size, seq_len))  # [batch_size, seq_len]

    # 前向传播
    logits = model(batch_text)  # 形状: [32, 2]
    print(logits.shape)  # 输出: torch.Size([32, 2])
基础 RNN 模型特点
  1. 数据流:输入序列 → 嵌入层 → RNN → 最后一层 hidden state → 全连接层 → 输出。
  2. 应用场景:常用于序列分类/回归(文本分类、情感分析、序列标注等)。
  3. 优点:结构简单,实现快速。
  4. 缺点:对长序列依赖可能不够,若需要输出一个序列(而不是一个标量或一个向量),就需要进一步改造或循环解码。

二、Seq2Seq 示例(以机器翻译为例)

对于Seq2Seq模型,一般包含 Encoder(编码器)Decoder(解码器)两个主要部分(可选地加入Attention机制)。下文示例使用 LSTM 结构,演示最基础的编码器-解码器形式(不带注意力机制)。带注意力的情况会多一步 Attention 计算。

1. Encoder(编码器)

import torch
import torch.nn as nn

class Encoder(nn.Module):
    def __init__(self, vocab_size, embedding_dim, hidden_dim, num_layers=1):
        super(Encoder, self).__init__()
        self.hidden_dim = hidden_dim
        self.num_layers = num_layers

        self.embedding = nn.Embedding(vocab_size, embedding_dim)
        self.lstm = nn.LSTM(embedding_dim, hidden_dim, num_layers, batch_first=True)

    def forward(self, src):
        """
        src: [batch_size, src_len]
        """
        embedded = self.embedding(src)  # [batch_size, src_len, embedding_dim]
        
        outputs, (hidden, cell) = self.lstm(embedded)
        # outputs: [batch_size, src_len, hidden_dim]
        # hidden: [num_layers, batch_size, hidden_dim]
        # cell:   [num_layers, batch_size, hidden_dim]
        return hidden, cell

2. Decoder(解码器)

class Decoder(nn.Module):
    def __init__(self, vocab_size, embedding_dim, hidden_dim, num_layers=1):
        super(Decoder, self).__init__()
        self.hidden_dim = hidden_dim
        self.num_layers = num_layers

        self.embedding = nn.Embedding(vocab_size, embedding_dim)
        self.lstm = nn.LSTM(embedding_dim, hidden_dim, num_layers, batch_first=True)
        self.fc_out = nn.Linear(hidden_dim, vocab_size)  # 输出到词表大小

    def forward(self, tgt, hidden, cell):
        """
        tgt: [batch_size] 当前时间步解码器输入(可以是一个词或一批词)
        hidden, cell: 编码器传过来的(或者上一时间步传下来的)隐状态
        """
        # 解码器一般一次输入一个token,也可以批量处理多个
        # 这里假设 tgt 形状是 [batch_size],先unsqueeze到[batch_size, 1]
        tgt = tgt.unsqueeze(1)  # [batch_size, 1]
        
        embedded = self.embedding(tgt)  # [batch_size, 1, embedding_dim]
        
        output, (hidden, cell) = self.lstm(embedded, (hidden, cell))
        # output: [batch_size, 1, hidden_dim]
        
        prediction = self.fc_out(output.squeeze(1))  # [batch_size, vocab_size]
        return prediction, hidden, cell

3. Seq2Seq 整合 

class Seq2Seq(nn.Module):
    def __init__(self, encoder, decoder, device):
        super(Seq2Seq, self).__init__()
        self.encoder = encoder
        self.decoder = decoder
        self.device = device
    
    def forward(self, src, tgt, teacher_forcing_ratio=0.5):
        """
        src: [batch_size, src_len]
        tgt: [batch_size, tgt_len] 训练时通常会有“开始标志 <sos>”和“结束标志 <eos>”。
        teacher_forcing_ratio: float,表示在训练时使用老师强制的概率
        """
        batch_size = src.shape[0]
        tgt_len = tgt.shape[1]
        vocab_size = self.decoder.fc_out.out_features

        # 用于存储解码器每个时间步的输出词分布
        outputs = torch.zeros(batch_size, tgt_len, vocab_size).to(self.device)
        
        # 1. 编码器对源序列进行编码,获取初始 hidden, cell
        hidden, cell = self.encoder(src)
        
        # 2. 解码器的第一个输入通常是 <sos>
        input_t = tgt[:, 0]  # 取第0个词(即<sos>)
        
        for t in range(1, tgt_len):
            # 2.1 将当前时间步的 token 喂给 Decoder
            output, hidden, cell = self.decoder(input_t, hidden, cell)
            # output 形状: [batch_size, vocab_size]

            # 2.2 存储
            outputs[:, t, :] = output
            
            # 2.3 决定下一时间步输入:是用模型自己的预测还是用老师的标签(teacher forcing)
            teacher_force = torch.rand(1).item() < teacher_forcing_ratio
            top1 = output.argmax(1)  # [batch_size], 最大值所对应的词索引
            
            input_t = tgt[:, t] if teacher_force else top1
        
        return outputs
Seq2Seq 模型特点
  1. 整体结构:输入序列先经过 Encoder 编码,将序列信息“压缩”到 hidden, cell(或 h_n, c_n),再在解码阶段通过 Decoder 逐步解码出目标序列。
  2. 输出:是一个序列而非一个标量/向量,通过循环或一步步地喂入上一步的输出(或真实标签)来生成。
  3. 扩展:在解码阶段可加入 Attention(Luong/Bahdanau 等),或者进一步用 Transformer 替代 RNN,形成更强大的序列到序列生成模型。
  4. 应用场景:机器翻译、文本摘要、对话系统、序列到序列的生成任务(如代码自动生成、语音到文本等)。

三、RNN VS Seq2Seq(回答问题:为什么要有编码器?直接把词向量拼在一起,然后用 RNN 1 to N 不行吗?)

在序列到序列(Seq2Seq)任务中(如机器翻译、文本摘要等),直接使用RNN后通过全连接层输出(1 to N)看似简单,但存在以下关键问题,而编码器-解码器(Encoder-Decoder)结构通过分步编码和解码的方式有效解决了这些挑战:

在序列到序列(Seq2Seq)任务中(如机器翻译、文本摘要等),直接使用词嵌入后通过全连接层(“M个神经元”)输出看似简单,但存在以下关键问题,而编码器-解码器(Encoder-Decoder)结构通过分步编码和解码的方式有效解决了这些挑战:


1. 序列的时序依赖性

自然语言中的单词顺序至关重要。例如:

  • 句子1猫追老鼠

  • 句子2老鼠追猫
    两个句子包含相同的词,但含义完全相反。

  • 简单词嵌入+全连接的缺陷
    若直接将所有词嵌入拼接成一个向量(如[猫, 追, 老鼠] → 一个长向量),模型会丢失词序信息,无法区分两个句子的差异。

  • 编码器-解码器的优势
    通过LSTM或Transformer按顺序处理输入词,编码器能够保留词序信息,并在隐藏状态中传递时序依赖关系。


2. 输入和输出的变长问题

在Seq2Seq任务中,输入和输出的长度通常是动态变化的。例如:

  • 输入:英文句子 "Hello world"(2个词)

  • 输出:中文翻译 "你好世界"(3个词)

  • 简单词嵌入+全连接的缺陷
    全连接层需要固定维度的输入和输出,无法处理变长序列。

  • 编码器-解码器的优势

    • 编码器可处理任意长度的输入序列,将其压缩为固定长度的上下文向量(hiddencell)。

    • 解码器基于上下文向量逐步生成变长的输出序列(逐词生成,直到预测到<eos>)。


3. 长距离依赖建模

语言中常存在跨越多个单词的依赖关系。例如:

  • 句子The cat, which was hungry and had been wandering the streets for days, finally found some food.
    主句的主语cat与谓语found相隔很远。

  • 简单词嵌入+全连接的缺陷
    全连接层难以捕捉长距离依赖(尤其是当句子较长时)。

  • 编码器-解码器的优势

    • LSTM通过门控机制(遗忘门、输入门)逐步更新cell状态,传递长期信息。

    • Transformer通过自注意力机制(Self-Attention)直接建模词与词之间的全局依赖。


4. 信息压缩与语义表示

编码器的核心作用是将输入序列编码为一个全局语义表示(上下文向量)。

  • 简单词嵌入+全连接的缺陷
    直接将所有词嵌入拼接为一个向量,缺乏对整体语义的抽象(相当于“词袋模型”)。

  • 编码器-解码器的优势

    • 编码器通过循环或注意力机制逐步融合上下文信息,生成紧凑的语义表示。

    • 解码器基于此表示逐步展开生成目标序列,确保输出与输入语义一致。


5. 模型效率与参数共享

  • 简单词嵌入+全连接的缺陷
    若输入长度为N,输出长度为M,全连接层的参数量为 (N×embedding_dim) × M,随序列长度增长迅速膨胀,导致计算成本高且易过拟合。

  • 编码器-解码器的优势

    • LSTM或Transformer通过参数共享(同一层处理所有时间步),参数量仅与隐藏层维度相关,与序列长度无关。

    • 例如,LSTM的参数量为 4×(embedding_dim + hidden_dim)×hidden_dim,与输入长度N无关。


6. 实际案例对比

假设用两种模型处理机器翻译任务:

方案1:简单全连接
  • 输入:将源句子所有词嵌入拼接为一个向量(如N=5embedding_dim=256 → 输入维度1280)。

  • 输出:直接映射到目标语言的词表(如vocab_size=10000),参数量为 1280×10000 = 12.8M

  • 问题

    • 无法处理长度变化的输入输出。

    • 无法建模词序和长距离依赖。

    • 参数量大且难以训练。

方案2:编码器-解码器(LSTM)
  • 编码器:LSTM逐步处理源序列,输出上下文向量(如hidden_dim=256)。

  • 解码器:LSTM基于上下文向量逐词生成目标序列。

  • 参数量:编码器和解码器的LSTM参数量均为 4×(256+256)×256 ≈ 1M,总计约2M

  • 优势

    • 处理变长序列。

    • 建模词序和长距离依赖。

    • 参数量小且高效。


总结

编码器-解码器结构通过分步编码和解码,解决了以下核心问题:

  1. 时序依赖性:保留词序信息。

  2. 变长序列处理:动态生成输出。

  3. 长距离依赖建模:通过LSTM或注意力机制捕捉全局关系。

  4. 语义压缩与传递:生成紧凑的上下文表示。

  5. 模型效率:参数共享降低计算成本。

直接使用词嵌入+全连接的方案虽然在理论上可行,但无法满足实际任务的需求。编码器-解码器及其改进模型(如Transformer)已成为序列建模的主流方法,广泛应用于机器翻译、对话系统、文本生成等任务。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

于动态规划的启幕之章,借 C++ 笔触绘就算法新篇

注意&#xff1a;代码由易到难 P1216 [IOI 1994] 数字三角形 Number Triangles 题目链接&#xff1a;[IOI 1994] 数字三角形 Number Triangles - 洛谷 题目描述 观察下面的数字金字塔。 写一个程序来查找从最高点到底部任意处结束的路径&#xff0c;使路径经过数字的和最大。每…

Three.js 后期处理(Post-Processing)详解

目录 前言 一、什么是后期处理&#xff1f; 二、Three.js 后期处理的工作流程 2.1 创建 EffectComposer 2.2 添加渲染通道&#xff08;Render Pass&#xff09; 2.3 应用最终渲染 三、后期处理实现示例 3.1 基础代码 四、常见的后期处理效果 4.1 辉光效果&#xf…

低代码系统-产品架构案例介绍、炎黄盈动-易鲸云(十二)

易鲸云作为炎黄盈动新推出的产品&#xff0c;在定位上为低零代码产品。 开发层 表单引擎 表单设计器&#xff0c;包括设计和渲染 流程引擎 流程设计&#xff0c;包括设计和渲染&#xff0c;需要说明的是&#xff1a;采用国际标准BPMN2.0&#xff0c;可以全球通用 视图引擎 视图…

从 HTTP/1.1 到 HTTP/3:如何影响网页加载速度与性能

一、前言 在最近使用Apipost时&#xff0c;突然注意到了http/1.1和http/2&#xff0c;如下图&#xff1a; 在我根深蒂固的记忆中&#xff0c;对于http的理解还停留在TCP协议、三次握手。由于我的好奇心&#xff0c;于是触发了我被动“开卷”&#xff0c;所以有了这篇文章&…

项目练习:重写若依后端报错cannot be cast to com.xxx.model.LoginUser

文章目录 一、情景说明二、解决办法 一、情景说明 在重写若依后端服务的过程中 使用了Redis存放LoginUser对象数据 那么&#xff0c;有存就有取 在取值的时候&#xff0c;报错 二、解决办法 方法1、在TokenService中修改如下 getLoginUser 方法中&#xff1a;LoginUser u…

C语言------二维数组指针从入门到精通

前言: 目标:需要了解及掌握数组指针的行地址、列地址、具体元素地址、具体元素地址的值是怎样定义及实现。 重点:指针的偏移,指针解引用。 难点:指针的升阶与降阶。 1. 基本概念 二维数组&#xff1a;二维数组可以看作是一个数组的数组。例如&#xff0c;int a[3][4] 表示一个 …

AI-ISP论文Learning to See in the Dark解读

论文地址&#xff1a;Learning to See in the Dark 图1. 利用卷积网络进行极微光成像。黑暗的室内环境。相机处的照度小于0.1勒克斯。索尼α7S II传感器曝光时间为1/30秒。(a) 相机在ISO 8000下拍摄的图像。(b) 相机在ISO 409600下拍摄的图像。该图像存在噪点和色彩偏差。©…

自定义数据集 ,使用朴素贝叶斯对其进行分类

代码&#xff1a; # 导入必要的库 import numpy as np import matplotlib.pyplot as plt# 定义类1的数据点&#xff0c;每个数据点是二维的坐标 class1_points np.array([[1.9, 1.2],[1.5, 2.1],[1.9, 0.5],[1.5, 0.9],[0.9, 1.2],[1.1, 1.7],[1.4, 1.1]])# 定义类2的数据点&…

蓝桥杯单片机第七届省赛

前言 这套题不难&#xff0c;相对于第六套题这一套比较简单了&#xff0c;但是还是有些小细节要抓 题目 OK&#xff0c;以上就是全部的题目了&#xff0c;这套题目相对来说逻辑比较简单&#xff0c;四个按键&#xff0c;S4控制pwm占空比&#xff0c;S5控制计时时间&#xff0…

小程序设计和开发:如何研究同类型小程序的优点和不足。

一、确定研究目标和范围 明确研究目的 在开始研究同类型小程序之前&#xff0c;首先需要明确研究的目的。是为了改进自己的小程序设计和开发&#xff0c;还是为了了解市场趋势和用户需求&#xff1f;不同的研究目的会影响研究的方法和重点。例如&#xff0c;如果研究目的是为了…

反向代理模块jmh

1 概念 1.1 反向代理概念 反向代理是指以代理服务器来接收客户端的请求&#xff0c;然后将请求转发给内部网络上的服务器&#xff0c;将从服务器上得到的结果返回给客户端&#xff0c;此时代理服务器对外表现为一个反向代理服务器。 对于客户端来说&#xff0c;反向代理就相当…

一文讲解HashMap线程安全相关问题

HashMap不是线程安全的&#xff0c;主要有以下几个问题&#xff1a; ①、多线程下扩容会死循环。JDK1.7 中的 HashMap 使用的是头插法插入元素&#xff0c;在多线程的环境下&#xff0c;扩容的时候就有可能导致出现环形链表&#xff0c;造成死循环。 JDK 8 时已经修复了这个问…

oracle:子查询

子查询: 一条查询语句中嵌入了另一条查询语句, 被嵌入里面的这条查询语句称为子查询, 外面的查询语句称为主查询 子查询的分类 相关性子查询&#xff08;Correlated Subquery&#xff09;是指子查询的执行依赖于外部查询的每一行数据。也就是说&#xff0c;子查询会对外部查询…

AI技术在SEO关键词优化中的应用策略与前景展望

内容概要 在数字营销的快速发展中&#xff0c;AI技术逐渐成为SEO领域的核心驱动力。其通过强大的数据分析和处理能力&#xff0c;不仅改变了我们优化关键词的方式&#xff0c;也提升了搜索引擎优化的效率和效果。在传统SEO中&#xff0c;关键词的选择与组合常依赖人工经验和直…

Java项目: 基于SpringBoot+mybatis+maven+mysql实现的疫苗发布和接种预约管理系统(含源码+数据库+开题报告+毕业论文)

一、项目简介 本项目是一套基于SpringBootmybatismavenmysql疫苗发布和接种预约管理系统 包含&#xff1a;项目源码、数据库脚本等&#xff0c;该项目附带全部源码可作为毕设使用。 项目都经过严格调试&#xff0c;eclipse或者idea 确保可以运行&#xff01; 该系统功能完善、…

VSCode中快速创建Html文件

1、下载并安装好VSCode软件&#xff0c;启动软件。 2、依次点击左上角选项卡“file”-“New File”。 输入文件名称后缀&#xff0c;如&#xff1a;HelloWorld.html。 选择新建文件的目录所在位置。 3、HelloWorld.html中输入英文格式的!&#xff0c;按回车键后会默认依据…

CMake项目编译与开源项目目录结构

Cmake 使用简单方便&#xff0c;可以跨平台构建项目编译环境&#xff0c;尤其比直接写makefile简单&#xff0c;可以通过简单的Cmake生成负责的Makefile文件。 如果没有使用cmake进行编译&#xff0c;需要如下命令&#xff1a;&#xff08;以muduo库echo服务器为例&#xff09;…

网络原理(4)—— 网络层详解

目录 一. IP协议报头结构 二. 地址管理 2.1 路由器 2.1.1 路由选择 2.1.2 WAN口&#xff08;Wide Area Network&#xff09; 2.1.3 LAN口&#xff08;Local Area Network&#xff09; 2.1.4 WLAN口&#xff08;Wireless Local Area Network&#xff09; 2.2 网段划分…

Hot100之图论

200岛屿数量 题目 思路解析 把访问过的格子插上棋子 思想是先污染再治理&#xff0c;我们有一个inArea&#xff08;&#xff09;函数&#xff0c;是判断是否出界了 我们先dfs&#xff08;&#xff09;放各个方向遍历&#xff0c;然后我们再把这个位置标为0 我们岛屿是连着…

【HarmonyOS之旅】基于ArkTS开发(三) -> 兼容JS的类Web开发(三)

目录 1 -> 生命周期 1.1 -> 应用生命周期 1.2 -> 页面生命周期 2 -> 资源限定与访问 2.1 -> 资源限定词 2.2 -> 资源限定词的命名要求 2.3 -> 限定词与设备状态的匹配规则 2.4 -> 引用JS模块内resources资源 3 -> 多语言支持 3.1 -> 定…