双向RNN和双向LSTM

news2024/9/21 4:36:15

双向RNN和双向LSTM

一、双向循环神经网络BiRNN

1、为什么要用BiRNN

双向RNN,即可以从过去的时间点获取记忆,又可以从未来的时间点获取信息,也就是说具有以下两个特点:

捕捉前后文信息:传统的单向 RNN 只能利用先前的上下文信息,而 BiRNN 同时利用了输入序列的前后文信息。在很多任务中,如自然语言处理中的命名实体识别、机器翻译等,理解一个词的前后文语境至关重要。

例如:img

判断句子中Teddy是否是人名,如果只从前面两个词是无法得知Teddy是否是人名,如果能有后面的信息就很好判断了,这就需要用的双向循环神经网络。

提高精度:在处理某些序列数据时,单向 RNN 可能无法充分捕捉整个序列中的重要信息,导致性能欠佳。BiRNN 能够通过双向处理,提高模型的表达能力和准确度。

2、BiRNN的架构

双向循环神经网络(BRNN)的基本思想是提出每一个训练序列向前和向后分别是两个循环神经网络(RNN),而且这两个都连接着一个输出层。这个结构提供给输出层输入序列中每一个点的完整的过去和未来的上下文信息。下图展示的是一个沿着时间展开的双向循环神经网络。六个独特的权值在每一个时步被重复的利用,六个权值分别对应:输入到向前和向后隐含层(w1, w3),隐含层到隐含层自己(w2, w5),向前和向后隐含层到输出层(w4, w6)。值得注意的是:向前和向后隐含层之间没有信息流,这保证了展开图是非循环的。每一个输出都是综合考虑两个方向获得的结果再输出,如下图所示:

在这里插入图片描述

H → t = ϕ ( X t W x h ( f ) + H → t − 1 W h h ( f ) + b h ( f ) ) , H ← t = ϕ ( X t W x h ( b ) + H ← t + 1 W h h ( b ) + b h ( b ) ) , \begin{array}{l} \overrightarrow{\mathbf{H}}_{t}=\phi\left(\mathbf{X}_{t} \mathbf{W}_{x h}^{(f)}+\overrightarrow{\mathbf{H}}_{t-1} \mathbf{W}_{h h}^{(f)}+\mathbf{b}_{h}^{(f)}\right), \\ \overleftarrow{\mathbf{H}}_{t}=\phi\left(\mathbf{X}_{t} \mathbf{W}_{x h}^{(b)}+\overleftarrow{\mathbf{H}}_{t+1} \mathbf{W}_{h h}^{(b)}+\mathbf{b}_{h}^{(b)}\right), \end{array} H t=ϕ(XtWxh(f)+H t1Whh(f)+bh(f)),H t=ϕ(XtWxh(b)+H t+1Whh(b)+bh(b)),
拼接得到结果:
H t = [ H → t H ← t ] \mathbf{H}_{t}=\left[\overrightarrow{\mathbf{H}}_{t} \overleftarrow{\mathbf{H}}_{t}\right] Ht=[H tH t]
至于网络单元到底是标准的RNN还是GRU或者是LSTM是没有关系的

(GRU:把遗忘门和输入门合并成一个更新门(Update Gate),并且把Cell State和Hidden State也合并成一个Hidden State,它的计算如下图所示)

在这里插入图片描述

对于整个双向循环神经网络(BRNN)的计算过程如下:

向前推算(Forward pass):

对于双向循环神经网络(BRNN)的隐含层,向前推算跟单向的循环神经网络(RNN)一样,除了输入序列对于两个隐含层是相反方向的,输出层直到两个隐含层处理完所有的全部输入序列才更新:

img

向后推算(Backward pass):

双向循环神经网络(BRNN)的向后推算与标准的循环神经网络(RNN)通过时间反向传播相似,除了所有的输出层δ项首先被计算,然后返回给两个不同方向的隐含层:

img

3、代码(用IMDB进行情感分析)

import pandas as pd
import torch
import torch.nn as nn
import torch.optim as optim
from sklearn.model_selection import train_test_split
from torch.utils.data import Dataset, DataLoader
import spacy
from torchtext.vocab import GloVe

