昇思25天学习打卡营第11天|RNN实现情感分类

概述

情感分类是自然语言处理中的经典任务,是典型的分类问题。本节使用MindSpore实现一个基于RNN网络的情感分类模型,实现如下的效果:

输入: This film is terrible
正确标签: Negative
预测标签: Negative

输入: This film is great
正确标签: Positive
预测标签: Positive

数据准备

本节使用情感分类的经典数据集IMDB影评数据集,数据集包含Positive和Negative两类,下面为其样例:

ReviewLabel
"Quitting" may be as much about exiting a pre-ordained identity as about drug withdrawal. As a rural guy coming to Beijing, class and success must have struck this young artist face on as an appeal to separate from his roots and far surpass his peasant parents' acting success. Troubles arise, however, when the new man is too new, when it demands too big a departure from family, history, nature, and personal identity. The ensuing splits, and confusion between the imaginary and the real and the dissonance between the ordinary and the heroic are the stuff of a gut check on the one hand or a complete escape from self on the other.Negative
This movie is amazing because the fact that the real people portray themselves and their real life experience and do such a good job it's like they're almost living the past over again. Jia Hongsheng plays himself an actor who quit everything except music and drugs struggling with depression and searching for the meaning of life while being angry at everyone especially the people who care for him most.Positive

此外,需要使用预训练词向量对自然语言单词进行编码,以获取文本的语义特征,本节选取Glove词向量作为Embedding。

数据下载模块

为了方便数据集和预训练词向量的下载,首先设计数据下载模块,实现可视化下载流程,并保存至指定路径。数据下载模块使用requests库进行http请求,并通过tqdm库对下载百分比进行可视化。此外针对下载安全性,使用IO的方式下载临时文件,而后保存至指定的路径并返回。

tqdmrequests库需手动安装,命令如下:pip install tqdm requests

%%capture captured_output
# 实验环境已经预装了mindspore==2.2.14,如需更换mindspore版本,可更改下面mindspore的版本号
!pip uninstall mindspore -y
!pip install -i https://pypi.mirrors.ustc.edu.cn/simple mindspore==2.2.14

# 查看当前 mindspore 版本
!pip show mindspore
import os
import shutil
import requests
import tempfile
from tqdm import tqdm
from typing import IO
from pathlib import Path

# 指定保存路径为 `home_path/.mindspore_examples`
cache_dir = Path.home() / '.mindspore_examples'

def http_get(url: str, temp_file: IO):
    """使用requests库下载数据,并使用tqdm库进行流程可视化"""
    req = requests.get(url, stream=True)
    content_length = req.headers.get('Content-Length')
    total = int(content_length) if content_length is not None else None
    progress = tqdm(unit='B', total=total)
    for chunk in req.iter_content(chunk_size=1024):
        if chunk:
            progress.update(len(chunk))
            temp_file.write(chunk)
    progress.close()

def download(file_name: str, url: str):
    """下载数据并存为指定名称"""
    if not os.path.exists(cache_dir):
        os.makedirs(cache_dir)
    cache_path = os.path.join(cache_dir, file_name)
    cache_exist = os.path.exists(cache_path)
    if not cache_exist:
        with tempfile.NamedTemporaryFile() as temp_file:
            http_get(url, temp_file)
            temp_file.flush()
            temp_file.seek(0)
            with open(cache_path, 'wb') as cache_file:
                shutil.copyfileobj(temp_file, cache_file)
    return cache_path

完成数据下载模块后,下载IMDB数据集进行测试(此处使用华为云的镜像用于提升下载速度)。下载过程及保存的路径如下:

imdb_path = download('aclImdb_v1.tar.gz', 'https://mindspore-website.obs.myhuaweicloud.com/notebook/datasets/aclImdb_v1.tar.gz')
imdb_path

'/home/nginx/.mindspore_examples/aclImdb_v1.tar.gz'

加载IMDB数据集

下载好的IMDB数据集为tar.gz文件,我们使用Python的tarfile库对其进行读取,并将所有数据和标签分别进行存放。原始的IMDB数据集解压目录如下:

    ├── aclImdb
    │   ├── imdbEr.txt
    │   ├── imdb.vocab
    │   ├── README
    │   ├── test
    │   └── train
    │         ├── neg
    │         ├── pos
    ...

