从零实现深度学习框架——再探多层双向RNN的实现

news2025/3/10 11:14:37

来源:投稿 作者:175
编辑:学姐

往期内容:

从零实现深度学习框架1:RNN从理论到实战(理论篇)

从零实现深度学习框架2:RNN从理论到实战(实战篇)

从零实现深度学习框架3:再探多层双向RNN的实现(本篇)

在前面的文章中,我们实现了多层、双向RNN。但是这几天一直在思考,这种实现方式是不是有问题。因为RNN的实现关乎后面ELMo和seq2seq,所以不得不重视。

双向RNN的实现方式

以两层双向RNN为例。我们之前实现的方式类似如下图所示:

这两张图片来自于:https://github.com/pytorch/pytorch/issues/4930#issuecomment-361851298

就是正向RNN和反向RNN可以看成是两个独立的两层RNN网络,最终拼接了它们的输出。但是总感觉双向RNN不会这么简单,带着这个疑问去拜读了双向RNN的论文1,得到下面的这张图片:

如果采用这种方式的话,那么两层双向RNN的实现应该像下图这样:

即第一层BRNN的输出同时考虑了正向和方向输出,将它们拼接在一起,作为第二层BRNN的输入。

但是这时遇到了一个问题,如果这样实现的话,那么输出的维度会怎样呢?BRNN中每层参数的维度会产生怎样的变化呢?

遇事不决找Torch,我们摸着PyTorch过河。

带着这个问题,我们去看PyTorch的文档,并查阅资料,梳理一下PyTorch实现的RNN(GRU、LSTM)中各种输入、输出、隐藏状态的维度。

理解RNN中的各种维度

以RNN为例,为什么不以最复杂的LSTM为例呢?因为LSTM参数过多,相比RNN太过复杂,不太容易理解。柿子要挑软的捏,我们理解了RNN,再去理解GRU或LSTM就会简单多了。

此图片参考了https://stackoverflow.com/a/48305882

从上图可以看出,在一个堆叠了l层的RNN中,output包含了最后一层RNN输出的所有隐藏状态;h_n包含了最后一个时间步上所有层的输出。

我们知道了它们的构成方式,下面看一下它们和上图中另外两个参数inputh_0在不同类型的RNN中维度如何2。

  • inputRNN的输入序列。若batch_first=False,则其大小为(seq_len, batch_size, input_size);若batch_first=True,则其大小为(batch_size, seq_len, input_size);

  • h_0 RNN的初始隐藏状态,可以为空。大小为(num_layers * num_directions, batch_size, hidden_size);

  • output RNN最后一层所有时间步的输出。若batch_first=False,则其大小为(seq_len, batch_size, num_directions * hidden_size);若batch_first=True,则其大小为(batch_size, seq_len, num_directions * hidden_size);

  • h_nRNN中所有层最后一个时间步的隐藏状态。其大小为(num_layers * num_directions, batch_size, hidden_size)。不受batch_first的影响,其批次维度表现和batch_first=False一样。后面以代码实现的角度解释下为何这样,不代表官方的意图。

其中seq_len表示输入序列长度;batch_size表示批次大小;input_size表示输入的特征数量;num_layers表示层数;num_directions表示方向个数,单向RNN时为1,双向RNN时为2;hidden_size表示隐藏状态的特征数。

h_0的形状应该和h_n是一致的。

下面我们进行验证,首先看一下初始参数:

# 输入大小
INPUT_SIZE = 2
# 序列长度
SEQ_LENGTH = 5
# 隐藏大小
HIDDEN_SIZE = 3
# 批大小
BATCH_SIZE = 4

以及输入:

inputs = Tensor.randn(BATCH_SIZE, SEQ_LENGTH, INPUT_SIZE)

简单RNN

简单RNN就是单向单层RNN:

rnn = nn.RNN(input_size=INPUT_SIZE, hidden_size=HIDDEN_SIZE, num_layers=1, batch_first=True)