# 加载数据集
base_csv = r'E:\kaggle\情感分析\archive (1)\IMDB Dataset.csv'
df = pd.read_csv(base_csv)

# 分割数据集
train_data, test_data = train_test_split(df, test_size=0.2, random_state=42)

# 保存为CSV文件
train_data.to_csv('train.csv', index=False)
test_data.to_csv('test.csv', index=False)

# 加载Spacy分词器
spacy_en = spacy.load('en_core_web_sm')
tokenizer = spacy_en.tokenizer

class TextDataset(Dataset):
    def __init__(self, dataframe, text_field, label_field, vocab):
        self.dataframe = dataframe
        self.text_field = text_field
        self.label_field = label_field
        self.vocab = vocab

    def __len__(self):
        return len(self.dataframe)

    def __getitem__(self, idx):
        text = self.dataframe.iloc[idx][self.text_field]
        label = self.dataframe.iloc[idx][self.label_field]
        tokens = tokenizer(text)
        token_ids = [self.vocab.get(token.text, self.vocab['<unk>']) for token in tokens]
        label = 1 if label == "positive" else 0
        return torch.tensor(token_ids, dtype=torch.long), torch.tensor(label, dtype=torch.long)

# 加载预训练词向量
glove_embeddings = GloVe(name='6B', dim=100)
vocab = glove_embeddings.stoi
vocab['<unk>'] = len(vocab)  # 添加 <unk> 标记
glove_embeddings.vectors = torch.cat((glove_embeddings.vectors, torch.zeros(1, glove_embeddings.dim)), 0)

# 创建数据集
train_dataset = TextDataset(train_data, 'review', 'sentiment', vocab)
test_dataset = TextDataset(test_data, 'review', 'sentiment', vocab)

# 创建数据加载器
batch_size = 32

def collate_fn(batch):
    texts, labels = zip(*batch)
    text_lengths = [len(text) for text in texts]
    padded_texts = nn.utils.rnn.pad_sequence(texts, batch_first=True, padding_value=0)
    return padded_texts, torch.tensor(labels, dtype=torch.long)

train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True, collate_fn=collate_fn)
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False, collate_fn=collate_fn)

# 定义模型
class BiRNN(nn.Module):
    def __init__(self, vocab_size, embed_size, hidden_size, num_layers, num_classes, pad_idx):
        super(BiRNN, self).__init__()
        self.hidden_size = hidden_size
        self.num_layers = num_layers
        self.num_directions = 2

        self.embedding = nn.Embedding(vocab_size, embed_size, padding_idx=pad_idx)
        self.rnn = nn.GRU(embed_size, hidden_size, num_layers, batch_first=True, bidirectional=True)
        self.fc = nn.Linear(hidden_size * self.num_directions, num_classes)

    def forward(self, x):
        h0 = torch.zeros(self.num_layers * self.num_directions, x.size(0), self.hidden_size).to(x.device)
        x = self.embedding(x)
        out, _ = self.rnn(x, h0)
        out = self.fc(out[:, -1, :])
        return out

# 设置参数
vocab_size = len(vocab)
embed_size = 100
hidden_size = 256
num_layers = 2
num_classes = 2
pad_idx = 0

model = BiRNN(vocab_size, embed_size, hidden_size, num_layers, num_classes, pad_idx)
model.embedding.weight.data.copy_(glove_embeddings.vectors)
model.embedding.weight.data[pad_idx] = torch.zeros(embed_size)

# 定义损失函数和优化器
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = model.to(device)
criterion = criterion.to(device)

# 训练模型
num_epochs = 5

for epoch in range(num_epochs):
    model.train()
    epoch_loss = 0
    for texts, labels in train_loader:
        texts = texts.to(device)
        labels = labels.to(device)

        optimizer.zero_grad()
        outputs = model(texts)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()
        epoch_loss += loss.item()

    print(f'Epoch {epoch + 1}/{num_epochs}, Loss: {epoch_loss / len(train_loader):.4f}')

# 评估模型
model.eval()
with torch.no_grad():
    correct = 0
    total = 0
    for texts, labels in test_loader:
        texts = texts.to(device)
        labels = labels.to(device)
        outputs = model(texts)
        _, predicted = torch.max(outputs.data, 1)
        total += labels.size(0)
        correct += (predicted == labels).sum().item()

    accuracy = correct / total
    print(f'Accuracy: {accuracy * 100:.2f}%')