数据集已分割为train和test两部分,且每部分包含neg和pos两个分类的文件夹,因此需分别train和test进行读取并处理数据和标签。

import re
import six
import string
import tarfile

class IMDBData():
    """IMDB数据集加载器

    加载IMDB数据集并处理为一个Python迭代对象。

    """
    label_map = {
        "pos": 1,
        "neg": 0
    }
    def __init__(self, path, mode="train"):
        self.mode = mode
        self.path = path
        self.docs, self.labels = [], []

        self._load("pos")
        self._load("neg")

    def _load(self, label):
        pattern = re.compile(r"aclImdb/{}/{}/.*\.txt$".format(self.mode, label))
        # 将数据加载至内存
        with tarfile.open(self.path) as tarf:
            tf = tarf.next()
            while tf is not None:
                if bool(pattern.match(tf.name)):
                    # 对文本进行分词、去除标点和特殊字符、小写处理
                    self.docs.append(str(tarf.extractfile(tf).read().rstrip(six.b("\n\r"))
                                         .translate(None, six.b(string.punctuation)).lower()).split())
                    self.labels.append([self.label_map[label]])
                tf = tarf.next()

    def __getitem__(self, idx):
        return self.docs[idx], self.labels[idx]

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

 完成IMDB数据加载器后,加载训练数据集进行测试,输出数据集数量:

imdb_train = IMDBData(imdb_path, 'train')
len(imdb_train)

25000

将IMDB数据集加载至内存并构造为迭代对象后,可以使用mindspore.dataset提供的Generatordataset接口加载数据集迭代对象,并进行下一步的数据处理,下面封装一个函数将train和test分别使用Generatordataset进行加载,并指定数据集中文本和标签的column_name分别为textlabel:

import mindspore.dataset as ds

def load_imdb(imdb_path):
    imdb_train = ds.GeneratorDataset(IMDBData(imdb_path, "train"), column_names=["text", "label"], shuffle=True, num_samples=10000)
    imdb_test = ds.GeneratorDataset(IMDBData(imdb_path, "test"), column_names=["text", "label"], shuffle=False)
    return imdb_train, imdb_test

加载IMDB数据集,可以看到imdb_train是一个GeneratorDataset对象。

imdb_train, imdb_test = load_imdb(imdb_path)
imdb_train

<mindspore.dataset.engine.datasets_user_defined.GeneratorDataset at 0xfffece2a5be0>

加载预训练词向量

预训练词向量是对输入单词的数值化表示,通过nn.Embedding层,采用查表的方式,输入单词对应词表中的index,获得对应的表达向量。 因此进行模型构造前,需要将Embedding层所需的词向量和词表进行构造。这里我们使用Glove(Global Vectors for Word Representation)这种经典的预训练词向量, 其数据格式如下:

WordVector
the0.418 0.24968 -0.41242 0.1217 0.34527 -0.044457 -0.49688 -0.17862 -0.00066023 ...
,0.013441 0.23682 -0.16899 0.40951 0.63812 0.47709 -0.42852 -0.55641 -0.364 ...

我们直接使用第一列的单词作为词表,使用dataset.text.Vocab将其按顺序加载;同时读取每一行的Vector并转为numpy.array,用于nn.Embedding加载权重使用。具体实现如下:

import zipfile
import numpy as np

def load_glove(glove_path):
    glove_100d_path = os.path.join(cache_dir, 'glove.6B.100d.txt')
    if not os.path.exists(glove_100d_path):
        glove_zip = zipfile.ZipFile(glove_path)
        glove_zip.extractall(cache_dir)

    embeddings = []
    tokens = []
    with open(glove_100d_path, encoding='utf-8') as gf:
        for glove in gf:
            word, embedding = glove.split(maxsplit=1)
            tokens.append(word)
            embeddings.append(np.fromstring(embedding, dtype=np.float32, sep=' '))
    # 添加 <unk>, <pad> 两个特殊占位符对应的embedding
    embeddings.append(np.random.rand(100))
    embeddings.append(np.zeros((100,), np.float32))

    vocab = ds.text.Vocab.from_list(tokens, special_tokens=["<unk>", "<pad>"], special_first=False)
    embeddings = np.array(embeddings).astype(np.float32)
    return vocab, embeddings