output, h_n = rnn(inputs)

print(f'Input Shape: {inputs.shape} ')
print(f'Output Shape: {output.shape} ')
print(f'Hidden Shape: {h_n.shape} ')
  • inputs维度是我们预先定理好的,注意这里batch_first=True,所以inputs的第一个维度是批大小。

  • output来自最后一层所有时间步的输出,时间步长度为5,包含整个批次内4条数据,每条数据的输出维度为3,可以理解为3分类问题。

  • $h_n$来自单层最后一个时间步的隐藏状态,包含整个批次内4条数据,每条数据的输出维度为3。

Input Shape: (4, 5, 2) 
Output Shape: (4, 5, 3) 
Hidden Shape: (1, 4, 3) 

堆叠RNN

如果将层数改成3,我们就得到了3层RNN堆叠在一起的架构,来看下此时output和h_n的维度会发生怎样的变化。

rnn = nn.RNN(input_size=INPUT_SIZE, hidden_size=HIDDEN_SIZE, num_layers=3, batch_first=True)

output, h_n = rnn(inputs)

print(f'Input Shape: {inputs.shape} ')
print(f'Output Shape: {output.shape} ')
print(f'Hidden Shape: {h_n.shape} ')
Input Shape: (4, 5, 2) 
Output Shape: (4, 5, 3) 
Hidden Shape: (3, 4, 3) 
  • output来自最后一层所有时间步的输出,时间步长度为5,包含整个批次内4条数据,每条数据的输出维度为3。其维度保持不变。

  • h_n来自所有三层最后一个时间步的隐藏状态,包含整个批次内4条数据,每条数据的输出维度为3。可以看到,其输出的第一个维度大小由1变成了3,因为包含了3层的结果。

双向RNN

传入bidirectional=True,并将层数改回单层。

rnn = nn.RNN(input_size=INPUT_SIZE, hidden_size=HIDDEN_SIZE, num_layers=1, batch_first=True, bidirectional=True)

output, h_n = rnn(inputs)

print(f'Input Shape: {inputs.shape} ')
print(f'Output Shape: {output.shape} ')
print(f'Hidden Shape: {h_n.shape} ')
Input Shape: (4, 5, 2) 
Output Shape: (4, 5, 6) 
Hidden Shape: (2, 4, 3) 
  • output来自最后一层所有时间步的输出,时间步长度为5,包含整个批次内4条数据,每条数据的输出维度为3,由于是双向,包含了两个方向上的结果,在此维度上进行堆叠,所以由3变成了6。

  • h_n最后一个时间步的隐藏状态,包含整个批次内4条数据,每条数据的输出维度为3。第一个维度由1变成了2,因为在此维度上堆叠了双向的结果。

它们都包含了双向的结果,那如果想分别得到每个方向上的结果,要怎么做呢?

对于output。若batch_first=True,将output按照out.reshape(shape=(batch_size, seq_len, num_directions, hidden_size))进行变形,正向和反向的维度值为别为0和1。

对于h_n,按照h_n.reshape(shape=(num_layers, num_directions, batch_size, hidden_size)),正向和反向的维度值为别为0和1。

我们来对output进行拆分:

# batch_first=True
output_reshaped = output.reshape((BATCH_SIZE, SEQ_LENGTH, 2, HIDDEN_SIZE))
print("Shape of the output after directions are separated: ", output_reshaped.shape)

# 分别获取正向和反向的输出
output_forward = output_reshaped[:, :, 0, :]
output_backward = output_reshaped[:, :, 1, :]
print("Forward output Shape: ", output_forward.shape)
print("Backward output Shape: ", output_backward.shape)
Shape of the output after directions are separated:  (4, 5, 2, 3)
Forward output Shape:  (4, 5, 3)
Backward output Shape:  (4, 5, 3)

对h_n进行拆分:

