当前位置:   article > 正文

GPT实战系列-P-Tuning本地化训练ChatGLM2等LLM模型,到底做了什么?(二)_pytorch 训练chatglm2 模型

pytorch 训练chatglm2 模型

GPT实战系列-如何使用P-Tuning本地化训练ChatGLM2等LLM模型?(二)


P-Tuning v2 将 ChatGLM2-6B 模型需要微调的参数量,减少到原来的 0.1%,再通过模型量化、Gradient Checkpoint 等方法,最低只需要 7GB 显存即可运行。

本文试图分析程序结构和代码,解释序列转换生成模型的微调训练。为了篇幅不要过长,分两篇文章解读,本文解读训练代码。框架概述请看前篇文章:GPT实战系列-P-Tuning本地化训练ChatGLM2等LLM模型,到底做了什么?(一)

对相关的内容感兴趣,也可以到GPT专栏里的文章看看。如果没有你感兴趣的,请留言,后续一起讨论。
GPT专栏:https://blog.csdn.net/alex_starsky/category_12467518.html

下面解读ptuning目录下的main.py代码。

1.训练参数配置传递

训练环节中,通过命令行配置参数(sh脚本),然后通过HfArgumentParser方法,把配置参数指定到模型,数据和训练的参数类中。通过参数解析器parser,进一步分类为模型参数,数据参数和训练超参数。

# 新版本的transformers中的ner没有采用传统的parser模块,利用HfArgumentParser方法,将参数类转化为argparse参数,以便于在命令行中指定他们。
# ModelArguments类为model/config/tokenizer涉及的参数。ModelArguments类 中包含的是关于模型的属性,如model_name,config_name,tokenizer_name等,类在run.py文件中定义;
# DataTrainingArguments类 为训练、测试数据集操作涉及到的参数。DataTrainingArguments类 中包含的是关于微调数据的属性,如task_name,data_dir等,类在transformers/data/datasets/glue.py文件中定义;
# TrainingArguments中包含的是关于微调过程的参数,如batch_size,learning_rate等参数,类在transformers/training_args.py中定义。

parser = HfArgumentParser((ModelArguments, DataTrainingArguments, Seq2SeqTrainingArguments))
# 通过参数解析器parser,进一步分类为模型参数,数据参数和训练超参数。    
# 训练时输入python run_ner.py *******.json,即从.json中读取参数。
# 当sys.argv参数为2时,此时sys.argv[0]为自己本身,即"run_ner.py",sys.argv[1]为json文件
if len(sys.argv) == 2 and sys.argv[1].endswith(".json"):
    model_args, data_args, training_args = parser.parse_json_file(json_file=os.path.abspath(sys.argv[1]))
else:
    model_args, data_args, training_args = parser.parse_args_into_dataclasses()

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

如果参数过多,或者需要保存参数,采用json文件格式,比如:

{
    "do_train": true,
    "train_file": "dataset/AdvertiseGen/train.json",
    "validation_file": "dataset/AdvertiseGen/dev.json",
    "preprocessing_num_workers": 10,
    "prompt_column": "content",
    "response_column": "summary",
    "overwrite_cache": true,
    "model_name_or_path": "/Users/andy/Desktop/LLM/model/chatglm2-6b",
    "output_dir": "output/adgen-chatglm2-6b-lora_version",
    "overwrite_output_dir": true,
    "max_source_length": 64,
    "max_target_length": 128,
    "per_device_train_batch_size": 1,
    "per_device_eval_batch_size": 1,
    "gradient_accumulation_steps": 16,
    "predict_with_generate": true,
    "max_steps": 3000,
    "logging_steps": 10,
    "save_steps": 100,
    "learning_rate": 2e-5,
    "lora_r": 32,
    "model_parallel_mode": true,
    "output_dir": "output"
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

训练过程的日志排版,打印信息等相关设置

#设置日志相关,排版格式,日期格式
# Setup logging
logging.basicConfig(
    format="%(asctime)s - %(levelname)s - %(name)s - %(message)s",
    datefmt="%m/%d/%Y %H:%M:%S",
    handlers=[logging.StreamHandler(sys.stdout)],
)
#设置打印信息
if training_args.should_log:
    # The default of training_args.log_level is passive, so we set log level at info here to have that default.
    transformers.utils.logging.set_verbosity_info()

#设置日志信息等级,transformer日志配置
log_level = training_args.get_process_log_level()
logger.setLevel(log_level)
# datasets.utils.logging.set_verbosity(log_level)
transformers.utils.logging.set_verbosity(log_level)
transformers.utils.logging.enable_default_handler()
transformers.utils.logging.enable_explicit_format()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

2.训练前准备

训练前的准备工作,包括预处理,训练数据集、验证数据集、测试数据集等参数配置

训练数据准备

# 加载数据集,训练文件,验证文件,测试文件,文件名,文件类型(扩展名)
    # Load dataset
    data_files = {}
    if data_args.train_file is not None:
        data_files["train"] = data_args.train_file
        extension = data_args.train_file.split(".")[-1]
    if data_args.validation_file is not None:
        data_files["validation"] = data_args.validation_file
        extension = data_args.validation_file.split(".")[-1]
    if data_args.test_file is not None:
        data_files["test"] = data_args.test_file
        extension = data_args.test_file.split(".")[-1]

    # 加载训练、验证、测试数据文件,cache目录
    raw_datasets = load_dataset(
        extension,
        data_files=data_files,
        cache_dir=model_args.cache_dir,
        use_auth_token=True if model_args.use_auth_token else None,
    )
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

训练模型的配置

预训练模型加载,序列长度,序列前缀处理

# 加载模型配置(模型架构),预训练模型
# Load pretrained model and tokenizer
config = AutoConfig.from_pretrained(model_args.model_name_or_path, trust_remote_code=True)
config.pre_seq_len = model_args.pre_seq_len
config.prefix_projection = model_args.prefix_projection
  • 1
  • 2
  • 3
  • 4
  • 5

加载分词器,对自然语言序列进行分解,分词处理

# 分词器
tokenizer = AutoTokenizer.from_pretrained(model_args.model_name_or_path, trust_remote_code=True)

  • 1
  • 2
  • 3

预训练模型的加载,有两种方式,一种是把过程加载方式,checkpoint,另一种是整合为一个模型bin格式,然后直接加载。

# 含有训练过程信息的checkpoint模型的加载模式,如ptuning训练中间结果,需要从pytorch_model.bin加载字典,
# 或者直接加载模型
if model_args.ptuning_checkpoint is not None:
    # Evaluation
    # Loading extra state dict of prefix encoder
    model = AutoModel.from_pretrained(model_args.model_name_or_path, config=config, trust_remote_code=True)
    prefix_state_dict = torch.load(os.path.join(model_args.ptuning_checkpoint, "pytorch_model.bin"))
    new_prefix_state_dict = {}
    for k, v in prefix_state_dict.items():
        if k.startswith("transformer.prefix_encoder."):
            new_prefix_state_dict[k[len("transformer.prefix_encoder."):]] = v
    model.transformer.prefix_encoder.load_state_dict(new_prefix_state_dict)
else:
    model = AutoModel.from_pretrained(model_args.model_name_or_path, config=config, trust_remote_code=True)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

训练量化精度配置
加载模型后,考虑到模型大小,占用显存大小,以及训练速度,需要配置模型数据精度,一般分为float32为浮点,f16半浮点精度,bf16半浮点精度,及int8,int4等整型8,4位精度。

# 量化处理,f16半浮点精度,即将pytorch默认的32位浮点型都改成16位浮点型。
if model_args.quantization_bit is not None:
    print(f"Quantized to {model_args.quantization_bit} bit")
    model = model.quantize(model_args.quantization_bit)
if model_args.pre_seq_len is not None:
    # P-tuning v2
    model = model.half()
    model.transformer.prefix_encoder.float() #前缀编码器
else:
    # Finetune
    model = model.float()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

数据预处理
数据集预处理,包括训练数据集,验证数据集,测试数据集。

prefix = data_args.source_prefix if data_args.source_prefix is not None else ""

    # 数据集预处理,分词处理
    # Preprocessing the datasets.
    # We need to tokenize inputs and targets.
    if training_args.do_train:
        column_names = raw_datasets["train"].column_names
    elif training_args.do_eval:
        column_names = raw_datasets["validation"].column_names
    elif training_args.do_predict:
        column_names = raw_datasets["test"].column_names
    else:
        logger.info("There is nothing to do. Please pass `do_train`, `do_eval` and/or `do_predict`.")
        return

    # 输入数据(prompt),标注目标数据(response),上下文历史会话(history)
    # Get the column names for input/target.
    prompt_column = data_args.prompt_column
    response_column = data_args.response_column
    history_column = data_args.history_column
    
    # 最大模板长度
    # Temporarily set max_target_length for training.
    max_target_length = data_args.max_target_length
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

3.训练参数配置

训练数据集参数配置,最大训练样本数,数据预处理函数,GPU显卡数,批处理,cache处理等配置

# 训练前配置和数据集处理,训练,训练数据,最大训练样本数,
    if training_args.do_train:
        if "train" not in raw_datasets:
            raise ValueError("--do_train requires a train dataset")
        train_dataset = raw_datasets["train"]
        if data_args.max_train_samples is not None:
            max_train_samples = min(len(train_dataset), data_args.max_train_samples)
            train_dataset = train_dataset.select(range(max_train_samples))
        with training_args.main_process_first(desc="train dataset map pre-processing"):
            train_dataset = train_dataset.map(
                preprocess_function_train,
                batched=True,
                num_proc=data_args.preprocessing_num_workers,
                remove_columns=column_names,
                load_from_cache_file=not data_args.overwrite_cache,
                desc="Running tokenizer on train dataset",
            )
        print_dataset_example(train_dataset[0])
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

同样方式,配置验证数据集,测试数据集参数。

    # 训练前验证评价的配置和数据集处理,验证评价
    if training_args.do_eval:
        max_target_length = data_args.val_max_target_length
        if "validation" not in raw_datasets:
            raise ValueError("--do_eval requires a validation dataset")
        eval_dataset = raw_datasets["validation"]
        #最大验证样本数
        if data_args.max_eval_samples is not None:
            max_eval_samples = min(len(eval_dataset), data_args.max_eval_samples)
            eval_dataset = eval_dataset.select(range(max_eval_samples))
        #
        with training_args.main_process_first(desc="validation dataset map pre-processing"):
            eval_dataset = eval_dataset.map(
                preprocess_function_eval,
                batched=True,
                num_proc=data_args.preprocessing_num_workers,
                remove_columns=column_names,
                load_from_cache_file=not data_args.overwrite_cache,
                desc="Running tokenizer on validation dataset",
            )
        print_dataset_example(eval_dataset[0])

    # 训练前预测的配置和数据集处理,预测
    if training_args.do_predict:
        max_target_length = data_args.val_max_target_length
        if "test" not in raw_datasets:
            raise ValueError("--do_predict requires a test dataset")
        predict_dataset = raw_datasets["test"]
        #最大预测样本
        if data_args.max_predict_samples is not None:
            max_predict_samples = min(len(predict_dataset), data_args.max_predict_samples)
            predict_dataset = predict_dataset.select(range(max_predict_samples))
        #数据集重映射
        with training_args.main_process_first(desc="prediction dataset map pre-processing"):
            predict_dataset = predict_dataset.map(
                preprocess_function_eval,
                batched=True,
                num_proc=data_args.preprocessing_num_workers,
                remove_columns=column_names,
                load_from_cache_file=not data_args.overwrite_cache,
                desc="Running tokenizer on prediction dataset",
            )
        print_dataset_example(predict_dataset[0])
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43

数据分词,类型转换,pad等数据预处理操作

#训练准备,模型,数据加载
    # Data collator
    label_pad_token_id = -100 if data_args.ignore_pad_token_for_loss else tokenizer.pad_token_id
    data_collator = DataCollatorForSeq2Seq(
        tokenizer,
        model=model,
        label_pad_token_id=label_pad_token_id,
        pad_to_multiple_of=None,
        padding=False
    )
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

4.训练对象,seq2seq训练

序列训练参数配置,生成的length,beams配置,

# 序列训练参数配置,如有配置则重置解码参数
    # Override the decoding parameters of Seq2SeqTrainer
    training_args.generation_max_length = (
        training_args.generation_max_length
        if training_args.generation_max_length is not None
        else data_args.val_max_target_length
    )
    training_args.generation_num_beams = (
        data_args.num_beams if data_args.num_beams is not None else training_args.generation_num_beams
    )
    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

训练器参数配置,预训练模型,训练超参数,相关数据集。分词器,训练度量器等过程参数配置。

#初始化训练参数,配置模型,训练参数,训练数据,分词器,数据加载工具,过程度量
    # Initialize our Trainer
    trainer = Seq2SeqTrainer(
        model=model,
        args=training_args,
        train_dataset=train_dataset if training_args.do_train else None,
        eval_dataset=eval_dataset if training_args.do_eval else None,
        tokenizer=tokenizer,
        data_collator=data_collator,
        compute_metrics=compute_metrics if training_args.predict_with_generate else None,
        save_changed=model_args.pre_seq_len is not None
    )
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

5.执行训练

执行训练过程,模型保存方式,过程度量及状态保存。

# 训练执行过程
    # Training
    if training_args.do_train:
        checkpoint = None
        if training_args.resume_from_checkpoint is not None:
            checkpoint = training_args.resume_from_checkpoint
        # elif last_checkpoint is not None:
        #     checkpoint = last_checkpoint
        model.gradient_checkpointing_enable()
        model.enable_input_require_grads()
        train_result = trainer.train(resume_from_checkpoint=checkpoint)
        # trainer.save_model()  # Saves the tokenizer too for easy upload

        metrics = train_result.metrics
        max_train_samples = (
            data_args.max_train_samples if data_args.max_train_samples is not None else len(train_dataset)
        )
        metrics["train_samples"] = min(max_train_samples, len(train_dataset))

        trainer.log_metrics("train", metrics)
        trainer.save_metrics("train", metrics)
        trainer.save_state()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

6.训练模型评估

训练后,数据集评估模型效果,预测处理。

#训练结束后评估
    # Evaluation
    results = {}
    max_seq_length = data_args.max_source_length + data_args.max_target_length + 1
    # 训练评估
    if training_args.do_eval:
        logger.info("*** Evaluate ***")
        metrics = trainer.evaluate(metric_key_prefix="eval", do_sample=True, top_p=0.7, max_length=max_seq_length, temperature=0.95)
        max_eval_samples = data_args.max_eval_samples if data_args.max_eval_samples is not None else len(eval_dataset)
        metrics["eval_samples"] = min(max_eval_samples, len(eval_dataset))

        trainer.log_metrics("eval", metrics)
        trainer.save_metrics("eval", metrics)

    # 训练预测
    if training_args.do_predict:
        logger.info("*** Predict ***")
        predict_results = trainer.predict(predict_dataset, metric_key_prefix="predict", max_length=max_seq_length, do_sample=True, top_p=0.7, temperature=0.95)
        metrics = predict_results.metrics
        max_predict_samples = (
            data_args.max_predict_samples if data_args.max_predict_samples is not None else len(predict_dataset)
        )
        metrics["predict_samples"] = min(max_predict_samples, len(predict_dataset))

        trainer.log_metrics("predict", metrics)
        trainer.save_metrics("predict", metrics)

        if trainer.is_world_process_zero():
            if training_args.predict_with_generate:
                predictions = tokenizer.batch_decode(
                    predict_results.predictions, skip_special_tokens=True, clean_up_tokenization_spaces=True
                )
                predictions = [pred.strip() for pred in predictions]
                labels = tokenizer.batch_decode(
                    predict_results.label_ids, skip_special_tokens=True, clean_up_tokenization_spaces=True
                )
                labels = [label.strip() for label in labels]
                output_prediction_file = os.path.join(training_args.output_dir, "generated_predictions.txt")
                with open(output_prediction_file, "w", encoding="utf-8") as writer:
                    for p, l in zip(predictions, labels):
                        res = json.dumps({"labels": l, "predict": p}, ensure_ascii=False)
                        writer.write(f"{res}\n")
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

依赖

中文分词器,中文评价指标,数据集管理

pip install rouge_chinese nltk jieba datasets
  • 1

依赖模块解读

transformers提供数千个预先训练好的模型来执行不同模式的任务,如文本、视觉和音频。

transformer提供api,可以快速下载,并在给定文本上使用这些预训练的模型,在您自己的数据集上对它们进行微调,然后在Huggingface的模型中心上与社区共享。同时,每个定义架构的python模块都是完全独立的,可以进行修改以进行快速研究实验。

  • AutoConfig模块,模型的配置类是指定模型的构建方式。配置指定模型的属性,如隐藏层或注意力头的数量。当从自定义配置类初始化模型时,将从头开始。模型属性是随机初始化的,在使用模型得到有意义的结果之前,需要对模型进行训练。导入AutoConfig,然后加载要修改的预训练模型。在AutoConfig.from_pretrained()中,可以指定要更改的属性,例如注意力头的数量。
  • AutoModel加载一个预训练模型,Transformers提供简单统一的方法来加载。
  • AutoTokenizer加载的一个自动分词器。
  • DataCollatorForSeq2Seq 是在进行序列生成任务时(QA、文本概括等)使用的数据收集器,需要模型的输出是一个序列。该数据收集器不仅会动态的填充数据的数据,而且也会填充数据对应的标签。
  • HfArgumentParser可以将类对象中的实例属性转换成转换为解析参数。必须注意的是,这里的类对象必须是通过@dataclass()创建的类对象。并且通过HfArgumentParser创建的解析参数,都是可选参数。
# Transformers 工具已经帮我们封装了用于训练文本生成模型的 Seq2SeqTrainer 类,无需我们自己再去定义损失函数与优化方法了。
import transformers
from transformers import (
    AutoConfig,
    AutoModel,
    AutoTokenizer,
    DataCollatorForSeq2Seq,
    HfArgumentParser,
    Seq2SeqTrainingArguments,
    set_seed,
)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

Trainer 所有模型都是标准的torch.nn.Module,因此可以在任何典型的训练循环中使用它们。虽然您可以编写自己的训练迭代器,但Transformers为PyTorch提供了Trainer类,其中包含基本的训练循环,并添加了额外的功能,如分布式训练、混合精度等。

  • Seq2SeqTrainingArguments 设置训练参数。
  • Seq2SeqTrainer 序列类的训练迭代器
# Seq2SeqTrainingArguments 设置训练参数。
# 例如,下面设置训练参数,配置学习率为2e-5,训练时batch大小为8,验证时为batch大小32,训练轮数为5,权重衰减大小为0.01,输出文件夹为model_for_seq2seqlm,日志记录的步长为10,即10个batch记录一次;评估策略为训练完一个epoch之后进行评估,模型保存策略同上,设置训练完成后加载最优模型,并指定最优模型的评估指标为rougeL,最后,需要指定predict_with_generate参数值为True。
# training_args = Seq2SeqTrainingArguments(
#     learning_rate=3e-5,
#     per_device_train_batch_size=8,
#     per_device_eval_batch_size=32,
#     num_train_epochs=5,
#     weight_decay=0.01,
#     output_dir="model_for_seq2seqlm",
#     logging_steps=10,
#     evaluation_strategy = "epoch",
#     save_strategy = "epoch",
#     load_best_model_at_end=True,
#     metric_for_best_model="rougeL",
#     predict_with_generate=True   # 训练最后会调用generate方法进行生成
# )

from trainer_seq2seq import Seq2SeqTrainer

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • ModelArguments模型参数,
  • DataTrainingArguments数据集操作涉及的参数
#ModelArguments类为model/config/tokenizer涉及的参数
#DataTrainingArguments类为数据涉及到的参数
from arguments import ModelArguments, DataTrainingArguments
  • 1
  • 2
  • 3

数据集的预处理

ADGEN数据集任务的数据形式,输入(content),生成输出(summary)

{
    "content": "类型#上衣*版型#宽松*版型#显瘦*图案#线条*衣样式#衬衫*衣袖型#泡泡袖*衣款式#抽绳",
    "summary": "这件衬衫的款式非常的宽松,利落的线条可以很好的隐藏身材上的小缺点,穿在身上有着很好的显瘦效果。领口装饰了一个可爱的抽绳,漂亮的绳结展现出了十足的个性,配合时尚的泡泡袖型,尽显女性甜美可爱的气息。"
}
  • 1
  • 2
  • 3
  • 4

觉得有用 点个赞 + 收藏 吧

End


GPT专栏文章:
GPT实战系列-P-Tuning本地化训练ChatGLM2等LLM模型,到底做了什么?(一)

GPT实战系列-ChatGLM3本地部署CUDA11+1080Ti+显卡24G实战方案

GPT实战系列-ChatGLM2模型的微调训练参数解读

GPT实战系列-如何用自己数据微调ChatGLM2模型训练

GPT实战系列-ChatGLM2部署Ubuntu+Cuda11+显存24G实战方案

GPT实战系列-Baichuan2本地化部署实战方案

决策引擎专栏:
Falcon构建轻量级的REST API服务

决策引擎-利用Drools实现简单防火墙策略

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家小花儿/article/detail/255342?site
推荐阅读
相关标签
  

闽ICP备14008679号