# 保存模型
torch.save(model.state_dict(), 'model.ckpt')

# 预测函数
def predict(text, model, vocab, device):
    model.eval()
    tokens = tokenizer(text)
    token_ids = [vocab.get(token.text, vocab['<unk>']) for token in tokens]
    token_tensor = torch.tensor(token_ids, dtype=torch.long).unsqueeze(0).to(device)

    with torch.no_grad():
        output = model(token_tensor)
        _, predicted = torch.max(output.data, 1)
        return 'positive' if predicted.item() == 1 else 'negative'

# 示例使用
sample_text = "This movie was fantastic! I really enjoyed it."
prediction = predict(sample_text, model, vocab, device)
print(f'Prediction: {prediction}')

4、多层RNN

将许多RNN层堆叠,构成一个多层RNN网络。RNN每读取一个新的 x t \mathrm{x}_{\mathrm{t}} xt输就会生成状态向量 h t \mathrm{h}_{\mathrm{t}} ht作为当前时刻的输出和下一时刻的输入状态。 将T 个输入 x 0 ∼ x T \mathrm{x}_{0} \sim \mathrm{x}_{\mathrm{T}} x0xT 依次输入RNN,相应地会产生个输出。第一层RNN输出的T TT个状态向量可以作为第二层RNN的输入,第二层RNN拥有独立的参数,依次读取T个来自第一层RNN的状态向量,产生T个新的输出。第二层RNN输出的T个状态向量可以作为第三层RNN的输入,依此类推,构成一个多层RNN网络。
在这里插入图片描述

RNN自带层数和是否双向,只要把层数设置成你想要的层数就可以

class RNN(nn.Module):
    def __init__(self,vocab_size, embedding_dim, hidden_size, num_classes, num_layers,bidirectional):
        super(RNN, self).__init__()
        self.vocab_size = vocab_size
        self.embedding_dim = embedding_dim
        self.hidden_size = hidden_size
        self.num_classes = num_classes
        self.num_layers = num_layers
        
        self.embedding = nn.Embedding(self.vocab_size, embedding_dim, padding_idx=word2idx['<PAD>'])
        # 这里用了torch的embeding函数
        self.rnn = nn.RNN(input_size=self.embedding_dim, hidden_size=self.hidden_size,batch_first=True,num_layers=self.num_layers)
        # input_size:表示输入 xt 的特征维度,从embeding层的size
        # hidden_size:表示输出的特征维度
        # num_layers:表示网络的层数
        # nonlinearity:表示选用的非线性激活函数,默认是 ‘tanh’
        # bias:表示是否使用偏置,默认使用
        # batch_first:表示输入数据的形式,默认是 False,就是这样形式,(seq, batch, feature),也就是将序列长度放在第一位,batch 放在第二位
        # dropout:表示是否在输出层应用(随机丢掉一些特征,重要的调参)
        # bidirectional:表示是否使用双向的 rnn,默认是 False。
        
    def forward(self, x):
        batch_size, seq_len = x.shape
        h0 = torch.randn(self.num_layers, batch_size, self.hidden_size).to(device)
        #初始化一个h0,也即c0,在RNN中一个Cell输出的ht和Ct是相同的,而LSTM的一个cell输出的ht和Ct是不同的
        #维度[layers, batch, hidden_len]

        x = self.embedding(x)   # 这里用的是nn的embeding
        out,_ = self.rnn(x, h0)  # 输入x,h0是初始化的特征,
        output = self.fc(out[:,-1,:]).squeeze(0) #因为有max_seq_len个时态,所以取最后一个时态即-1层
        return output   

二、双向LSTM----Bi-LSTM

BiLSTM无非就是把cell变成了LSTM的基本单元,其他同理如上所示

需要修改的内容:

  1. 替换RNN层为LSTM层:将nn.RNN替换为nn.LSTM
  2. 设置双向LSTM:将LSTM层的bidirectional参数设置为True
  3. 修改隐藏状态的初始化:LSTM的隐藏状态包含两个张量(隐藏状态和细胞状态),因此需要初始化这两个张量。
  4. 处理双向LSTM的输出:双向LSTM的输出维度会加倍(因为有两个方向)