# 1: 层数   2: 方向数
h_n_reshaped = h_n.reshape((1, 2, BATCH_SIZE, HIDDEN_SIZE))
print("Shape of the hidden after directions are separated: ", h_n_reshaped.shape)

h_n_forward = h_n_reshaped[:, 0, :, :]
h_n_backward = h_n_reshaped[:, 1, :, :]
print("Forward h_n Shape: ", h_n_forward.shape)
print("Backward h_n Shape: ", h_n_backward.shape)
Shape of the hidden after directions are separated:  (1, 2, 4, 3)
Forward h_n Shape:  (1, 4, 3)
Backward h_n Shape:  (1, 4, 3)

堆叠双向RNN

设置bidirectional=True,并将层数设成3层。

rnn = nn.RNN(input_size=INPUT_SIZE, hidden_size=HIDDEN_SIZE, num_layers=3, batch_first=True, bidirectional=True)

output, h_n = rnn(inputs)

print(f'Input Shape: {inputs.shape} ')
print(f'Output Shape: {output.shape} ')
print(f'Hidden Shape: {h_n.shape} ')
Input Shape: (4, 5, 2) 
Output Shape: (4, 5, 6) 
Hidden Shape: (6, 4, 3) 
  • output来自最后一层所有时间步的输出,时间步长度为5,包含整个批次内4条数据,每条数据的输出维度为3,由于是双向,包含了两个方向上的结果,在此维度上进行堆叠,所以由3变成了6。

  • h_n来自所有三层最后一个时间步的隐藏状态,包含整个批次内4条数据,每条数据的输出维度为3。第一个维度由变成了6,因为三层输出在此维度上堆叠了双向的结果。

如果我们也对它们按方向进行拆分的话。

首先对output拆分:

# batch_first=True
output_reshaped = output.reshape((BATCH_SIZE, SEQ_LENGTH, 2, HIDDEN_SIZE))
print("Shape of the output after directions are separated: ", output_reshaped.shape)

# 分别获取正向和反向的输出
output_forward = output_reshaped[:, :, 0, :]
output_backward = output_reshaped[:, :, 1, :]
print("Forward output Shape: ", output_forward.shape)
print("Backward output Shape: ", output_backward.shape)
Shape of the output after directions are separated:  (4, 5, 2, 3)
Forward output Shape:  (4, 5, 3)
Backward output Shape:  (4, 5, 3)

其次对h_out拆分:

# 3: 层数   2: 方向数
h_n_reshaped = h_n.reshape((3, 2, BATCH_SIZE, HIDDEN_SIZE))
print("Shape of the hidden after directions are separated: ", h_n_reshaped.shape)

h_n_forward = h_n_reshaped[:, 0, :, :]
h_n_backward = h_n_reshaped[:, 1, :, :]
print("Forward h_n Shape: ", h_n_forward.shape)
print("Backward h_n Shape: ", h_n_backward.shape)
Shape of the hidden after directions are separated:  (3, 2, 4, 3)
Forward h_n Shape:  (3, 4, 3)
Backward h_n Shape:  (3, 4, 3)

重构双向RNN的实现

我们按照对每层输出状态进行拼接的方式来重构多层双向RNN。

这里有一个问题是,由于我们对隐藏状态进行了拼接, 其维度变成了(n_steps, batch_size, num_directions * hidden_size)。

受到了PyTorch官网启发:

  • ~RNN.weight_ih_l[k] – the learnable input-hidden weights of the k-th layer, of shape (hidden_size, input_size) for k = 0. Otherwise, the shape is (hidden_size, num_directions * hidden_size)

  • ~RNN.weight_hh_l[k] – the learnable hidden-hidden weights of the k-th layer, of shape (hidden_size, hidden_size)

所以,我们相应地改变输入到隐藏状态的维度:(hidden_size, num_directions * hidden_size)。

我们说 h_n的输出维度不受batch_first的影响,其批次维度表现和batch_first=False一样。这是因为在实现时,为了统一,将input的时间步放到了第1个维度,将批大小放到中间,input就像batch_first=False一样,而隐藏状态的方式和它保持一致即可。

