Comprensión detallada (notas de estudio) | DETR (integrando el marco de detección de objetivos de Transformer) Interpretación de entradas DETR e implementación práctica de Transformer

I. Resumen

DETR , nombre completo DEtection TRansformer, es una red de detección de objetivos de extremo a extremo basada en transformador propuesta por Facebook, publicada en ECCV2020.

Texto original:
enlace
Código fuente:
enlace

El modelo de red de detección de objetivos de extremo a extremo DETR es el primer modelo de marco de detección de objetivos que integra con éxito a Transformer como el bloque de construcción central de la canalización de detección. Basado en la detección de objetivos de extremo a extremo de Transformers, no hay un paso de posprocesamiento de NMS, la implementación real no usa ancla y supera a este último en comparación con Faster RCNN.

En el conjunto de datos COCO, el diagrama de efecto de comparación es el siguiente:

La imagen proviene del texto original.
Como se puede ver en la figura anterior, el efecto de DETR es muy bueno. DETR basado en ResNet50 ha logrado efectos comparables a Faster-RCNN después de varios ajustes. Al mismo tiempo, DETR tiene el mejor rendimiento en la detección de objetivos grandes, pero es ligeramente peor en objetivos pequeños, y la pérdida basada en coincidencias dificulta la convergencia del aprendizaje (es decir, es difícil aprender la situación óptima). La aparición de Deformable DETR ha mejorado relativamente bien estos dos problemas.

El bloque de construcción principal del nuevo marco de Detección de Transformador o DETR es una función de pérdida global basada en conjuntos que impone predicciones únicas a través de una combinación bipartita y una arquitectura de codificador-descodificador de transformador. Dado un pequeño conjunto fijo de consultas de objetos aprendidos, DETR considera la relación entre el objeto de destino y el contexto de la imagen global , y genera directamente el conjunto final de predicciones en paralelo.

A diferencia de muchos otros detectores modernos, el nuevo modelo es conceptualmente simple y no requiere bibliotecas especializadas. DETR logra una precisión y un rendimiento de tiempo de ejecución comparables a la base de referencia Faster R-CNN bien establecida y altamente optimizada en el desafiante conjunto de datos de detección de objetos COCO. Además, DETR se puede transferir fácilmente a otras tareas, como la segmentación panóptica.

El transformador de detección puede predecir el movimiento violento de todos los objetos y se entrena de extremo a extremo mediante el establecimiento de una función de pérdida que realiza una comparación binaria entre los objetos predichos y los objetos reales. DETR simplifica la canalización de detección al eliminar múltiples pasos de posprocesamiento diseñados a mano, como nms, que codifican componentes de conocimiento previo . A diferencia de la mayoría de los métodos de detección existentes, DETR no requiere capas personalizadas y, por lo tanto, se puede replicar fácilmente en cualquier marco que incluya clases de transformadores y CNN estándar.

2. Transformador

Transformer, el texto original , ha sido muy utilizado desde que fue propuesto, su núcleo es el uso superpuesto del mecanismo de atención, lo que hace que el modelo de IA se enfoque selectivamente en ciertas partes de la entrada, por lo que el razonamiento es más eficiente. No solo ha logrado resultados notables en el campo de la PNL, sino que ahora está siendo malversado en el campo del CV. Todavía es esencialmente una estructura de Codificador-Decodificador. Tanto el codificador como el decodificador son una superposición múltiple de módulos de Autoatención. A través de la forma de codificación-decodificación, es posible aprender y adquirir características importantes con múltiples cabezas de atención, y luego combine los dos para lograr Integrar la "información de orden de palabras de contexto" de la imagen para realizar una mejor detección de objetivos. La estructura del módulo se muestra en la figura a continuación:

inserte la descripción de la imagen aquí
inserte la descripción de la imagen aquí
Módulo codificador:
inserte la descripción de la imagen aquí

En comparación con los modelos de secuencia tradicionales como RNN, Transformer mejora principalmente en:

  1. Convertir RNN en una superposición de múltiples estructuras de autoatención
  2. Calcule la correlación de cualquier elemento en la secuencia en relación con todos los demás elementos en paralelo, extraiga de manera eficiente la correlación en el contexto e introduzca el mecanismo de atención de múltiples cabezas para extraer características desde múltiples perspectivas .
  3. El código de posición se utiliza para describir la información anterior y posterior de la secuencia , que reemplaza el proceso de cálculo de serie RNN.