在这里插入图片描述

class RNN(nn.Module):
    def __init__(self, vocab_size, embedding_dim, hidden_size, num_classes, num_layers, bidirectional):
        super(RNN, self).__init__()
        self.vocab_size = vocab_size
        self.embedding_dim = embedding_dim
        self.hidden_size = hidden_size
        self.num_classes = num_classes
        self.num_layers = num_layers
        self.bidirectional = bidirectional
        self.num_directions = 2 if bidirectional else 1
        
        self.embedding = nn.Embedding(self.vocab_size, embedding_dim, padding_idx=word2idx['<PAD>'])
        
        self.lstm = nn.LSTM(input_size=self.embedding_dim, 
                            hidden_size=self.hidden_size, 
                            num_layers=self.num_layers, 
                            bidirectional=self.bidirectional, 
                            batch_first=True)#替换了原来的nn.RNN层,使其变为LSTM,并添加了双向参数
        
        # 全连接层的输入大小需要考虑双向LSTM的输出维度
        self.fc = nn.Linear(self.hidden_size * self.num_directions, self.num_classes)
    
    def forward(self, x):
        batch_size, seq_len = x.shape
        # 初始化隐藏状态和细胞状态,双向LSTM需要初始化2*num_layers的hidden states
        h0 = torch.zeros(self.num_layers * self.num_directions, batch_size, self.hidden_size).to(device)
        c0 = torch.zeros(self.num_layers * self.num_directions, batch_size, self.hidden_size).to(device)
        
        x = self.embedding(x)  # Embedding layer
        
        # LSTM layer
        out, (hn, cn) = self.lstm(x, (h0, c0))
        
        # 取最后一个时间步的输出,双向LSTM会拼接两个方向的输出
        output = self.fc(out[:, -1, :])
        
        return output

三、两者异同

双向RNN(Bidirectional Recurrent Neural Network)和双向LSTM(Bidirectional Long Short-Term Memory)都是用于处理序列数据的神经网络模型。它们在处理时间序列、自然语言处理等任务中非常有用,因为它们能够考虑到序列中的前后信息。尽管它们有一些相似之处,但也有一些关键的不同点。

1、相同点

  1. 双向结构:无论是双向RNN还是双向LSTM,它们都有双向结构,这意味着它们有两个隐藏层,一个从前向后处理序列,另一个从后向前处理序列。这种结构允许模型在每个时间步上同时考虑前面和后面的信息,从而提高预测性能。

  2. 序列处理:两者都是为序列数据设计的,适用于自然语言处理、时间序列预测等任务。

  3. 隐层状态的结合:在每个时间步,它们都会结合来自两个方向的隐藏状态(通常是通过连接或求和)来产生输出。

2、不同点

  1. 基本单元

    • 双向RNN:基本单元是RNN,即简单的循环神经网络。它们依赖于隐藏状态将信息从一个时间步传播到下一个时间步。然而,RNN在处理长序列时容易遇到梯度消失和梯度爆炸问题,这限制了它们捕捉长距离依赖的能力。
    • 双向LSTM:基本单元是LSTM,即长短期记忆网络。LSTM通过引入遗忘门、输入门和输出门来控制信息的流动,从而更好地解决了梯度消失和梯度爆炸问题。因此,LSTM在处理长序列时比普通RNN更有效。
  2. 记忆能力

    • 双向RNN:由于其结构的限制,普通RNN在处理长期依赖关系时表现不佳。它们更适合短期依赖任务。
    • 双向LSTM:LSTM单元通过其门控机制能够有效地捕捉和保持长期依赖信息,因此在需要长时间记忆的任务中表现更优。
  3. 复杂性和计算成本

    • 双向RNN:结构相对简单,计算成本较低。但由于其局限性,可能需要更多层次的堆叠或者更复杂的架构来提升性能。
    • 双向LSTM:由于引入了门机制,LSTM单元比RNN单元复杂,计算成本也更高。但其增强的记忆能力通常能带来更好的性能。

参考https://zhuanlan.zhihu.com/p/519965073

https://fancyerii.github.io/books/rnn-intro/

https://www.cnblogs.com/Lee-yl/p/10066531.html

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

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

相关文章

电路板维修【四】