if self.batch_first:
    batch_size, n_steps, _ = input.shape
    input = input.transpose((1, 0, 2))  # 将batch放到中间维度

下面看具体实现:

RNNCellBase

class RNNCellBase(Module):
    def reset_parameters(self) -> None:
        stdv = 1.0 / math.sqrt(self.hidden_size) if self.hidden_size > 0 else 0
        for weight in self.parameters():
            init.uniform_(weight, -stdv, stdv)

    def __init__(self, input_size, hidden_size: int, num_chunks: int, bias: bool = True, num_directions=1,
                 reset_parameters=True, device=None, dtype=None) -> None:
        '''
        RNN单时间步的抽象
        :param input_size: 输入x的特征数
        :param hidden_size: 隐藏状态的特征数
        :param bias: 线性层是否包含偏置
        :param nonlinearity: 非线性激活函数 tanh | relu (mode = RNN)
        '''
        factory_kwargs = {'device': device, 'dtype': dtype}

        super(RNNCellBase, self).__init__()

        self.input_size = input_size
        self.hidden_size = hidden_size

        # 输入x的线性变换
        self.input_trans = Linear(num_directions * input_size, num_chunks * hidden_size, bias=bias, **factory_kwargs)
        # 隐藏状态的线性变换
        self.hidden_trans = Linear(hidden_size, num_chunks * hidden_size, bias=bias, **factory_kwargs)
        if reset_parameters:
            self.reset_parameters()

    def extra_repr(self) -> str:
        s = 'input_size={input_size}, hidden_size={hidden_size}'
        if 'bias' in self.__dict__ and self.bias is not True:
            s += ', bias={bias}'
        if 'nonlinearity' in self.__dict__ and self.nonlinearity != "tanh":
            s += ', nonlinearity={nonlinearity}'
        return s.format(**self.__dict__)

RNNCell

class RNNCell(RNNCellBase):
    def __init__(self, input_size, hidden_size: int, bias: bool = True, nonlinearity: str = 'tanh', num_directions=1,
                 reset_parameters=True, device=None, dtype=None):
        factory_kwargs = {'device': device, 'dtype': dtype, 'reset_parameters': reset_parameters}
        super(RNNCell, self).__init__(input_size, hidden_size, num_chunks=1, bias=bias, num_directions=num_directions,
                                      **factory_kwargs)

        if nonlinearity == 'tanh':
            self.activation = F.tanh
        else:
            self.activation = F.relu

    def forward(self, x: Tensor, h: Tensor, c: Tensor = None) -> Tuple[Tensor, None]:
        h_next = self.activation(self.input_trans(x) + self.hidden_trans(h))
        return h_next, None

在RNNCell的forward中也返回了一个元组,元组中第二个元素代表了c_next,为了兼容LSTM的实现。

RNNBase