由于数据集中可能存在词表没有覆盖的单词,因此需要加入<unk>标记符;同时由于输入长度的不一致,在打包为一个batch时需要将短的文本进行填充,因此需要加入<pad>标记符。完成后的词表长度为原词表长度+2。

下面下载Glove词向量,并加载生成词表和词向量权重矩阵。

glove_path = download('glove.6B.zip', 'https://mindspore-website.obs.myhuaweicloud.com/notebook/datasets/glove.6B.zip')
vocab, embeddings = load_glove(glove_path)
len(vocab.vocab())

400002

使用词表将the转换为index id,并查询词向量矩阵对应的词向量:

idx = vocab.tokens_to_ids('the')
embedding = embeddings[idx]
idx, embedding

(0,
 array([-0.038194, -0.24487 ,  0.72812 , -0.39961 ,  0.083172,  0.043953,
        -0.39141 ,  0.3344  , -0.57545 ,  0.087459,  0.28787 , -0.06731 ,
         0.30906 , -0.26384 , -0.13231 , -0.20757 ,  0.33395 , -0.33848 ,
        -0.31743 , -0.48336 ,  0.1464  , -0.37304 ,  0.34577 ,  0.052041,
         0.44946 , -0.46971 ,  0.02628 , -0.54155 , -0.15518 , -0.14107 ,
        -0.039722,  0.28277 ,  0.14393 ,  0.23464 , -0.31021 ,  0.086173,
         0.20397 ,  0.52624 ,  0.17164 , -0.082378, -0.71787 , -0.41531 ,
         0.20335 , -0.12763 ,  0.41367 ,  0.55187 ,  0.57908 , -0.33477 ,
        -0.36559 , -0.54857 , -0.062892,  0.26584 ,  0.30205 ,  0.99775 ,
        -0.80481 , -3.0243  ,  0.01254 , -0.36942 ,  2.2167  ,  0.72201 ,
        -0.24978 ,  0.92136 ,  0.034514,  0.46745 ,  1.1079  , -0.19358 ,
        -0.074575,  0.23353 , -0.052062, -0.22044 ,  0.057162, -0.15806 ,
        -0.30798 , -0.41625 ,  0.37972 ,  0.15006 , -0.53212 , -0.2055  ,
        -1.2526  ,  0.071624,  0.70565 ,  0.49744 , -0.42063 ,  0.26148 ,
        -1.538   , -0.30223 , -0.073438, -0.28312 ,  0.37104 , -0.25217 ,
         0.016215, -0.017099, -0.38984 ,  0.87424 , -0.72569 , -0.51058 ,
        -0.52028 , -0.1459  ,  0.8278  ,  0.27062 ], dtype=float32))

数据集预处理

通过加载器加载的IMDB数据集进行了分词处理,但不满足构造训练数据的需要,因此要对其进行额外的预处理。其中包含的预处理如下:

  • 通过Vocab将所有的Token处理为index id。
  • 将文本序列统一长度,不足的使用<pad>补齐,超出的进行截断。

这里我们使用mindspore.dataset中提供的接口进行预处理操作。这里使用到的接口均为MindSpore的高性能数据引擎设计,每个接口对应操作视作数据流水线的一部分,详情请参考MindSpore数据引擎。 首先针对token到index id的查表操作,使用text.Lookup接口,将前文构造的词表加载,并指定unknown_token。其次为文本序列统一长度操作,使用PadEnd接口,此接口定义最大长度和补齐值(pad_value),这里我们取最大长度为500,填充值对应词表中<pad>的index id。

除了对数据集中text进行预处理外,由于后续模型训练的需要,要将label数据转为float32格式。

import mindspore as ms

lookup_op = ds.text.Lookup(vocab, unknown_token='<unk>')
pad_op = ds.transforms.PadEnd([500], pad_value=vocab.tokens_to_ids('<pad>'))
type_cast_op = ds.transforms.TypeCast(ms.float32)

 完成预处理操作后,需将其加入到数据集处理流水线中,使用map接口对指定的column添加操作。