【开关电源输出电压偏低不稳&#xff0c;用示波器立马锁定故障范围】&#xff1a;https://www.bilibili.com/video/BV1pf421D73K?vd_source3cc3c07b09206097d0d8b0aefdf07958 可以用示波器查看MOS的输出波形来查看其是否损坏&#xff1a; 电源芯片的供电电压来回跳变&#xf…

一位不合格的面试官在这两周让三位同学破防了

一位不合格的面试官在这两周让三位同学破防了 最近部门招聘 Java 技术同学&#xff1b; 技术需要两面&#xff0c;我也参与招聘过程并作为第一面的面试官&#xff0c;这两周平均每天一个。但是这两周我却让好几位同学破防了&#xff0c;内心其实也是五味杂陈的&#xff0c;做一…

Linux基础之僵尸进程与孤儿进程

目录 一、僵尸进程 1.1 什么是僵尸进程 1.2 为什么要有僵尸状态 1.3 观察我们的僵尸状态 1.4 关于僵尸进程的小Tip 二、孤儿进程 2.1 什么是孤儿进程 一、僵尸进程 1.1 什么是僵尸进程 在上一篇文章中&#xff0c;我们有提到过进程的死亡状态的概念&#xff0c;而我们的…

计算机寄存器是如何实现的

你好&#xff0c;我是 shengjk1&#xff0c;多年大厂经验&#xff0c;努力构建 通俗易懂的、好玩的编程语言教程。 欢迎关注&#xff01;你会有如下收益&#xff1a; 了解大厂经验拥有和大厂相匹配的技术等 希望看什么&#xff0c;评论或者私信告诉我&#xff01; 文章目录 一…

labelimg删除用不到的标签(yolo格式)以及 下载使用

问题&#xff1a;当我们标注完成新的类别后后直接删除classes.txt中不需要的类别之后再次打开labelimg会闪退&#xff0c;如何删除不需要的标签并且能够正确运行呢&#xff1f;&#xff08;yolo格式&#xff09; 原因&#xff1a;当我们打开labelimg进行标注的时候&#xff0c…

Golang RPC实现-day02

导航 Golang RPC实现一、客户端异步并发多个请求1、 客户端结构体2、 一个客户端&#xff0c;异步发送多个请求&#xff0c;使用call结构体代表客户端的每次请求3、客户端并发多个请求4、客户端接收请求 Golang RPC实现 day01 我们实现了简单的服务端和客户端。我们简单总结一…

26 分钟惊讶世界,GPT-4o 引领未来人机交互

前言 原文链接&#xff1a;OpenAI最新模型——GPT-4o&#xff0c;实时语音视频交互&#xff0c;未来人机交互近在眼前 - Kaiho小站 北京时间 5 月 14 日凌晨&#xff0c;OpenAI 发布新一代模型——GPT-4o&#xff0c;仅在 ChatGPT 面世 17 个月后&#xff0c;OpenAI 再次通过…

985大学电子信息专硕,考C语言+数据结构!中央民族大学25计算机考研考情分析!

中央民族大学&#xff08;Minzu University of China&#xff09;坐落于北京市学府林立的海淀区&#xff0c;南邻国家图书馆&#xff0c;北依中关村科技园&#xff0c;校园环境典雅&#xff0c;古朴幽美&#xff0c;人文氛围浓郁&#xff0c;具有鲜明的民族特色。由北京市、国家…

ubuntu下不生成core dumped

1、先用ulimit -c&#xff0c;如果看到0&#xff0c;说明没有开core dump。 所以我们输入ulimit -c unlimited&#xff0c;打开core dump。 再次用ulimit -c&#xff0c;看到unlimited了&#xff0c;说明core dump打开了。 注意这句ulimit -c unlimited只对当前会话有效。要永…

通俗易懂讲乐观锁与悲观锁

浅谈乐观锁与悲观锁 乐观锁和悲观锁是Java并发编程中的两个概念。使用乐观锁和悲观锁可以解决并发编程中数据不一致性、死锁、性能差等问题&#xff0c;乐观锁与悲观锁的实行方式不同&#xff0c;所以其特性也不近相同&#xff0c;下文将详细介绍两者的特性与适用场景。 《熊…

STM32-09-IWDG