class RNNBase(Module):
    def __init__(self, cell: RNNCellBase, input_size: int, hidden_size: int, batch_first: bool = False,
                 num_layers: int = 1, bidirectional: bool = False, bias: bool = True, dropout: float = 0,
                 reset_parameters=True, device=None, dtype=None) -> None:
        '''
           :param input_size:  输入x的特征数
           :param hidden_size: 隐藏状态的特征数
           :param batch_first: 批次维度是否在前面
           :param num_layers: 层数
           :param bidirectional: 是否为双向
           :param bias: 线性层是否包含偏置
           :param dropout: 用于多层堆叠RNN,默认为0代表不使用dropout
           :param reset_parameters: 是否执行reset_parameters
           :param device:
           :param dtype:
       '''
        super(RNNBase, self).__init__()

        factory_kwargs = {'device': device, 'dtype': dtype, 'reset_parameters': reset_parameters}

        self.num_layers = num_layers
        self.hidden_size = hidden_size
        self.input_size = input_size
        self.batch_first = batch_first
        self.bidirectional = bidirectional
        self.bias = bias

        self.num_directions = 2 if self.bidirectional else 1

        # 支持多层
        self.cells = ModuleList([cell(input_size, hidden_size, bias, **factory_kwargs)] +
                                [cell(hidden_size, hidden_size, bias, num_directions=self.num_directions,
                                      **factory_kwargs) for _ in
                                 range(num_layers - 1)])
        if self.bidirectional:
            # 支持双向
            self.back_cells = copy.deepcopy(self.cells)

        self.dropout = dropout
        if dropout != 0:
            # Dropout层
            self.dropout_layer = Dropout(dropout)

    def _one_directional_op(self, input, n_steps, cell, h, c) -> Tuple[Tensor, Tensor, Tensor]:
        hs = []
        # 沿着input时间步进行遍历
        for t in range(n_steps):
            inp = input[t]

            h, c = cell(inp, h, c)
            hs.append(h)

        return h, c, F.stack(hs)

    def _handle_hidden_state(self, input, state):
        assert input.ndim == 3  # 必须传入批数据,最小批大小为1

        if self.batch_first:
            batch_size, n_steps, _ = input.shape
            input = input.transpose((1, 0, 2))  # 将batch放到中间维度
        else:
            n_steps, batch_size, _ = input.shape

        if state is None:
            h = Tensor.zeros((self.num_layers * self.num_directions, batch_size, self.hidden_size), dtype=input.dtype,
                             device=input.device)
        else:
            h = state

        # 得到每层的状态
        hs = list(F.unbind(h))  # 按层数拆分h

        return hs, [None] * len(hs), input, n_steps, batch_size

    def forward(self, input: Tensor, state: Tensor) -> Tuple[Tensor, Tensor, Tensor]:
        '''
        RNN的前向传播
        :param input: 形状 [n_steps, batch_size, input_size] 若batch_first=False
        :param state: (隐藏状态,单元状态)元组, 每个元素形状 [num_layers, batch_size, hidden_size]
        :return:
            num_directions = 2 if self.bidirectional else 1

            output: (n_steps, batch_size, num_directions * hidden_size)若batch_first=False 或
                    (batch_size, n_steps, num_directions * hidden_size)若batch_first=True
                    包含每个时间步最后一层(多层RNN)的输出h_t
            h_n: (num_directions * num_layers, batch_size, hidden_size) 包含最终隐藏状态
            c_n: (num_directions * num_layers, batch_size, hidden_size) 包含最终单元状态(LSTM);非LSTM为None

        '''

        hs, cs, input, n_steps, batch_size = self._handle_hidden_state(input, state)

        # 正向得到的h_n,反向得到的h_n,正向得到的c_n,反向得到的c_n
        h_n_f, h_n_b, c_n_f, c_n_b = [], [], [], []

        for layer in range(self.num_layers):
            h, c, hs_f = self._one_directional_op(input, n_steps, self.cells[layer], hs[layer], cs[layer])

            h_n_f.append(h)  # 保存最后一个时间步的隐藏状态
            c_n_f.append(c)
            if self.bidirectional:
                h, c, hs_b = self._one_directional_op(F.flip(input, 0), n_steps, self.back_cells[layer],
                                                      hs[layer + self.num_layers], cs[layer + self.num_layers])
                hs_b = F.flip(hs_b, 0)  # 将输出时间步维度逆序,使得时间步t=0上,是看了整个序列的结果。
                # 拼接两个方向上的输入

                h_n_b.append(h)
                c_n_b.append(c)
                input = F.cat([hs_f, hs_b], 2)  # (n_steps, batch_size, num_directions * hidden_size)
            else:
                input = hs_f  # (n_steps, batch_size, num_directions * hidden_size)

            # 在第1层之后,最后一层之前需要经过dropout
            if self.dropout and layer != self.num_layers - 1:
                input = self.dropout_layer(input)

        output = input  # (n_steps, batch_size, num_directions * hidden_size) 最后一层最后计算的输入,就是它的输出
        c_n = None
        if self.bidirectional:
            h_n = F.cat([F.stack(h_n_f), F.stack(h_n_b)], 0)
            if c is not None:
                c_n = F.cat([F.stack(c_n_f), F.stack(c_n_b)], 0)
        else:
            h_n = F.stack(h_n_f)
            if c is not None:
                c_n = F.stack(c_n_f)

        if self.batch_first:
            output = output.transpose((1, 0, 2))

        return output, h_n, c_n

    def extra_repr(self) -> str:
        s = 'input_size={input_size}, hidden_size={hidden_size}'
        if self.num_layers != 1:
            s += ', num_layers={num_layers}'
        if self.bias is not True:
            s += ', bias={bias}'
        if self.batch_first is not False:
            s += ', batch_first={batch_first}'
        if self.dropout:
            s += ', dropout={dropout}'
        if self.bidirectional is not False:
            s += ', bidirectional={bidirectional}'
        return s.format(**self.__dict__)