imdb_train = imdb_train.map(operations=[lookup_op, pad_op], input_columns=['text'])
imdb_train = imdb_train.map(operations=[type_cast_op], input_columns=['label'])

imdb_test = imdb_test.map(operations=[lookup_op, pad_op], input_columns=['text'])
imdb_test = imdb_test.map(operations=[type_cast_op], input_columns=['label'])

由于IMDB数据集本身不包含验证集,我们手动将其分割为训练和验证两部分,比例取0.7, 0.3。

imdb_train, imdb_valid = imdb_train.split([0.7, 0.3])

最后指定数据集的batch大小,通过batch接口指定,并设置是否丢弃无法被batch size整除的剩余数据。

调用数据集的mapsplitbatch为数据集处理流水线增加对应操作,返回值为新的Dataset类型。现在仅定义流水线操作,在执行时开始执行数据处理流水线,获取最终处理好的数据并送入模型进行训练。

imdb_train = imdb_train.batch(64, drop_remainder=True)
imdb_valid = imdb_valid.batch(64, drop_remainder=True)

模型构建

完成数据集的处理后,我们设计用于情感分类的模型结构。首先需要将输入文本(即序列化后的index id列表)通过查表转为向量化表示,此时需要使用nn.Embedding层加载Glove词向量;然后使用RNN循环神经网络做特征提取;最后将RNN连接至一个全连接层,即nn.Dense,将特征转化为与分类数量相同的size,用于后续进行模型优化训练。整体模型结构如下:

nn.Embedding -> nn.RNN -> nn.Dense

这里我们使用能够一定程度规避RNN梯度消失问题的变种LSTM(Long short-term memory)做特征提取层。下面对模型进行详解:

Embedding

Embedding层又可称为EmbeddingLookup层,其作用是使用index id对权重矩阵对应id的向量进行查找,当输入为一个由index id组成的序列时,则查找并返回一个相同长度的矩阵,例如:

embedding = nn.Embedding(1000, 100) # 词表大小(index的取值范围)为1000,表示向量的size为100
input shape: (1, 16)                # 序列长度为16
output shape: (1, 16, 100)

这里我们使用前文处理好的Glove词向量矩阵,设置nn.Embeddingembedding_table为预训练词向量矩阵。对应的vocab_size为词表大小400002,embedding_size为选用的glove.6B.100d向量大小,即100。

RNN(循环神经网络)

循环神经网络(Recurrent Neural Network, RNN)是一类以序列(sequence)数据为输入,在序列的演进方向进行递归(recursion)且所有节点(循环单元)按链式连接的神经网络。下图为RNN的一般结构:

RNN-0

图示左侧为一个RNN Cell循环,右侧为RNN的链式连接平铺。实际上不管是单个RNN Cell还是一个RNN网络,都只有一个Cell的参数,在不断进行循环计算中更新。

由于RNN的循环特性,和自然语言文本的序列特性(句子是由单词组成的序列)十分匹配,因此被大量应用于自然语言处理研究中。下图为RNN的结构拆解:

RNN

RNN单个Cell的结构简单,因此也造成了梯度消失(Gradient Vanishing)问题,具体表现为RNN网络在序列较长时,在序列尾部已经基本丢失了序列首部的信息。为了克服这一问题,LSTM(Long short-term memory)被提出,通过门控机制(Gating Mechanism)来控制信息流在每个循环步中的留存和丢弃。下图为LSTM的结构拆解:

LSTM

本节我们选择LSTM变种而不是经典的RNN做特征提取,来规避梯度消失问题,并获得更好的模型效果。下面来看MindSpore中nn.LSTM对应的公式:

ℎ0:𝑡,(ℎ𝑡,𝑐𝑡)=LSTM(𝑥0:𝑡,(ℎ0,𝑐0))ℎ0:𝑡,(ℎ𝑡,𝑐𝑡)=LSTM(𝑥0:𝑡,(ℎ0,𝑐0))