文章目录 STM32 IWDG1. IWDG2. IWDG框图3. IWDG寄存器4. IWDG寄存器操作步骤5. IWDG溢出时间计算6. IWDG配置步骤7. 代码实现 STM32 IWDG 1. IWDG IWDG Independent watchdog&#xff0c;即独立看门狗&#xff0c;本质上是一个定时器&#xff0c;这个定时器有一个输出端&#…

ZYNQ之嵌入式驱动开发——字符设备驱动

文章目录 Linux驱动程序分类Linux应用程序和驱动程序的关系简单的测试驱动程序在petalinux中添加LED驱动新字符设备驱动 Linux驱动程序分类 驱动程序分为字符设备驱动、块设备驱动和网络设备驱动。 字符设备是按字节访问的设备&#xff0c;比如以一个字节收发数据的串口&#…

谷歌全力反击 OpenAI:Google I/O 2024 揭晓 AI 新篇章,一场激动人心的技术盛宴

&#x1f680; 谷歌全力反击 OpenAI&#xff1a;Google I/O 2024 揭晓 AI 新篇章&#xff0c;一场激动人心的技术盛宴&#xff01; 在这个人工智能的全新时代&#xff0c;只有谷歌能让你眼前一亮&#xff01;来自全球瞩目的 Google I/O 2024 开发者大会&#xff0c;谷歌用一场…

项目组GIT操作规范

分支规范 在开发过程中&#xff0c;一般会存在以下几种分支&#xff1a; main分支(master) master为主分支&#xff0c;也是用于部署生产环境的分支&#xff0c;一般由 dev 以及 fixbug分支合并&#xff0c;任何时间都不能直接修改代码。dev分支 develop 为开发分支&#xff…

Altium Designer封装库和元器件符号库下载与导入教程(SnapEDA 、Ultra Librarian、Alldatasheetcn)

1.AD封装库和元器件符号库下载网址 以下是一些全球热门的Altium Designer封装库和元器件符号库下载网址推荐&#xff1a; Altium Content Vault (现称为Altium Manufacturer Part Search)&#xff1a;这是Altium官方提供的元器件库&#xff0c;可以直接在Altium Designer中使用…

Java码农的福音:再也不怕乱码了

即便是Java这样成熟的语言&#xff0c;开发者们也常常会遇到一个恼人的问题——乱码。 本文将深入探讨乱码的根本原因&#xff0c;并针对Java开发中的乱码场景提出有效的解决方案&#xff0c;辅以实战代码&#xff0c;让Java程序员从此告别乱码困扰。 一&#xff0c;字符集的…

文件存储解决方案-阿里云OSS

文章目录 1.菜单分级显示问题1.问题引出1.苹果灯&#xff0c;放到节能灯下面也就是id大于1272.查看菜单&#xff0c;并没有出现苹果灯3.放到灯具下面id42&#xff0c;就可以显示 2.问题分析和解决1.判断可能出现问题的位置2.找到递归返回树形菜单数据的位置3.这里出现问题的原因…

什么是最大路径?什么是极大路径?

最近学习中&#xff0c;在这两个概念上出现了混淆&#xff0c;导致了一些误解&#xff0c;在此厘清。 最大路径 在一个简单图G中&#xff0c;u、v之间的距离 d ( u , v ) min ⁡ { u 到 v 的最短路的长度 } d(u,v) \min \{ u到v的最短路的长度 \} d(u,v)min{u到v的最短路的…

音乐的力量

常听音乐的好处可以让人消除工作紧张、减轻生活压力、避免各类慢性疾病等等&#xff0c;其实这些都是有医学根据的。‍ 在医学研究中发现&#xff0c;经常的接触音乐节 奏、旋律会对人体的脑波、心跳、肠胃蠕动、神经感应等等&#xff0c;产生某些作用&#xff0c;进而促进身心…

Postman基础功能-接口返回值获取

大家好&#xff0c;之前给大家分享关于Postman的接口关联&#xff0c;我们平时在做接口测试时&#xff0c;请求接口返回的数据都是很复杂的 JSON 数据&#xff0c;有着多层嵌套&#xff0c;这样的数据层级在 Postman 中要怎么获取呢&#xff1f; 接下来给大家展示几个获取 JSO…