同样,做了兼容LSTM的实现,会多了一些if判断。

RNN

class RNN(RNNBase):
    def __init__(self, *args, **kwargs) -> None:
        '''
        :param input_size:  输入x的特征数
        :param hidden_size: 隐藏状态的特征数
        :param batch_first:
        :param num_layers: 层数
        :param bidirectional: 是否为双向
        :param bias: 线性层是否包含偏置
        :param dropout: 用于多层堆叠RNN,默认为0代表不使用dropout
        :param nonlinearity: 非线性激活函数 tanh | relu
        '''
        super(RNN, self).__init__(RNNCell, *args, **kwargs)

    def forward(self, input: Tensor, state: Tensor = None) -> Tuple[Tensor, Tensor]:
        output, h_n, _ = super().forward(input, state)
        return output, h_n

因为基类RNNBase的forward会返回output,h_n,c_n,所以RNN这里重写了forward方法,仅返回output和h_n。

通过这种方式实现GRU和RNN非常类似。

GRU

class GRU(RNNBase):
    def __init__(self, *args, **kwargs):
        '''
        :param input_size:  输入x的特征数
        :param hidden_size: 隐藏状态的特征数
        :param batch_first:
        :param num_layers: 层数
        :param bidirectional: 是否为双向
        :param bias: 线性层是否包含偏置
        :param dropout: 用于多层堆叠RNN,默认为0代表不使用dropout
        '''
        super(GRU, self).__init__(GRUCell, *args, **kwargs)

    def forward(self, input: Tensor, state: Tensor = None) -> Tuple[Tensor, Tensor]:
        output, h_n, _ = super().forward(input, state)
        return output, h_n

实例测试

同样的配置下:

embedding_dim = 128
hidden_dim = 128
batch_size = 32
num_epoch = 10
n_layers = 2
dropout = 0.2

model = RNN(len(vocab), embedding_dim, hidden_dim, num_class, n_layers, dropout, bidirectional=True, mode=mode)

两层双向RNN可以得到75%的准确率。

Training Epoch 0: 94it [01:16,  1.23it/s]
Loss: 220.78
Training Epoch 1: 94it [01:16,  1.24it/s]
Loss: 151.85
Training Epoch 2: 94it [01:14,  1.26it/s]
Loss: 125.62
Training Epoch 3: 94it [01:15,  1.25it/s]
Loss: 110.55
Training Epoch 4: 94it [01:14,  1.27it/s]
Loss: 100.75
Training Epoch 5: 94it [01:13,  1.28it/s]
Loss: 94.12
Training Epoch 6: 94it [01:12,  1.29it/s]
Loss: 88.64
Training Epoch 7: 94it [01:12,  1.29it/s]
Loss: 84.51
Training Epoch 8: 94it [01:13,  1.28it/s]
Loss: 80.83
Training Epoch 9: 94it [01:13,  1.27it/s]
Loss: 78.12
Testing: 29it [00:06,  4.79it/s]
Acc: 0.75
Cost:749.8793613910675