这里nn.LSTM隐藏了整个循环神经网络在序列时间步(Time step)上的循环,送入输入序列、初始状态,即可获得每个时间步的隐状态(hidden state)拼接而成的矩阵,以及最后一个时间步对应的隐状态。我们使用最后的一个时间步的隐状态作为输入句子的编码特征,送入下一层。

Time step:在循环神经网络计算的每一次循环,成为一个Time step。在送入文本序列时,一个Time step对应一个单词。因此在本例中,LSTM的输出ℎ0:𝑡ℎ0:𝑡对应每个单词的隐状态集合,ℎ𝑡ℎ𝑡和𝑐𝑡𝑐𝑡对应最后一个单词对应的隐状态。

Dense

在经过LSTM编码获取句子特征后,将其送入一个全连接层,即nn.Dense,将特征维度变换为二分类所需的维度1,经过Dense层后的输出即为模型预测结果。

import math
import mindspore as ms
import mindspore.nn as nn
import mindspore.ops as ops
from mindspore.common.initializer import Uniform, HeUniform

class RNN(nn.Cell):
    def __init__(self, embeddings, hidden_dim, output_dim, n_layers,
                 bidirectional, pad_idx):
        super().__init__()
        vocab_size, embedding_dim = embeddings.shape
        self.embedding = nn.Embedding(vocab_size, embedding_dim, embedding_table=ms.Tensor(embeddings), padding_idx=pad_idx)
        self.rnn = nn.LSTM(embedding_dim,
                           hidden_dim,
                           num_layers=n_layers,
                           bidirectional=bidirectional,
                           batch_first=True)
        weight_init = HeUniform(math.sqrt(5))
        bias_init = Uniform(1 / math.sqrt(hidden_dim * 2))
        self.fc = nn.Dense(hidden_dim * 2, output_dim, weight_init=weight_init, bias_init=bias_init)

    def construct(self, inputs):
        embedded = self.embedding(inputs)
        _, (hidden, _) = self.rnn(embedded)
        hidden = ops.concat((hidden[-2, :, :], hidden[-1, :, :]), axis=1)
        output = self.fc(hidden)
        return output

损失函数与优化器

完成模型主体构建后,首先根据指定的参数实例化网络;然后选择损失函数和优化器。针对本节情感分类问题的特性,即预测Positive或Negative的二分类问题,我们选择nn.BCEWithLogitsLoss(二分类交叉熵损失函数)。

hidden_size = 256
output_size = 1
num_layers = 2
bidirectional = True
lr = 0.001
pad_idx = vocab.tokens_to_ids('<pad>')

model = RNN(embeddings, hidden_size, output_size, num_layers, bidirectional, pad_idx)
loss_fn = nn.BCEWithLogitsLoss(reduction='mean')
optimizer = nn.Adam(model.trainable_params(), learning_rate=lr)

训练逻辑

在完成模型构建,进行训练逻辑的设计。一般训练逻辑分为一下步骤:

  1. 读取一个Batch的数据;
  2. 送入网络,进行正向计算和反向传播,更新权重;
  3. 返回loss。

下面按照此逻辑,使用tqdm库,设计训练一个epoch的函数,用于训练过程和loss的可视化。

def forward_fn(data, label):
    logits = model(data)
    loss = loss_fn(logits, label)
    return loss

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

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

def train_one_epoch(model, train_dataset, epoch=0):
    model.set_train()
    total = train_dataset.get_dataset_size()
    loss_total = 0
    step_total = 0
    with tqdm(total=total) as t:
        t.set_description('Epoch %i' % epoch)
        for i in train_dataset.create_tuple_iterator():
            loss = train_step(*i)
            loss_total += loss.asnumpy()
            step_total += 1
            t.set_postfix(loss=loss_total/step_total)
            t.update(1)

评估指标和逻辑

训练逻辑完成后,需要对模型进行评估。即使用模型的预测结果和测试集的正确标签进行对比,求出预测的准确率。由于IMDB的情感分类为二分类问题,对预测值直接进行四舍五入即可获得分类标签(0或1),然后判断是否与正确标签相等即可。下面为二分类准确率计算函数实现:

def binary_accuracy(preds, y):
    """
    计算每个batch的准确率
    """

    # 对预测值进行四舍五入
    rounded_preds = np.around(ops.sigmoid(preds).asnumpy())
    correct = (rounded_preds == y).astype(np.float32)
    acc = correct.sum() / len(correct)
    return acc

有了准确率计算函数后,类似于训练逻辑,对评估逻辑进行设计, 分别为以下步骤:

  1. 读取一个Batch的数据;
  2. 送入网络,进行正向计算,获得预测结果;
  3. 计算准确率。

同训练逻辑一样,使用tqdm进行loss和过程的可视化。此外返回评估loss至供保存模型时作为模型优劣的判断依据。

在进行evaluate时,使用的模型是不包含损失函数和优化器的网络主体; 在进行evaluate前,需要通过model.set_train(False)将模型置为评估状态,此时Dropout不生效。

def evaluate(model, test_dataset, criterion, epoch=0):
    total = test_dataset.get_dataset_size()
    epoch_loss = 0
    epoch_acc = 0
    step_total = 0
    model.set_train(False)

    with tqdm(total=total) as t:
        t.set_description('Epoch %i' % epoch)
        for i in test_dataset.create_tuple_iterator():
            predictions = model(i[0])
            loss = criterion(predictions, i[1])
            epoch_loss += loss.asnumpy()

            acc = binary_accuracy(predictions, i[1])
            epoch_acc += acc

            step_total += 1
            t.set_postfix(loss=epoch_loss/step_total, acc=epoch_acc/step_total)
            t.update(1)

    return epoch_loss / total

模型训练与保存

前序完成了模型构建和训练、评估逻辑的设计,下面进行模型训练。这里我们设置训练轮数为5轮。同时维护一个用于保存最优模型的变量best_valid_loss,根据每一轮评估的loss值,取loss值最小的轮次,将模型进行保存。为节省用例运行时长,此处num_epochs设置为2,可根据需要自行修改。

num_epochs = 2
best_valid_loss = float('inf')
ckpt_file_name = os.path.join(cache_dir, 'sentiment-analysis.ckpt')

for epoch in range(num_epochs):
    train_one_epoch(model, imdb_train, epoch)
    valid_loss = evaluate(model, imdb_valid, loss_fn, epoch)

    if valid_loss < best_valid_loss:
        best_valid_loss = valid_loss
        ms.save_checkpoint(model, ckpt_file_name)

可以看到每轮Loss逐步下降,在验证集上的准确率逐步提升。

模型加载与测试

模型训练完成后,一般需要对模型进行测试或部署上线,此时需要加载已保存的最优模型(即checkpoint),供后续测试使用。这里我们直接使用MindSpore提供的Checkpoint加载和网络权重加载接口:1.将保存的模型Checkpoint加载到内存中,2.将Checkpoint加载至模型。

load_param_into_net接口会返回模型中没有和Checkpoint匹配的权重名,正确匹配时返回空列表。

param_dict = ms.load_checkpoint(ckpt_file_name)
ms.load_param_into_net(model, param_dict)

([], [])

 对测试集打batch,然后使用evaluate方法进行评估,得到模型在测试集上的效果。

imdb_test = imdb_test.batch(64)
evaluate(model, imdb_test, loss_fn)

自定义输入测试

最后我们设计一个预测函数,实现开头描述的效果,输入一句评价,获得评价的情感分类。具体包含以下步骤:

  1. 将输入句子进行分词;
  2. 使用词表获取对应的index id序列;
  3. index id序列转为Tensor;
  4. 送入模型获得预测结果;
  5. 打印输出预测结果。

具体实现如下:

score_map = {
    1: "Positive",
    0: "Negative"
}

def predict_sentiment(model, vocab, sentence):
    model.set_train(False)
    tokenized = sentence.lower().split()
    indexed = vocab.tokens_to_ids(tokenized)
    tensor = ms.Tensor(indexed, ms.int32)
    tensor = tensor.expand_dims(0)
    prediction = model(tensor)
    return score_map[int(np.round(ops.sigmoid(prediction).asnumpy()))]

 最后我们预测开头的样例,可以看到模型可以很好地将评价语句的情感进行分类。

predict_sentiment(model, vocab, "This film is terrible")

'Negative'