Implementación de pytorch de transformador

Use la interfaz pytorch para mostrar el uso real de Transformer de la siguiente manera en este artículo
Combate de Transformer

El encapsulado de Transformer por pytorch se realiza a través de torch.nnTransformer, que incluye principalmente los siguientes parámetros:

torch.nn.Transformer(d_model: int = 512,nhead: int = 8,num_encoder_layers: int = 6,num_decoder_layers: int = 6,dim_feedforward: int = 2048,dropout: float = 0.1,activation: str = 'relu',custom_encoder: Optional[Any] = None,custom_decoder: Optional[Any] = None)

Entre ellos,
d_model es el número de canales de incrustación de palabras,
n_head es el número de cabezales de atención multicabezal,
num_encoder_layers y num_decoder_layers corresponden al número de módulos de autoatención del codificador y decodificador, respectivamente,
dim_feedforward corresponde al Lineal en el codificador-decodificador La dimensión de la capa.

La función de reenvío de nn.Transformer implementa el proceso de codificación y decodificación:

forward(src: torch.Tensor,tgt: torch.Tensor,src_mask: Optional[torch.Tensor] = None,tgt_mask: Optional[torch.Tensor] = None,memory_mask: Optional[torch.Tensor] = None,src_key_padding_mask: Optional[torch.Tensor] = None,tgt_key_padding_mask: Optional[torch.Tensor] = None,memory_key_padding_mask: Optional[torch.Tensor] = None)→ torch.Tensor

Entre ellos, los dos parámetros que se deben ingresar son src y tgt, que corresponden a las entradas de entrada del codificador y las entradas de salida del decodificador respectivamente. El papel de tgt es similar a una restricción condicional. La entrada tgt de la primera capa de Decoder es un vector de incrustación de palabras, y el resultado del cálculo de la capa anterior es de la segunda capa.

Entre otros parámetros opcionales, [src/tgt/memory]_mask es una matriz de máscaras, que define la estrategia para calcular la Atención, correspondiente a la Sección 3.1 del texto original. Una explicación popular es: en una secuencia de palabras, cada palabra solo puede verse afectada por las palabras anteriores, por lo que todas las posiciones detrás de la palabra deben ignorarse , por lo que al calcular Atención, el vector de palabras y las palabras detrás de él. La correlación de la vectores es 0. ( Sin embargo, de hecho, cada palabra, especialmente en chino, debe estar relacionada con la semántica contextual y el orden de las palabras, para aprender mejor el significado específico de la palabra) .

[src, tgt, memory]_key_padding_mask también es una matriz de máscaras, que define qué posiciones en src, tgt y memory deben reservarse y cuáles deben ignorarse.

3. DETR

La idea de DETR es similar a la idea esencial de la detección de objetivos tradicional, pero la forma de expresión es muy diferente. Los métodos tradicionales, como el método basado en anclajes, clasifican esencialmente las categorías de anclajes densos predefinidos y hacen una regresión de los coeficientes del marco. DETR considera la detección de objetivos como un problema de predicción de conjuntos (los conjuntos y las anclas tienen funciones similares). Dado que Transformer es esencialmente una función de conversión de secuencias, DETR puede considerarse como un proceso de conversión de una secuencia de imágenes a una secuencia de colección. Esta colección es en realidad una codificación posicional que se puede aprender (también denominada consulta de objetos o codificación posicional de salida en el artículo y query_embed en el código ).

Diagrama de estructura de red de DETR (flujo de algoritmo):
inserte la descripción de la imagen aquí
Estructura de transformador utilizada por DETR:
inserte la descripción de la imagen aquí
la codificación posicional espacial es un método de codificación posicional espacial bidimensional propuesto por el autor. La codificación posicional se agrega a la atención propia del codificador y la atención cruzada del decodificador respectivamente Al mismo tiempo, el objeto Las consultas también se agregan a las dos atenciones del decodificador . El Transformer original agregó codificación posicional a la incrustación de entrada y salida. Vale la pena mencionar que el autor señaló en el experimento de ablación que incluso sin agregar ningún código de posición al codificador, el AP final es solo 1.3 puntos más bajo que el DETR completo.