完整代码

https://github.com/nlp-greyfoss/metagrad

References

Bidirectional recurrent neural networkshttps://www.researchgate.net/publication/3316656_Bidirectional_recurrent_neural_networks

Pytorch [Basics] — Intro to
RNNhttps://towardsdatascience.com/pytorch-basics-how-to-train-your-neural-net-intro-to-rnn-cb6ebc594677

关注下方《学姐带你玩AI》🚀🚀🚀

220+篇AI必读论文免费领取

码字不易,欢迎大家点赞评论收藏!

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

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

相关文章

【Vue3】首页主体-面板组件封装

首页主体-面板组件封装 新鲜好物、人气推荐俩个模块的布局结构上非常类似,我们可以抽离出一个通用的面板组件来进行复用 目标:封装一个通用的面板组件 思路分析 图中标出的四个部分都是可能会发生变化的,需要我们定义为可配置主标题和副标题…

您可以使用 21 个很棒的搜索引擎来代替 Google

在过去的 20 年里,Google 一直是大多数人用于日常搜索、产品研究和了解最新消息的搜索引擎。凭借其长期的统治地位和大部分市场份额,很难说任何搜索引擎都能提供比谷歌更好的结果。由于这种市场主导地位,谷歌也一直是SEO和营销专业人士关注的…

随笔-老子不想牺牲了

18年来到这个项目组,当时只有8个人,包括经常不在的架构师和经理。当时的工位在西区1栋A座,办公桌很宽敞。随着项目的发展,入职的人越来越多,项目的工位也是几经搬迁。基本上每次搬迁时,我的工位都是挑剩下的…

Allegro如何实现同一个屏幕界面分屏显示操作指导

Allegro如何实现同一个屏幕界面分屏显示操作指导 在做PCB设计的时候,会需要分屏显示,比如一边是放大的视图,另外一边是缩小的视图,Allegro支持同一个屏幕界面下进行分屏显示,如下图 而且会实时同步起来 如何分屏,具体操作如下 点击View

(python)降低图像质量

降低数字图像质量(python实现) 目录 降低数字图像质量(python实现)一、分别采用五种不同的方式来降低图像的质量1. 给图像添加椒盐噪声2. 给图像添加高斯噪声3. 对图像进行高斯模糊4. 对图像进行运动模糊5. 对图像进行插值下采样二、实现代码一、分别采用五种不同的方式来降…

操作系统(六)磁盘调度算法与优化

操作系统(六)磁盘调度算法与优化 一、磁盘调度算法 时间指标 寻找时间:在读/写数据前,将磁头移动到指定磁道需要的时间延迟时间:通过旋转盘面,将磁头定位到目标扇区所需的时间传输时间:从磁盘…

Leetcode.1124 表现良好的最长时间段

题目链接 Leetcode.1124 表现良好的最长时间段 Rating : 1908 题目描述 我们认为当员工一天中的工作小时数大于 8 小时的时候,那么这一天就是「劳累的一天」。 所谓「表现良好的时间段」,意味在这段时间内,「劳累的天数」是严格…

argocd 调研

Argo CD 是基于 Kubernetes 的声明式, GitOps 持续交付工具。GitOps AgentGitOps IaC Git CI/CD,即基于 IaC 的版本化 CI/CD。它的核心是使用 Git 仓库来管理基础设施和应用的配置,并且以 Git 仓库作为基础设施和应用的单一事实来源。Git …

Windows 11 办公实用效率小技巧总结(持续更新)

Win11 办公实用效率小技巧总结:1. 虚拟桌面2. 拖拽任务窗口转移到不同虚拟桌面3. 自定义多网页名称4. Focus专注组件5. 多选项分屏6. 抖一抖最小化1. 虚拟桌面 虽然过去只有 Linux, Mac 系统中有虚拟桌面,而且在win10 中也实现了这个功能,但…