predict_sentiment(model, vocab, "This film is great")

'Positive'

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

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

相关文章

SpringBoot之健康监控(Actuator)

1&#xff0c;基本介绍 Spring Actuator 是 Spring Boot 提供的一个扩展模块&#xff0c;用于监控和管理应用程序的生产环境。它通过 HTTP 端点暴露了大量的监控和管理功能&#xff0c;使得开发者可以在运行时查看应用程序的运行状况、配置信息、性能指标等。 主要功能&#…

【Qt】探索Qt框架:开发经典贪吃蛇游戏的全过程与实践

文章目录 引言项目链接&#xff1a;1. Qt框架的使用简介2. 贪吃蛇游戏设计2.1 游戏规则和玩法介绍2.2 游戏界面设计概述 3. 核心代码解析3.1 主界面&#xff08;GameHall&#xff09;3.1.1 布局和功能介绍3.1.2 代码实现分析 3.2 游戏选择界面&#xff08;GameSelect&#xff0…

WPF+MvvmLight 项目入门完整教程(一)

WPF+MvvmLight入门完整教程一 创建项目MvvmLight框架安装完善整个项目的目录结构创建自定义的字体资源下载更新和使用字体资源创建项目 打开VS2022,点击创建新项目,选择**WPF应用(.NET Framework)** 创建一个名称为 CommonProject_DeskTop 的项目,如下图所示:MvvmLight框架…

redis原理之底层数据结构(二)-压缩列表

1.绪论 压缩列表是redis最底层的结构之一&#xff0c;比如redis中的hash&#xff0c;list在某些场景下使用的都是压缩列表。接下来就让我们看看压缩列表结构究竟是怎样的。 2.ziplist 2.1 ziplist的组成 在低版本中压缩列表是由ziplist实现的&#xff0c;我们来看看他的结构…

uniapp 微信小程序根据后端返回的文件链接打开并保存到手机文件夹中【支持doc、docx、txt、xlsx等类型的文件】

项目场景&#xff1a; 我们在使用uniapp官方提供的uni.downloadFile以及uni.saveFile时&#xff0c;会发现这个文件下载的默认保存位置和我们预想的不太一样&#xff0c;容易找不到&#xff0c;而且没有提示&#xff0c;那么我们就需要把文件打开自己保存并且有提示保存到哪个…

fastadmin导入vue