El código reescribe las clases TransformerEncoderLayer y TransformerDecoderLayer basadas en PyTorch. La única interfaz de PyTorch utilizada es la clase nn.MultiheadAttention. El código fuente requiere PyTorch 1.5.0 o superior.

El núcleo del código se encuentra en models/transformer.py y models/detr.py .

Transformador.py

class Transformer(nn.Module):

    def __init__(self, d_model=512, nhead=8, num_encoder_layers=6,
                 num_decoder_layers=6, dim_feedforward=2048, dropout=0.1,
                 activation="relu", normalize_before=False,
                 return_intermediate_dec=False):
        super().__init__()
        encoder_layer = TransformerEncoderLayer(d_model, nhead, dim_feedforward,
                                                dropout, activation, normalize_before)
        encoder_norm = nn.LayerNorm(d_model) if normalize_before else None
        self.encoder = TransformerEncoder(encoder_layer, num_encoder_layers, encoder_norm)
        decoder_layer = TransformerDecoderLayer(d_model, nhead, dim_feedforward,
                                                dropout, activation, normalize_before)
        decoder_norm = nn.LayerNorm(d_model)
        self.decoder = TransformerDecoder(decoder_layer, num_decoder_layers, decoder_norm,
                                          return_intermediate=return_intermediate_dec)

    def forward(self, src, mask, query_embed, pos_embed):
        # flatten NxCxHxW to HWxNxC
        bs, c, h, w = src.shape
        src = src.flatten(2).permute(2, 0, 1)
        pos_embed = pos_embed.flatten(2).permute(2, 0, 1)
        query_embed = query_embed.unsqueeze(1).repeat(1, bs, 1)
        mask = mask.flatten(1)

        tgt = torch.zeros_like(query_embed)
        memory = self.encoder(src, src_key_padding_mask=mask, pos=pos_embed)
        hs = self.decoder(tgt, memory, memory_key_padding_mask=mask,
                          pos=pos_embed, query_pos=query_embed)
        return hs.transpose(1, 2), memory.permute(1, 2, 0).view(bs, c, h, w)

La clase Transformer contiene un objeto Encoder y Decoder. La implementación de clases relacionadas se puede encontrar en transformer.py. Concéntrese en la función directa, hay una operación de transformación en el tensor de entrada : # flatten NxCxHxW a HWxNxC.

Combinado con las definiciones de forma de src y tgt en PyTorch, se puede encontrar que la idea de **DETR es expandir los píxeles del mapa de características de salida de la red troncal en unidimensional y tomarlo como la longitud de la secuencia, mientras que el las definiciones de lote y canal permanecen sin cambios. **Entonces DETR puede calcular la correlación de cada píxel del mapa de características en relación con todos los demás píxeles,Esto se logra en CNN confiando en el campo receptivo.Se puede ver que Transformer puede capturar un rango receptivo más grande que CNN.

DETR no utiliza la atención enmascarada al calcular la atención , porque después de que el mapa de funciones se expande en una dimensión, todos los píxeles pueden estar relacionados entre sí , por lo que no es necesario especificar la máscara. Y src_key_padding_mask se usa para eliminar la parte de zero_pad.

Hay dos variables clave pos_embed y query_embed en la función de reenvío . donde pos_embed es la codificación de posición, ubicada en models/position_encoding.py .

position_encoding.py

De acuerdo con las características del mapa de características bidimensional, DETR implementa su propio método de codificación de posición bidimensional . El código de implementación es el siguiente:

class PositionEmbeddingSine(nn.Module):
    """
    This is a more standard version of the position embedding, very similar to the one
    used by the Attention is all you need paper, generalized to work on images.
    """
    def __init__(self, num_pos_feats=64, temperature=10000, normalize=False, scale=None):
        super().__init__()
        self.num_pos_feats = num_pos_feats
        self.temperature = temperature
        self.normalize = normalize
        if scale is not None and normalize is False:
            raise ValueError("normalize should be True if scale is passed")
        if scale is None:
            scale = 2 * math.pi
        self.scale = scale

    def forward(self, tensor_list: NestedTensor):
        x = tensor_list.tensors
        mask = tensor_list.mask
        assert mask is not None
        not_mask = ~mask
        y_embed = not_mask.cumsum(1, dtype=torch.float32)
        x_embed = not_mask.cumsum(2, dtype=torch.float32)
        if self.normalize:
            eps = 1e-6
            y_embed = y_embed / (y_embed[:, -1:, :] + eps) * self.scale
            x_embed = x_embed / (x_embed[:, :, -1:] + eps) * self.scale

        dim_t = torch.arange(self.num_pos_feats, dtype=torch.float32, device=x.device)
        dim_t = self.temperature ** (2 * (dim_t // 2) / self.num_pos_feats)

        pos_x = x_embed[:, :, :, None] / dim_t
        pos_y = y_embed[:, :, :, None] / dim_t
        pos_x = torch.stack((pos_x[:, :, :, 0::2].sin(), pos_x[:, :, :, 1::2].cos()), dim=4).flatten(3)
        pos_y = torch.stack((pos_y[:, :, :, 0::2].sin(), pos_y[:, :, :, 1::2].cos()), dim=4).flatten(3)
        pos = torch.cat((pos_y, pos_x), dim=3).permute(0, 3, 1, 2)
        return pos

En el interior, la máscara es una matriz de máscaras de posición. Para una imagen que no ha pasado por zero_pad, su máscara es una matriz de todos los 0.

Al comparar el código, se puede ver que DETR calcula un código de posición para las direcciones x e y del mapa de características bidimensional, y la longitud del código de posición de cada dimensión es num_pos_feats (este valor es en realidad la mitad de hidden_dim), para x o y, calcule el seno de la posición impar, calcule el coseno de la posición par y luego concatene pos_x y pos_y para obtener una matriz NHWD , y luego pase permute (0,3,1,2), la forma se convierte en NDHW , donde D es igual a hidden_dim . Este hidden_dim es la dimensión del vector de entrada del Transformador En términos de implementación, debe ser igual a la dimensión de la salida del mapa de características por la red troncal CNN. Entonces, la forma del código pos y la función de salida de CNN es exactamente la misma.

src = src.flatten(2).permute(2, 0, 1)         
pos_embed = pos_embed.flatten(2).permute(2, 0, 1)

Realice operaciones de aplanamiento y permutación en las funciones y la salida de código pos de CNN para cambiar la forma a SNE , que se ajusta a la definición de forma de entrada de PyTorch. En TransformerEncoder, se agregan src y pos_embed. Puede ver el código usted mismo.

detr.py

clase DETR

Esta clase encapsula todo el proceso de cálculo de DETR . Primero, veamos cuáles son las consultas de objetos mencionadas repetidamente en el artículo .

La respuesta es query_embed .

En el código, query_embed es en realidad una matriz incrustada:

self.query_embed = nn.Embedding(num_queries, hidden_dim)

Entre ellos, num_queries es el número de consultas objetivo predefinidas, que es 100 por defecto en el código. Su significado es: **Según las características de la codificación de Encoder, Decoder convierte 100 consultas en 100 objetivos. **Por lo general, 100 consultas son suficientes. Pocas imágenes pueden contener más de 100 objetivos (a menos que sean tareas súper intensivas). Por el contrario, la cantidad de anclas que deben predecir los métodos basados ​​en CNN es de decenas de miles, y el costo de cálculo es realmente muy grande.

La función de reenvío de Transformer define un destino de matriz con la misma forma que query_embed y todos los 0 , y luego agrega query_embed y target en el reenvío de TransformerDecoderLayer ( aquí , el rol de query_embed es similar al de la codificación de posición), como en la atención propia consulta y clave ; como consulta en atención multicabezal :

class TransformerDecoderLayer(nn.Module):
    def forward_post(self, tgt, memory,
                     tgt_mask: Optional[Tensor] = None,
                     memory_mask: Optional[Tensor] = None,
                     tgt_key_padding_mask: Optional[Tensor] = None,
                     memory_key_padding_mask: Optional[Tensor] = None,
                     pos: Optional[Tensor] = None,
                     query_pos: Optional[Tensor] = None):
        q = k = self.with_pos_embed(tgt, query_pos)
        tgt2 = self.self_attn(q, k, value=tgt, attn_mask=tgt_mask,
                              key_padding_mask=tgt_key_padding_mask)[0]
        tgt = tgt + self.dropout1(tgt2)
        tgt = self.norm1(tgt)
        tgt2 = self.multihead_attn(query=self.with_pos_embed(tgt, query_pos),
                                   key=self.with_pos_embed(memory, pos),
                                   value=memory, attn_mask=memory_mask,
                                   key_padding_mask=memory_key_padding_mask)[0]
        tgt = tgt + self.dropout2(tgt2)
        tgt = self.norm2(tgt)
        tgt2 = self.linear2(self.dropout(self.activation(self.linear1(tgt))))
        tgt = tgt + self.dropout3(tgt2)
        tgt = self.norm3(tgt)
        return tgt

Después de que el decodificador calcule las consultas de objetos , se generará una matriz de forma TNE , donde T es la longitud de secuencia de las consultas de objetos , es decir, 100, N es el tamaño del lote y E es el canal de funciones.

Finalmente, la predicción de clase se genera a través de una capa lineal y la predicción de caja se genera a través de una estructura de perceptrón multicapa :

self.class_embed = nn.Linear(hidden_dim, num_classes + 1)
self.bbox_embed = MLP(hidden_dim, hidden_dim, 4, 3)

#forward
hs = self.transformer(self.input_proj(src), mask, self.query_embed.weight, pos[-1])[0]
outputs_class = self.class_embed(hs)
outputs_coord = self.bbox_embed(hs).sigmoid()
out = {
    
    'pred_logits': outputs_class[-1], 'pred_boxes': outputs_coord[-1]}

El canal de salida de la clasificación es num_classes+1, la categoría comienza desde 0 y la categoría de fondo es num_classes.

conjunto de criterios de clase

Esta clase es responsable del cálculo de la pérdida.

El método basado en CNN calculará el resultado de la predicción de cada ancla , y luego usará el cálculo del Iou entre el resultado de la predicción y el cuadro de verdad del terreno , y seleccionará aquellas anclas cuyo Iou sea mayor que cierto umbral como muestras positivas para devolver su clase y deltas de caja . De manera similar, DETR también calculará la predicción de cada consulta de objeto , pero DETR calculará directamente los valores normalizados de las cuatro esquinas del cuadro, en lugar de basarse en los deltas del cuadro:

Luego realice una coincidencia binaria entre estas predicciones de objetos y el cuadro de verdad del terreno . DETR utiliza el algoritmo húngaro para completar este proceso de coincidencia.

Diagrama de flujo completo:
inserte la descripción de la imagen aquí
si hay N objetivos, entonces N de 100 predicciones de objetos podrán coincidir con la verdad del terreno N , y los otros se combinarán con éxito con "ningún objeto", y se asignará la etiqueta de categoría de estas predicciones . Es num_classes , lo que significa que la predicción es el fondo.

Este diseño es muy bueno, es un punto culminante en DETR y también es una de sus características, por lo que, en teoría, cada consulta de objeto tiene un objetivo de coincidencia único y no habrá superposición, por lo que DETR no necesita nms para Postprocesamiento.

Calcula la pérdida de la función de pérdida de acuerdo con el resultado de la coincidencia. La fórmula de cálculo de la función de pérdida ya no se incluye aquí.

class SetCriterion(nn.Module):
    def forward(self, outputs, targets):
        """ This performs the loss computation.
        Parameters:
             outputs: dict of tensors, see the output specification of the model for the format
             targets: list of dicts, such that len(targets) == batch_size.
                      The expected keys in each dict depends on the losses applied, see each loss' doc
        """
        outputs_without_aux = {
    
    k: v for k, v in outputs.items() if k != 'aux_outputs'}

        # Retrieve the matching between the outputs of the last layer and the targets
        indices = self.matcher(outputs_without_aux, targets)

        # Compute the average number of target boxes accross all nodes, for normalization purposes
        num_boxes = sum(len(t["labels"]) for t in targets)
        num_boxes = torch.as_tensor([num_boxes], dtype=torch.float, device=next(iter(outputs.values())).device)
        if is_dist_avail_and_initialized():
            torch.distributed.all_reduce(num_boxes)
        num_boxes = torch.clamp(num_boxes / get_world_size(), min=1).item()

        # Compute all the requested losses
        losses = {
    
    }
        for loss in self.losses:
            losses.update(self.get_loss(loss, outputs, targets, indices, num_boxes))

Haga coincidir outputs_without_aux y objetivos a través de self.matcher . El algoritmo húngaro devolverá una tupla de índices , que contiene el índice de src y target . Para conocer el proceso de coincidencia específico, consulte models/matcher.py.

pérdida de clasificación

La pérdida de clasificación utiliza la pérdida de entropía cruzada para todas las predicciones

def loss_labels(self, outputs, targets, indices, num_boxes, log=True):
    src_logits = outputs['pred_logits']

    idx = self._get_src_permutation_idx(indices)
    target_classes_o = torch.cat([t["labels"][J] for t, (_, J) in zip(targets, indices)])
    target_classes = torch.full(src_logits.shape[:2], self.num_classes,
                                    dtype=torch.int64, device=src_logits.device)
    target_classes[idx] = target_classes_o

    loss_ce = F.cross_entropy(src_logits.transpose(1, 2), target_classes, self.empty_weight)
    losses = {
    
    'loss_ce': loss_ce}

    return losses

target_classes_o se obtiene de acuerdo con el índice de destino para obtener todas las clases de verdad coincidentes y colocarlas en la posición correspondiente de target_classes de acuerdo con el índice src . Las predicciones que no coinciden se completan en target_classes con self.num_classes . La función de la función _get_src_permutation_idx es obtener el índice de lote de src y el índice de coincidencia correspondiente de la tupla de índices .

pérdida de caja

El box loss usa l1 loss y giou loss para predicciones que coinciden con éxito

def loss_boxes(self, outputs, targets, indices, num_boxes):
    """Compute the losses related to the bounding boxes, the L1 regression loss and the GIoU loss
       targets dicts must contain the key "boxes" containing a tensor of dim [nb_target_boxes, 4]
       The target boxes are expected in format (center_x, center_y, w, h), normalized by the image size.
    """
    assert 'pred_boxes' in outputs
    idx = self._get_src_permutation_idx(indices)
    src_boxes = outputs['pred_boxes'][idx]
    target_boxes = torch.cat([t['boxes'][i] for t, (_, i) in zip(targets, indices)], dim=0)

    loss_bbox = F.l1_loss(src_boxes, target_boxes, reduction='none')

    losses = {
    
    }
    losses['loss_bbox'] = loss_bbox.sum() / num_boxes

    loss_giou = 1 - torch.diag(box_ops.generalized_box_iou(
        box_ops.box_cxcywh_to_xyxy(src_boxes),
        box_ops.box_cxcywh_to_xyxy(target_boxes)))
    losses['loss_giou'] = loss_giou.sum() / num_boxes
    return losses

target_boxes es el cuadro de verdad de todas las coincidencias exitosas obtenidas por el índice objetivo , src_boxes son las predicciones de coincidencia exitosa obtenidas por el índice src , y se calculan las pérdidas l1_loss y giou entre ellas .

Aplicación de DETR en Segmentación Panorámica (mirada superficial)

Agregar un cabezal de máscara a cada objeto incrustado del decodificador puede realizar la función de segmentación a nivel de píxel. El cabezal de máscara puede entrenarse conjuntamente con el cuadro incrustado , o el cabezal de la máscara puede entrenarse por separado después de entrenar el cuadro incrustado .

El enfoque de DETR es similar al de Mask-RCNN , que predice la segmentación del cuadro correspondiente a la instancia en función de la predicción del cuadro dada . Aquí, DETR aumenta la muestra de la salida del mapa de atención por el encabezado de la máscara , lo agrega a algunas ramas de la columna vertebral , e implementa una función FPN , y luego realiza una operación argmax bit a bit en el estilo en negrita del mapa de la máscara correspondiente a todos los cuadros para obtener el resultado final. imagen de segmentación.

Finalmente (opinión personal)

La aplicación de la estructura Transformador en el campo CV muestra la estrecha relación entre los dos campos de IA informática de CV y ​​NLP , y la relación de promoción mutua entre los principales subcampos en el campo informático. Sin embargo, el enorme efecto de la estructura Transformer en el orden semántico de las palabras es aún más significativo en el procesamiento del lenguaje natural, y se aplica al campo de los grandes CV . Personalmente, creo que se debe a su papel especial en datos de características como serie temporal, que lo convierte en el objetivo Ha logrado muy buenos resultados en tareas relacionadas con el tiempo, como la detección y la comprensión de imágenes. Sin embargo, quizás las mejoras posteriores hagan que los dos se interrelacionen para obtener mejores resultados.

Este artículo es solo para aprender y compartir, registrar notas, comuníquese para eliminar la infracción

Supongo que te gusta

Origin blog.csdn.net/qq_53250079/article/details/127457575
Recomendado
Clasificación