Springboot835学生成绩分析系统--java

开发语言:Java 框架:Springboot 技术:JSP JDK版本:JDK1.8 目 录 1 绪 论 5 1.1课题背景 5 1.2 课题研究的意义 5 1.3 系统实现的功能 5 1.4 课题研究现状 5 2系统相关技术 7 2.1 Java技术 7 2.2 B/S架构 7 2.3 MySQL 介绍 7 2.4My…

zabbix邮件报警配置

在Zabbix服务端设置邮件报警,当被监控主机宕机或者达到触发器预设值时,会自动发送报警邮件到指定邮箱。 邮件服务可以使用系统自带的邮件服务来发送邮件或者使用其他邮件服务调用第三方邮件来发送警告邮件。 一、开启发件服务器SMTP功能 这里发送邮件的…

初始C++(四):内联函数

文章目录一.内联函数概念二.内联函数用法三.内联函数的特性四.内联函数和宏一.内联函数概念 以inline修饰的函数叫做内联函数,编译时C编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。 二.内联函…

SpringBoot环境-MySQL主从复制,读写分离的实现

目录 概述 环境 主从复制 读写分离 概述 记录在MySQL数据库中主从复制以及SpringBoot环境操作MySQL数据库读写分离的实现步骤。 背景 :因为我们在对数据库进行操作时,如果读写操作都由一台数据库承担的话压力会比较大,为了减轻数据库压力&…

【Linux】rsyslog日志服务(配置,测试、日志转储)

一、rsyslog简介 Rsyslog的全称是 rocket-fast system for log ,可用于接受来自各种来源的输入,转换 它们,并将结果输出到不同的目的地。 它提供了高性能、强大的安全功能和模块化设计。虽然rsyslog最初是一个常规的系 统日志,但它已经发展成…

H5APP开发封装流程

1.利用H5写想要的功能 2.打包APP 3.手机安装 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0, maximum-scale1.0, user-sca…

【基础】Flink -- Time and Window

Flink -- Time and WindowFlink 时间语义水位线 Watermark水位线的概念有序流中的水位线乱序流中的水位线水位线的特性水位线的基本使用水位线生成策略内置水位线生成器自定义水位线策略在自定义数据源中发送水位线窗口 Window窗口的基本概述窗口的基本概念窗口的分类窗口的 AP…

ccc-Backpropagation-李宏毅(7)

文章目录NotationBackpropagationForward passBackward passSummaryNotation 神经网络求解最优化Loss function时参数非常多&#xff0c;反向传播使用链式求导的方式提升计算梯度向量时的效率&#xff0c;链式法则如下&#xff1a; Backpropagation 损失函数计算为所有样本…

Pulsar

一、简介Apache Pulsar是Apache软件基金会顶级项目&#xff0c;是下一代云原生分布式消息流平台&#xff0c;集消息、存储、轻量化函数式计算为一体&#xff0c;采用计算与存储分离架构设计&#xff0c;支持多租户、持久化存储、多机房跨区域数据复制&#xff0c;具有强一致性、…

常见的函数式编程操作

1、柯里化&#xff1a; (概念 & 应用 & 好处) 柯里化的概念&#xff1a; 柯里化&#xff08;Currying&#xff09;是把接受多个参数的函数变成接受单一参数的函数&#xff0c;并且返回一个用于接受剩余参数的新函数&#xff0c;当参 数都传递完成后&#xff0c; 则立…

Linux 文件权限讲解

目录 文件的一般权限 一般权限有哪些 使用ls -l查看文件/目录权限 配置一般权限和文件所属信息 chmod 修改文件权限 chown 修改文件所属信息&#xff08;所有者和所属组&#xff09; 文件特殊权限 SUID 针对所有者的特殊权限 SGID SBID 配置特殊权限 文件的隐藏权限…