前台 require-frontend.js或frontend-init.js 后台 require-backend.js或backend-init.js 后台 方法一 require-backend.js 在 paths 中加入’vue’:‘…/libs/vue/vue.min’, 在shim 中加入 paths: {............vue:../libs/vue/vue.min, } shim: {............vue: {ex…

acrobat 中 PDF 复制时不能精确选中所选内容所在行的一种解决方法

现象&#xff1a;划取行的时候&#xff0c;自动扩展为多行 如果整段选中复制&#xff0c;粘贴后是乱码 解决步骤 识别完&#xff0c;保存 验证 可以按行复制了。 如果遇到仅使用 acrobat OCR 不能彻底解决的&#xff0c;更换其他自己熟悉的进行 OCR。

小程序-模板与配置

一、WXML模板语法 1.数据绑定 2.事件绑定 什么是事件 小程序中常用的事件 事件对象的属性列表 target和currentTarget的区别 bindtap的语法格式 在事件处理函数中为data中的数据赋值 事件传参 &#xff08;以下为错误示例&#xff09; 3.事件传参与数据同步 4.条件渲染 …

three.js创建基础模型

场景是一个三维空间&#xff0c;是所有物品的容器。可以将其想象成一个空房间&#xff0c;里面可以放置要呈现的物体、相机、光源等。 通过new THREE.Scene()来创建一个新的场景。 /**1. 创建场景 -- 放置物体对象的环境*/ const scene new THREE.Scene();场景只是一个三维的…

如何安装node.js

Node.js Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行时环境。 主要特点和优势&#xff1a; 非阻塞 I/O 和事件驱动&#xff1a;能够高效处理大量并发连接&#xff0c;非常适合构建高并发的网络应用&#xff0c;如 Web 服务器、实时聊天应用等。 例如&#xff0c;在…

电脑远程开关机

1. 远程开机 参考&#xff1a;https://post.smzdm.com/p/664774/ 1.1 Wake On LAN - 局域网唤醒&#xff08;需要主板支持&#xff0c;一般都支持&#xff09; 要使用远程唤醒&#xff0c;有几种方式&#xff1a;使用类似向日葵开机棒&#xff08;很贵&#xff09;、公网ip&…

车载音视频MediaPlayer优化方案

媒体播放现状 从手机到车载&#xff0c;在很多地方还是有很大的不同。针对多媒体的场景Android车机目前大部分结构大致结构如下图&#xff1a; 从以上图看出的问题&#xff1a; 各个音视频APP单独实现播控界面&#xff0c;播放链路不一致&#xff0c;使用的底层播放器和音频焦…

书生大模型实战入门:Git

任务1: 破冰活动&#xff1a;自我介绍 任务2: 实践项目&#xff1a;构建个人项目 任务1: 破冰活动&#xff1a;自我介绍 VSCODE下载Git插件&#xff1a; 打开Github&#xff1a;https://github.com/InternLM/Tutorial&#xff0c;新建分支&#xff08;Fork&#xff09; 创建后…

在VS2017下FFmpeg+SDL编写最简单的视频播放器

1.下载ShiftMediaProject/FFmpeg 2.下载SDL2 3.新建VC控制台应用 3.配置include和lib 4.把FFmpeg和SDL的dll 复制到工程Debug目录下&#xff0c;并设置调试命令 5.复制一下mp4视频到工程Debug目录下&#xff08;复制一份到*.vcxproj同一目录&#xff0c;用于调试&#xff09; 6…

Spring MVC入门5

你能学到什么 获取header的两种方式学习Spring MVC的第三部分 “ 响应 ” 返回静态页面返回HTML代码返回JSON设置状态码 正文 获取header 获取Header也是从 HttpServletRequest 中获取 再举个例子 获取header的简洁方法&#xff08;RequestHeader&#xff09; Reques…

rtf是什么格式的文件?rtf格式和word的区别是什么?

RTF是什么格式的文件? RTF&#xff08;富文本格式&#xff0c;Rich Text Format&#xff09;和Word文档&#xff08;以.doc和.docx为扩展名的Microsoft Word文档&#xff09;是两种常用的文本文件格式。它们在文件结构、兼容性、功能和使用场景等方面存在一些显著差异。 比如…

macOS Sequoia 15(Macos15系统)v15.0 Beta 3发布 macOS Sequoia 15 功能预览

macOS Sequoia 15 功能预览 犀利一如 Mac macOS Sequoia 15(Macos15系统)v15.0 Beta 3测试版本下载安装 连续互通 你的 Mac 上&#xff0c;iPhone 用起来。 有了 iPhone 镜像功能&#xff0c;在 Mac 上就能看到 iPhone 屏幕画面&#xff0c;还能直接进行操控&#xff0c;不拿…

网络安全——防御(防火墙)带宽以及双机热备实验

12&#xff0c;对现有网络进行改造升级&#xff0c;将当个防火墙组网改成双机热备的组网形式&#xff0c;做负载分担模式&#xff0c;游客区和DMZ区走FW3&#xff0c;生产区和办公区的流量走FW1 13&#xff0c;办公区上网用户限制流量不超过100M&#xff0c;其中销售部人员在其…

内容长度不同的div如何自动对齐展示

平时我们经常会遇到页面内容div结构相同页&#xff0c;这时为了美观我们会希望div会对齐展示&#xff0c;但当div里的文字长度不一时又不想写固定高度&#xff0c;就会出现div长度长长短短&#xff0c;此时实现样式可以这样写&#xff1a; .e-commerce-Wrap {display: flex;fle…

使用Copilot 高效开发繁忙的一天

在现代软件开发的世界里&#xff0c;使用AI工具如GitHub Copilot可以显著提高开发效率。 早晨&#xff1a;规划与启动 7:00 AM - 起床与准备 开发者早早起床&#xff0c;享用健康的早餐&#xff0c;并浏览新闻和技术博客&#xff0c;了解最新的科技动态。快速整理思路&#x…