基于BERT实现机器阅读理解

本文介绍基于BERT的阅读理解实验,旨在掌握BERT相关知识及飞桨构建方法。实验以DuReaderRobust数据集为对象,通过数据处理、模型构建等六步实现。数据预处理含特征转换,模型含多层结构,经训练、评估,最终保存模型,评估用ROUGE-L指标,F1值达84.3176。

☞☞☞AI 智能聊天, 问答助手, AI 智能搜索, 免费无限量使用 DeepSeek R1 模型☜☜☜

基于bert实现机器阅读理解 - 创想鸟

1. 实验介绍

1.1 实验目的

理解并掌握BERT预训练语言模型基础知识点,包括:Transformer、LayerNorm等; 掌握BERT预训练语言模型的设计原理以及构建流程; 熟悉使用飞桨开源框架构建BERT的方法。

1.2 实验内容

1.2.1 阅读理解任务

阅读理解是自然语言处理中的一个重要的任务,最常见的数据集是单篇章、抽取式阅读理解数据集。即:对于一个给定的问题 q 和一个篇章 p ,根据篇章内容,给出该问题的答案 a 。数据集中的每个样本都是一个三元组。举例说明:

问题 q: 燃气热水器哪个牌子好?

篇章 p : 选择燃气热水器时,一定要关注这几个问题:

出水稳定性要好,不能出现忽热忽冷的现象 快速到达设定的需求水温 操作要智能、方便 安全性要好,要装有安全报警装置 市场上燃气热水器品牌众多,购买时还需多加对比和仔细鉴别。 方太今年主打的磁化恒温热水器在使用体验方面做了全面升级:9秒速热,可快速进入洗浴模式;水温持久稳定,不会出现忽热忽冷的现象,并通过水量伺服技术将出水温度精确控制在±0.5℃,可满足家里宝贝敏感肌肤洗护需求;配备CO和CH4双气体报警装置更安全(市场上一般多为CO单气体报警)。另外,这款热水器还有智能WIFI互联功能,只需下载个手机APP即可用手机远程操作热水器,实现精准调节水温,满足家人多样化的洗浴需求。当然方太的磁化恒温系列主要的是增加磁化功能,可以有效吸附水中的铁锈、铁屑等微小杂质,防止细菌滋生,使沐浴水质更洁净,长期使用磁化水沐浴更利于身体健康。

参考答案 a : 方太

目前阅读理解任务已经在各产业领域广泛应用,如:在智能客服应用中,可以使用机器阅读用户手册等材料,自动或辅助客服回答用户问题;在教育领域,可利用该技术从海量题库中辅助出题;在金融领域,该技术实现可从大量新闻文本中抽取相关金融信息等。

注:用BERT微调来解决机器阅读理解问题已经成为NLP的主流思路,本文的实验都是基于bert进行阅读理解任务。

1.2.2 BERT介绍

BERT(Bidirectional Encoder Representation from Transformers,BERT)是2018年10月由Google AI研究院提出的预训练模型,BERT在机器阅读理解顶级水平测试SQuAD1.1中表现出惊人的成绩: 全部两个衡量指标上全面超越人类,并且在11种不同NLP测试中创出SOTA表现,包括将GLUE基准推高至80.4% (绝对改进7.6%),MultiNLI准确度达到86.7% (绝对改进5.6%),成为NLP发展史上的里程碑式的模型成就。

BERT的网络架构使用的是《Attention is all you need》中提出的多层Transformer结构,如图2所示。其最大的特点是抛弃了传统的RNN和CNN,通过Attention机制将任意位置的两个单词的距离转换成1,有效的解决了NLP中棘手的长期依赖问题。Transformer的结构在NLP和CV领域中已经得到了广泛应用。 基于BERT实现机器阅读理解 - 创想鸟        

1.2.3 BERT的预训练任务

BERT是一个多任务模型,它的预训练(Pre-training)任务是由两个自监督任务组成,即MLM和NSP,如图3所示。

基于BERT实现机器阅读理解 - 创想鸟        

1.MLM是指在训练的时候随即从输入预料上mask掉一些单词,然后通过的上下文预测该单词,该任务非常像我们在中学时期经常做的完形填空。正如传统的语言模型算法和RNN匹配那样,MLM的这个性质和Transformer的结构是非常匹配的。在BERT的实验中,15%的WordPiece Token会被随机Mask掉。在训练模型时,一个句子会被多次喂到模型中用于参数学习,但是Google并没有在每次都mask掉这些单词,而是在确定要Mask掉的单词之后,做如下处理: 80%的时候会直接替换为[Mask],将句子 “I love my family” 转换为句子 “I love my [Mask]”。

10%的时候将其替换为其它任意单词,将单词 “family” 替换成另一个随机词,例如 “cat”。将句子 “I love my family” 转换为句子 “I love my cat”。

10%的时候会保留原始Token,例如保持句子为 “I love my family” 不变。

2.Next Sentence Prediction(NSP)的任务是判断句子B是否是句子A的下文。如果是的话输出’IsNext‘,否则输出’NotNext‘。训练数据的生成方式是从平行语料中随机抽取的连续两句话,其中50%保留抽取的两句话,它们符合IsNext关系,另外50%的第二句话是随机从预料中提取的,它们的关系是NotNext的。这个关系保存在图4中的[CLS]符号中。

输入 = [CLS] 我 喜欢 [Mask] 学习 [SEP] 我 最 擅长 的 [Mask] 是 NLP [SEP] 类别 = IsNext

输入 = [CLS] 我 喜欢 [Mask] 学习 [SEP] 今天 我 跟 别人 [Mask] 了 [SEP] 类别 = NotNext

1.2.4 BERT的微调

在海量的语料上训练完BERT之后,便可以将其应用到NLP的各个任务进行微调了。微调(Fine-Tuning)的任务包括:基于句子对的分类任务,基于单个句子的分类任务,问答任务,命名实体识别等。下面分别介绍BERT的微调任务:

1. 基于句子对的分类任务

MNLI:给定一个前提 (Premise) ,根据这个前提去推断假设 (Hypothesis) 与前提的关系。该任务的关系分为三种,蕴含关系 (Entailment)、矛盾关系 (Contradiction) 以及中立关系 (Neutral)。所以这个问题本质上是一个分类问题,我们需要做的是去发掘前提和假设这两个句子对之间的交互信息。 QQP:基于Quora,判断 Quora 上的两个问题句是否表示的是一样的意思。 QNLI:用于判断文本是否包含问题的答案,类似于我们做阅读理解定位问题所在的段落。 STS-B:预测两个句子的相似性,包括5个级别。 MRPC:也是判断两个句子是否是等价的。 RTE:类似于MNLI,但是只是对蕴含关系的二分类判断,而且数据集更小。 SWAG:从四个句子中选择为可能为前句下文的那个。 2. 基于单个句子的分类任务

SST-2:电影评价的情感分析。 CoLA:句子语义判断,是否是可接受的(Acceptable)。 3. 问答任务

SQuAD v1.1:给定一个句子(通常是一个问题)和一段描述文本,输出这个问题的答案,类似于做阅读理解的简答题。 4. 命名实体识别

CoNLL-2003 NER:判断一个句子中的单词是不是Person,Organization,Location,Miscellaneous或者other(无命名实体)。

基于BERT实现机器阅读理解 - 创想鸟        

1.3 实验环境

建议您使用AI Studio进行操作。

1.4 实验设计

本实验构建了一个基于BERT的阅读理解模型,实现方案如 图5 所示,阅读理解的主体部分是由BERT组成。

训练阶段:BERT模型的输入是Question(问题)和Paragraph(文章),输出则是答案的位置。

推理阶段:使用训练好的抽取式阅读理解模型,输入问题和文章,模型输出答案的位置,然后在文章中抽取相应位置的文字即可。

基于BERT实现机器阅读理解 - 创想鸟基于BERT实现机器阅读理解 - 创想鸟        

从图中可以看到,QA 任务的输入是两个句子,用 [SEP] 分隔,第一个句子是问题(Question),第二个句子是含有答案的上下文(Paragraph);输出是作为答案开始和结束的可能性(Start/End Span)

2. 实验详细实现

机器阅读理解实验流程如 图6 所示,包含如下6个步骤:

数据处理:根据网络接收的数据格式,完成相应的预处理操作,保证模型正常读取; 模型构建:设计BERT网络结构; 训练配置:实例化模型,加载模型参数,指定模型采用的寻解算法(优化器); 模型训练:执行多轮训练不断调整参数,以达到较好的效果; 模型保存:将模型参数保存到指定位置,便于后续推理或继续训练使用。 模型评估:对训练好的模型进行评估测试,观察准确率和Loss;

基于BERT实现机器阅读理解 - 创想鸟        

2.1 数据处理

2.1.1 数据集介绍

PaddleNLP已经内置SQuAD,CMRC等中英文阅读理解数据集,一键即可加载。本实例加载的是DuReaderRobust中文阅读理解数据集。由于DuReaderRobust数据集采用SQuAD数据格式。

DuReaderRobust数据集的格式如下

{ “data”: [ { “title”: “”, “paragraphs”: [ { “qas”: [ { “question”: “非洲气候带”, “id”: “bd664cb57a602ae784ae24364a602674”, “answers”: [ { “text”: “热带气候”, “answer_start”: 45 } ] } ], “context”: “1、全年气温高,有热带大陆之称。主要原因在与赤道穿过大陆中部,位于南北纬30度之间,主要是热带气候,没有温带和寒带。2、气候带呈明显带状分布,且南北对称。原因在于赤道穿过大陆中部,整个大陆基本被赤道均分为两部分。因此,纬度地带性明显。气候带以热带雨林为中心,向南北依次分布着热带草原、热带沙漠和地中海式气候。3、气候炎热干燥。第一:热带雨林气候面积较小,主要位于刚果河流域,面积较小。第二,地中海式气候,位于大陆的南北边缘,面积较小。夏季炎热而干旱,冬季温暖而湿润。第三,面积较大热带草原气候,有明显的干湿季。第四,热带沙漠气候主要位于撒哈拉大沙漠和西南角狭长地带。而撒哈拉沙漠占非洲总面积的1/4,全年炎热干燥,日照时间长,昼夜温差大。总之,全非洲纬度低,气温高;干燥地区广,常年湿润地区面积小。” }, { “qas”: [ { “question”: “韩国全称”, “id”: “a7eec8cf0c55077e667e0d85b45a6b34”, “answers”: [ { “text”: “大韩民国”, “answer_start”: 5 } ] } ], “context”: “韩国全称“大韩民国”,位于朝鲜半岛南部,隔“三八线”x0d与朝鲜民主主义人民共和国相邻,面积9.93万平方公理,x0d南北长约500公里,东西宽约250公里,东濒日本海,西临黄海 ,东南与日本隔海相望。 韩国的地形特点是山地多,平原少,海岸线长而曲折。韩国四 季分明,气候温和、湿润。x0d目前韩国主要政党包括执政的新千年民主党和在野的大国家党、x0d自由民主联盟等,大x0d国家党为韩国会内的第一大党。x0d韩国首都为汉城,全国设有1个特别市(汉城市)、6个广域市(x0d釜山市、大邱市、仁x0d川市、光州市、大田市、蔚山市)、9个道(京畿道、江源道、x0d忠清北道、忠清南道、全x0d罗北道、全罗南道、庆尚北道、庆尚南道、济州道)。x0d海岸线全长5,259公里,主要港口x0d有釜山、仁川、浦项、蔚山、光阳等。” }, } ] }

说明:

数据是以json的形式存储的,json最顶层是data,接下来是title和paragraphs,paragraphs包含多个qas和context,其中context和qas是一一对应的,每个qas包含question,id,answers,context则是qas对应的上下文。机器阅读理解就是从context中找到给定questions对应的answers片段,answers里面的answer_start表示的是答案的起始位置,text表示的是答案。

2.1.2 数据预处理

数据预处理部分首先要把paddlenlp升级到最新版本,部分功能的实现还需要paddlenlp的参与,然后实现专门处理bert输入文本的BertTokenizer,利用BertTokenizer处理加载的文本就可以了。

aistudio默认的paddlenlp过旧,所以这里手动升级paddlenlp版本,保持paddlenlp为最新版本

In [ ]

!pip install paddlenlp --upgrade

   

加载paddle的库和第三方的库

In [ ]

import siximport functoolsimport inspectimport paddleimport osimport jsonimport paddleimport paddle.nn as nnimport paddle.tensor as tensorimport paddle.nn.functional as Ffrom paddle.nn import Layerfrom paddle.nn import TransformerEncoder, Linear, Layer, Embedding, LayerNorm, Tanhfrom paddlenlp.transformers.tokenizer_utils import PretrainedTokenizerfrom paddlenlp.transformers.bert.tokenizer import BasicTokenizer,WordpieceTokenizerfrom paddlenlp.data import Pad, Stack, Tuple, Dictfrom paddle.io import DataLoaderfrom paddle.optimizer.lr import LambdaDecayimport sysimport math

   

BertTokenizer的实现需要继承PretrainedTokenizer,然后下载相应的配置文件,本实验下载的是bert-base-chinese的配置,初始化BasicTokenizer,WordpieceTokenizer,以下分别介绍这几个tokenizer各自的功能:

BasicTokenizer的主要是进行unicode转换、标点符号分割、小写转换、中文字符分割、去除重音符号等操作,最后返回的是关于词的数组(中文是字的数组) WordpieceTokenizer的目的是将合成词分解成类似词根一样的词片。例如将”unwanted”分解成[“un”, “##want”, “##ed”]这么做的目的是防止因为词的过于生僻没有被收录进词典最后只能以[UNK]代替的局面,因为英语当中这样的合成词非常多,词典不可能全部收录。 PretrainedTokenizer:实现从本地文件或目录加载/保存tokenizer常用方法,或从库提供的预训练的tokenizer加载/保存 [CLS]表示该特征用于分类模型,对非分类模型,该符号可以省去。[SEP]表示分句符号,用于断开输入语料中的两个句子。[MASK]用于预训练MLM任务中,用于替换被mask的位置的占位符。[PAD]表示的是对齐符号,把模型的输入序列对齐到固定长度。

In [ ]

class BertTokenizer(PretrainedTokenizer):    resource_files_names = {"vocab_file": "vocab.txt"}  # for save_pretrained    pretrained_resource_files_map = {        "vocab_file": {            "bert-base-chinese":            "https://paddle-hapi.bj.bcebos.com/models/bert/bert-base-chinese-vocab.txt",        }    }    pretrained_init_configuration = {        "bert-base-chinese": {            "do_lower_case": False        },    }    padding_side = 'right'    def __init__(self,vocab_file,do_lower_case=True,unk_token="[UNK]",sep_token="[SEP]",                 pad_token="[PAD]",                 cls_token="[CLS]",                 mask_token="[MASK]"):        if not os.path.isfile(vocab_file):            raise ValueError(                "Can't find a vocabulary file at path '{}'. To load the "                "vocabulary from a pretrained model please use "                "`tokenizer = BertTokenizer.from_pretrained(PRETRAINED_MODEL_NAME)`"                .format(vocab_file))        # 加载词汇文件vocab.txt,返回一个有序字典        self.vocab = self.load_vocabulary(vocab_file, unk_token=unk_token)        # 定义 BasicTokenizer        self.basic_tokenizer = BasicTokenizer(do_lower_case=do_lower_case)        # 定义 WordpieceTokenizer        self.wordpiece_tokenizer = WordpieceTokenizer(            vocab=self.vocab, unk_token=unk_token)    @property    def vocab_size(self):        # 返回词汇表的大小        return len(self.vocab)    def _tokenize(self, text):        split_tokens = []        # 进行unicode转换、标点符号分割、小写转换、中文字符分割、去除重音符号等操作,最后返回的是关于词的数组(中文是字的数组)        for token in self.basic_tokenizer.tokenize(text):            # 将合成词分解成类似词根一样的词片,例如将"unwanted"分解成["un", "##want", "##ed"]            for sub_token in self.wordpiece_tokenizer.tokenize(token):                split_tokens.append(sub_token)        return split_tokens    def tokenize(self, text):        # 对文本进行切分和wordPiece化        return self._tokenize(text)    def convert_tokens_to_string(self, tokens):        # 对tokens的list进行拼接,并去除里面的##符号        out_string = " ".join(tokens).replace(" ##", "").strip()        return out_string    def num_special_tokens_to_add(self, pair=False):        token_ids_0 = []        token_ids_1 = []        return len(            self.build_inputs_with_special_tokens(token_ids_0, token_ids_1                                                  if pair else None))    def build_inputs_with_special_tokens(self, token_ids_0, token_ids_1=None):        # 给输入文本加上cls开始符号,和sep分隔符号        if token_ids_1 is None:            return [self.cls_token_id] + token_ids_0 + [self.sep_token_id]        _cls = [self.cls_token_id]        _sep = [self.sep_token_id]        return _cls + token_ids_0 + _sep + token_ids_1 + _sep    def build_offset_mapping_with_special_tokens(self,offset_mapping_0,offset_mapping_1=None):        # 用来记录每个词起始字符和结束字符的索引        if offset_mapping_1 is None:            return [(0, 0)] + offset_mapping_0 + [(0, 0)]        return [(0, 0)] + offset_mapping_0 + [(0, 0)                                              ] + offset_mapping_1 + [(0, 0)]    def create_token_type_ids_from_sequences(self,                                             token_ids_0,                                             token_ids_1=None):        # 从传递的两个序列创建一个掩码,用于序列对分类任务。        _sep = [self.sep_token_id]        _cls = [self.cls_token_id]        if token_ids_1 is None:            return len(_cls + token_ids_0 + _sep) * [0]        return len(_cls + token_ids_0 + _sep) * [0] + len(token_ids_1 +                                                          _sep) * [1]    def get_special_tokens_mask(self,                                token_ids_0,                                token_ids_1=None,                                already_has_special_tokens=False):        # 从没有添加特殊标记的标记列表中检索序列ID。添加时调用此方法使用标记器“encode”方法的特殊标记。        if already_has_special_tokens:            if token_ids_1 is not None:                raise ValueError(                    "You should not supply a second sequence if the provided sequence of "                    "ids is already formatted with special tokens for the model."                )            return list(                map(lambda x: 1 if x in [self.sep_token_id, self.cls_token_id] else 0,                    token_ids_0))        if token_ids_1 is not None:            return [1] + ([0] * len(token_ids_0)) + [1] + (                [0] * len(token_ids_1)) + [1]        return [1] + ([0] * len(token_ids_0)) + [1]

   

BERT提供了简单和复杂两个模型,分别是bert-base和bert-large,本实验采用bert-base模型,因为dureader是中文数据集,所以选择中文的配置和模型,最终我们选择bert-base-chinese来做本次实验,以下代码加载了bert-base-chinese的词汇和配置,用于后面的中文处理。

In [ ]

model_name_or_path='bert-base-chinese'tokenizer = BertTokenizer.from_pretrained(model_name_or_path)

   

由于本地没有bert-base-chinese-vocab.txt,所以自动下载了bert-base-chinese-vocab.txt,然后实例化了tokenizer

2.1.3 批量数据读取

训练集合处理

使用load_dataset()API默认读取到的数据集是MapDataset对象,MapDataset是paddle.io.Dataset的功能增强版本。其内置的map()方法适合用来进行批量数据集处理。map()方法传入的是一个用于数据处理的function。

由于文章加问题的文本长度可能大于max_seq_length,答案出现的位置有可能出现在文章最后,所以不能简单的对文章进行截断。那么对于过长的文章,则采用滑动窗口将文章分成多段,分别与问题组合。再用对应的tokenizer转化为模型可接受的feature。doc_stride参数就是每次滑动的距离。滑动窗口生成样本的过程如 图7 所示:基于BERT实现机器阅读理解 - 创想鸟        

In [ ]

from paddlenlp.datasets import load_datasetdoc_stride=128  # 滑动窗口的大小max_seq_length=384  # 分词后的最大长度task_name='dureader_robust'

   In [ ]

def prepare_train_features(examples):    contexts = [examples[i]['context'] for i in range(len(examples))]    questions = [examples[i]['question'] for i in range(len(examples))]    tokenized_examples = tokenizer(            questions,            contexts,            stride=doc_stride,            max_seq_len=max_seq_length)    for i, tokenized_example in enumerate(tokenized_examples):        # 把不可能的答案用CLS字符来索引        input_ids = tokenized_example["input_ids"]        cls_index = input_ids.index(tokenizer.cls_token_id)        # offset mapping会建立一个从token到字符在原文中的位置的映射,这帮助我们计算开始位置和结束位置        offsets = tokenized_example['offset_mapping']        # 抓取样本对应的序列(知道上下文是什么,问题是什么)        sequence_ids = tokenized_example['token_type_ids']        # 一个样本可以有多个span,这是包含此文本span的示例的索引。        sample_index = tokenized_example['overflow_to_sample']        answers = examples[sample_index]['answers']        answer_starts = examples[sample_index]['answer_starts']        # 文本中答案所在的开始字符和结束字符的索引        start_char = answer_starts[0]        end_char = start_char + len(answers[0])        # 文本中当前span的开始token的索引        token_start_index = 0        while sequence_ids[token_start_index] != 1:            token_start_index += 1        # 文本中当前span的结束token的索引        token_end_index = len(input_ids) - 1        while sequence_ids[token_end_index] != 1:            token_end_index -= 1        token_end_index -= 1        # 检查答案是否超过了span(特征用cls的索引标注),如果超过了,答案的位置用cls的位置代替        if not (offsets[token_start_index][0] = end_char):            tokenized_examples[i]["start_positions"] = cls_index            tokenized_examples[i]["end_positions"] = cls_index        else:            # 把token_start_index和token_end_index移动到答案的两端            while token_start_index < len(offsets) and offsets[                    token_start_index][0] = end_char:                token_end_index -= 1            tokenized_examples[i]["end_positions"] = token_end_index + 1    return tokenized_examples

   

读取dureaderrobust的训练集合,把json数据处理成list的形式,list里面每一项以键值对的形式存放。

In [ ]

train_ds = load_dataset(task_name, splits='train')print(train_ds[:2])

   

输出了模型的2条数据,用list格式保存,list的每一项都是一个字典,键值对形式。

In [ ]

train_ds.map(prepare_train_features, batched=True)

   In [ ]

for idx in range(2):    print('input_ids:{}'.format(train_ds[idx]['input_ids']))    print('token_type_ids:{}'.format(train_ds[idx]['token_type_ids']))    print('overflow_to_sample:{}'.format(train_ds[idx]['overflow_to_sample']))    print('offset_mapping:{}'.format(train_ds[idx]['offset_mapping']))    print('start_positions:{}'.format(train_ds[idx]['start_positions']))    print('end_positions:{}'.format(train_ds[idx]['end_positions']))    print()

   

从以上结果可以看出,数据集中的example已经被转换成了模型可以接收的feature,包括input_ids、token_type_ids、答案的起始位置等信息。 其中:

input_ids: 表示输入文本的token ID。 token_type_ids: 表示对应的token属于输入的问题还是答案。(Transformer类预训练模型支持单句以及句对输入)。 overflow_to_sample: feature对应的example的编号。 offset_mapping: 每个token的起始字符和结束字符在原文中对应的index(用于生成答案文本)。 start_positions: 答案在这个feature中的开始位置。 end_positions: 答案在这个feature中的结束位置。

测试集合处理

测试集合的生成跟训练集合类似

In [ ]

def prepare_validation_features(examples):    contexts = [examples[i]['context'] for i in range(len(examples))]    questions = [examples[i]['question'] for i in range(len(examples))]    tokenized_examples = tokenizer(            questions,            contexts,            stride=doc_stride,            max_seq_len=max_seq_length)    # 对于验证,不需要计算开始和结束位置    for i, tokenized_example in enumerate(tokenized_examples):        # 抓取样本对应的序列(知道上下文是什么,问题是什么)        sequence_ids = tokenized_example['token_type_ids']        # 一个样本可以有多个span,这是包含此文本span的示例的索引。        sample_index = tokenized_example['overflow_to_sample']        tokenized_examples[i]["example_id"] = examples[sample_index]['id']        # 将不属于上下文的偏移量映射设置为None,这样就可以很容易地确定token位置是否属于上下文        tokenized_examples[i]["offset_mapping"] = [                (o if sequence_ids[k] == 1 else None)                for k, o in enumerate(tokenized_example["offset_mapping"])            ]    return tokenized_examples

   

读取dureaderrobust的dev集合,把json数据处理成list的形式,list里面每一项以键值对的形式存放。

In [ ]

dev_ds = load_dataset(task_name, splits='dev')print(dev_ds[:2])

   In [ ]

dev_ds.map(prepare_validation_features, batched=True)

   

2.2 模型构建

阅读理解本质是一个答案抽取任务,PaddleNLP对于各种预训练模型已经内置了对于下游任务-答案抽取的Fine-tune网络。

答案抽取任务的本质就是根据输入的问题和文章,预测答案在文章中的起始位置和结束位置。基于BERT实现机器阅读理解 - 创想鸟        

机器阅读理解模型的构成需要实现BertEmbeddings,TransformerEncoderLayer,BertPooler,FC层这四部分。由于TransformerEncoderLayer已经由paddle实现了,所以下面介绍一下其他部分的实现:

BertEmbeddings表示的是bert的嵌入层,包括word embedding,position embedding和token_type embedding三部分。 TransformerEncoderLayer表示的是Transformer编码层,这部分paddle已经实现,Transformer编码器层由两个子层组成:多头自注意力机制和前馈神经网络。BERT主体是transformer的编码层结构,transformer的具体介绍请参考transformer BertPooler层的输入是transformer最后一层的输出,取出每一句的第一个单词,做全连接和激活。 FC层:是bert做下有任务的一般做法,把bert得到的句向量或者单词向量加入全连接,用于分类等下游任务。

2.2.1 BertEmbeddings实现

词嵌入张量: Token Embedding,例如 [CLS] dog 等,通过训练学习得到。 语句分块张量: token_type_embeddings(或者Segment Embedding),用于区分每一个单词属于句子 A 还是句子 B,如果只输入一个句子就只使用 EA,通过训练学习得到。 位置编码张量: position_embeddings,编码单词出现的位置,与 Transformer 使用固定的公式计算不同,BERT 的 Position Embedding 也是通过学习得到的,在 BERT 中,假设句子最长为 512。 最终的embedding向量是将上述的3个向量直接做加和的结果。 基于BERT实现机器阅读理解 - 创想鸟        

In [ ]

class BertEmbeddings(Layer):    def __init__(self,                 vocab_size,                 hidden_size=768,                 hidden_dropout_prob=0.1,                 max_position_embeddings=512,                 type_vocab_size=16):        super(BertEmbeddings, self).__init__()        # Token Embedding        self.word_embeddings = nn.Embedding(vocab_size, hidden_size)        # position embedding        self.position_embeddings = nn.Embedding(max_position_embeddings,                                                hidden_size)        # token_type embedding        self.token_type_embeddings = nn.Embedding(type_vocab_size, hidden_size)        # 层归一化        self.layer_norm = nn.LayerNorm(hidden_size)        # dropout层        self.dropout = nn.Dropout(hidden_dropout_prob)    def forward(self, input_ids, token_type_ids=None, position_ids=None):        if position_ids is None:            ones = paddle.ones_like(input_ids, dtype="int64")            seq_length = paddle.cumsum(ones, axis=-1)            position_ids = seq_length - ones            position_ids.stop_gradient = True        if token_type_ids is None:            token_type_ids = paddle.zeros_like(input_ids, dtype="int64")        # token embedding        input_embedings = self.word_embeddings(input_ids)        # position embedding        position_embeddings = self.position_embeddings(position_ids)        # token_type embedding        token_type_embeddings = self.token_type_embeddings(token_type_ids)        # token embedding, position embedding和token type embedding进行拼接        embeddings = input_embedings + position_embeddings + token_type_embeddings        # 层归一化        embeddings = self.layer_norm(embeddings)        # dropout操作        embeddings = self.dropout(embeddings)        return embeddings

   

2.2.2 BertPooler实现

BertPooler: 只是取每个 sequence 的第一个token,即原本的输入大小为 [batch_size, seq_length, hidden_size],变换后大小为 [batch_size, hidden_size],去掉了 seq_length 维度,相当于是每个 sequence 都只用第一个 token 来表示。然后接上一层 hidden_size 大小的线性映射即可,激励函数为 nn.tanh。

In [ ]

class BertPooler(Layer):    def __init__(self, hidden_size):        super(BertPooler, self).__init__()        # 全连接层        self.dense = nn.Linear(hidden_size, hidden_size)        # tanh激活函数        self.activation = nn.Tanh()    def forward(self, hidden_states):        # 把隐藏状态的第0个token的向量取出来        first_token_tensor = hidden_states[:, 0]        # 全连接        pooled_output = self.dense(first_token_tensor)        # tanh激活函数        pooled_output = self.activation(pooled_output)        return pooled_output

   

2.2.3 BertModel的实现

PretrainedModel: 负责存储模型的配置,并处理加载/下载/保存模型的方法以及一些通用于所有模型的方法:(i)调整输入embedding的大小,(ii)修剪自我注意头中的头。 bert-base-chinese预训练模型各参数的含义

“bert-base-chinese”: { “vocab_size”: 21128, #词典中词数 “hidden_size”: 768, #隐藏单元数 “num_hidden_layers”: 12, #隐藏层数 “num_attention_heads”: 12, #每个隐藏层中的attention head数 “intermediate_size”: 3072, #升维维度 “hidden_act”: “gelu”, #激活函数 “hidden_dropout_prob”: 0.1, #隐藏层dropout概率 “attention_probs_dropout_prob”: 0.1, #乘法attention时,softmax后dropout概率 “max_position_embeddings”: 512, # 一个大于seq_length的参数,用于生成position_embedding “type_vocab_size”: 2, #segment_ids类别 [0,1] “initializer_range”: 0.02, #初始化范围 “pad_token_id”: 0, #对齐的值 }

In [ ]

from paddlenlp.transformers import PretrainedModel

   

**注:**BertPretrainedModel是一个预处理bert的抽象类,它提供了bert相关的模型配置,model_config_file, resource_files_names, pretrained_resource_files_map,pretrained_init_configuration, base_model_prefix。用于加载和下载预训练模型。

In [ ]

class BertPretrainedModel(PretrainedModel):    model_config_file = "model_config.json"    pretrained_init_configuration = {        "bert-base-chinese": {            "vocab_size": 21128,            "hidden_size": 768,            "num_hidden_layers": 12,            "num_attention_heads": 12,            "intermediate_size": 3072,            "hidden_act": "gelu",            "hidden_dropout_prob": 0.1,            "attention_probs_dropout_prob": 0.1,            "max_position_embeddings": 512,            "type_vocab_size": 2,            "initializer_range": 0.02,            "pad_token_id": 0,        },    }    resource_files_names = {"model_state": "model_state.pdparams"}    pretrained_resource_files_map = {        "model_state": {            "bert-base-chinese":            "http://paddlenlp.bj.bcebos.com/models/transformers/bert/bert-base-chinese.pdparams",               }    }    base_model_prefix = "bert"    def init_weights(self, layer):        """ Initialization hook """        if isinstance(layer, (nn.Linear, nn.Embedding)):            if isinstance(layer.weight, paddle.Tensor):                layer.weight.set_value(                    paddle.tensor.normal(                        mean=0.0,                        std=self.initializer_range                        if hasattr(self, "initializer_range") else                        self.bert.config["initializer_range"],                        shape=layer.weight.shape))        elif isinstance(layer, nn.LayerNorm):            layer._epsilon = 1e-12

   In [ ]

from paddlenlp.transformers import register_base_model

   

register_base_model: 为修饰类的基类添加“base_model_class”属性,在相同体系结构的派生类中表示基模型类。register_base_model在BertPretrainedModel的子类上使用。 BertModel: BERT模型的具体实现。

In [ ]

@register_base_modelclass BertModel(BertPretrainedModel):    def __init__(self,                 vocab_size,                 hidden_size=768,                 num_hidden_layers=12,                 num_attention_heads=12,                 intermediate_size=3072,                 hidden_act="gelu",                 hidden_dropout_prob=0.1,                 attention_probs_dropout_prob=0.1,                 max_position_embeddings=512,                 type_vocab_size=16,                 initializer_range=0.02,                 pad_token_id=0):        super(BertModel, self).__init__()        self.pad_token_id = pad_token_id        self.initializer_range = initializer_range        # BertEmbeddings层        self.embeddings = BertEmbeddings(            vocab_size, hidden_size, hidden_dropout_prob,            max_position_embeddings, type_vocab_size)        # TransformerEncoderLayer层 12个multi-head        encoder_layer = nn.TransformerEncoderLayer(            hidden_size,            num_attention_heads,            intermediate_size,            dropout=hidden_dropout_prob,            activation=hidden_act,            attn_dropout=attention_probs_dropout_prob,            act_dropout=0)        # TransformerEncoder层 12层encoder_layer        self.encoder = nn.TransformerEncoder(encoder_layer, num_hidden_layers)        # BertPooler层        self.pooler = BertPooler(hidden_size)        # 模型参数初始化        self.apply(self.init_weights)    def forward(self,input_ids,token_type_ids=None,position_ids=None,attention_mask=None):        if attention_mask is None:            attention_mask = paddle.unsqueeze(                (input_ids == self.pad_token_id                 ).astype(self.pooler.dense.weight.dtype) * -1e9,                axis=[1, 2])        # input_id,position_id,token_type_id embedding        embedding_output = self.embeddings(            input_ids=input_ids,            position_ids=position_ids,            token_type_ids=token_type_ids)        # 调用TransformerEncoder层 12层encoder_layer        encoder_outputs = self.encoder(embedding_output, attention_mask)        sequence_output = encoder_outputs        # 调用BertPooler        pooled_output = self.pooler(sequence_output)        return sequence_output, pooled_output

   

2.2.4 BertForQuestionAnswering的实现

BertForQuestionAnswering需要继承BertPretrainedModel; 实例化BERT; 在BERT后面加入一层全连接,连接的神经元的数目2,表示的是答案开始位置和答案结束位置输出。

In [ ]

class BertForQuestionAnswering(BertPretrainedModel):    def __init__(self, bert, dropout=None):        super(BertForQuestionAnswering, self).__init__()        # 加载bert配置        self.bert = bert          # 全连接层        self.classifier = nn.Linear(self.bert.config["hidden_size"], 2)        self.apply(self.init_weights)    def forward(self, input_ids, token_type_ids=None):        # Bert接收输入        sequence_output, _ = self.bert(            input_ids,            token_type_ids=token_type_ids,            position_ids=None,            attention_mask=None)        # 分类        logits = self.classifier(sequence_output)        logits = paddle.transpose(logits, perm=[2, 0, 1])        start_logits, end_logits = paddle.unstack(x=logits, axis=0)        return start_logits, end_logits

   

2.2.5 模型的损失函数设计

由于BertForQuestionAnswering模型对将BertModel的sequence_output拆开成start_logits和end_logits进行输出,所以阅读理解任务的loss也由start_loss和end_loss组成,我们需要自己定义损失函数。对于答案其实位置和结束位置的预测可以分别成两个分类任务。所以设计的损失函数如下: 基于BERT实现机器阅读理解 - 创想鸟        

In [ ]

class CrossEntropyLossForSQuAD(paddle.nn.Layer):    def __init__(self):        super(CrossEntropyLossForSQuAD, self).__init__()    def forward(self, y, label):        # 预测值的开始位置和结束位置        start_logits, end_logits = y        # ground truth的开始位置和结束位置        start_position, end_position = label        start_position = paddle.unsqueeze(start_position, axis=-1)        end_position = paddle.unsqueeze(end_position, axis=-1)        # 计算start loss        start_loss = paddle.nn.functional.softmax_with_cross_entropy(            logits=start_logits, label=start_position, soft_label=False)        start_loss = paddle.mean(start_loss)        # 计算end loss        end_loss = paddle.nn.functional.softmax_with_cross_entropy(            logits=end_logits, label=end_position, soft_label=False)        end_loss = paddle.mean(end_loss)        # 求最终的损失        loss = (start_loss + end_loss) / 2        return loss

   

2.2.6 LinearDecayWithWarmup的实现

LinearDecayWithWarmup:创建一个学习率调度程序,线性地提高学习率,从0到给定的“学习率”,在这个热身期后学习率,从基本学习率线性下降到0。

In [ ]

# 判断number是否是整型def is_integer(number):    if sys.version > '3':        return isinstance(number, int)    return isinstance(number, (int, long))class LinearDecayWithWarmup(LambdaDecay):    def __init__(self,                 learning_rate,                 total_steps,                 warmup,                 last_epoch=-1,                 verbose=False):        warmup_steps = warmup if is_integer(warmup) else int(            math.floor(warmup * total_steps))        def lr_lambda(current_step):            # current_step小于warmup_steps的时候,学习率逐渐增加            if current_step < warmup_steps:                return float(current_step) / float(max(1, warmup_steps))            # 学习率随着current_step的增加而降低            return max(0.0,                       float(total_steps - current_step) /                       float(max(1, total_steps - warmup_steps)))        super(LinearDecayWithWarmup, self).__init__(learning_rate, lr_lambda,                                                    last_epoch, verbose)

   

2.3 训练配置

训练配置包括实例化BertForQuestionAnswering,损失函数,优化器参数设置,优化器,数据加载等。

实例化BertForQuestionAnswering模型In [ ]

# 设置GPU模式paddle.set_device("gpu")# 实例化BertForQuestionAnswering模型model = BertForQuestionAnswering.from_pretrained(model_name_or_path)

   实例化损失函数In [ ]

criterion = CrossEntropyLossForSQuAD()

   模型超参数设置

1.需要设置batch_size的大小,训练的轮次num_train_epochs,设置优化器的参数learning_rate,weight_decayadam_epsilon这些。

2.warmup_proportion表示,慢热学习的比例。比如warmup_proportion=0.1,在前10%的steps中,lr从0线性增加到 init_learning_rate,这个阶段又叫 warmup,然后,lr又从 init_learning_rate 线性衰减到0(完成所有steps)。可以避免较早的对mini-batch过拟合,即较早的进入不好的局部最优而无法跳出;保持模型深层的稳定性。

In [ ]

# batch size的大小batch_size=16 # epoch的大小num_train_epochs=1# warmup的比例warmup_proportion=0.1# 优化器的配置learning_rate=3e-5 weight_decay=0.01adam_epsilon=1e-8

   

加载数据

In [ ]

train_batch_sampler = paddle.io.DistributedBatchSampler(train_ds, batch_size=batch_size, shuffle=True)train_batchify_fn = lambda samples, fn=Dict({            "input_ids": Pad(axis=0, pad_val=tokenizer.pad_token_id),            "token_type_ids": Pad(axis=0, pad_val=tokenizer.pad_token_type_id),            "start_positions": Stack(dtype="int64"),            "end_positions": Stack(dtype="int64")        }): fn(samples)train_data_loader = DataLoader(            dataset=train_ds,            batch_sampler=train_batch_sampler,            collate_fn=train_batchify_fn,            return_list=True)

   

定义优化器

AdamW使用在hugging face版的transformer中,BERT,XLNET,ELECTRA等主流的NLP模型,都是用了AdamW优化器,详细介绍请参考链接AdamWIn [ ]

num_training_steps = len(train_data_loader) * num_train_epochslr_scheduler = LinearDecayWithWarmup(            learning_rate, num_training_steps, warmup_proportion)decay_params = [            p.name for n, p in model.named_parameters()            if not any(nd in n for nd in ["bias", "norm"])        ]# 优化器optimizer = paddle.optimizer.AdamW(            learning_rate=lr_scheduler,            epsilon=adam_epsilon,            parameters=model.parameters(),            weight_decay=weight_decay,            apply_decay_param_fun=lambda x: x in decay_params)

   

2.4 模型训练

模型训练的过程通常有以下步骤:

从dataloader中取出一个batch data; 将batch data喂给model,做前向计算; 将前向计算结果传给损失函数,计算loss; loss反向回传,更新梯度。重复以上步骤。

基于BERT实现机器阅读理解 - 创想鸟        

In [ ]

global_step = 0logging_steps=100save_steps=1000import timetic_train = time.time()for epoch in range(num_train_epochs):    # 遍历每个batch的数据    for step, batch in enumerate(train_data_loader):        global_step += 1        input_ids, token_type_ids, start_positions, end_positions = batch        # 调用模型        logits = model(input_ids=input_ids, token_type_ids=token_type_ids)        # 求损失        loss = criterion(logits, (start_positions, end_positions))        # 打印日志        if global_step % logging_steps == 0:            print("global step %d, epoch: %d, batch: %d, loss: %f, speed: %.2f step/s"                        % (global_step, epoch, step, loss,                           logging_steps / (time.time() - tic_train)))            tic_train = time.time()        # 反向传播,更新权重,清除梯度        loss.backward()        optimizer.step()        lr_scheduler.step()        optimizer.clear_grad()

   

2.5 模型保存

训练完成后,可以将模型参数保存到磁盘,用于模型推理或继续训练。

In [ ]

output_path='./chekpoint/dureader_robust/'# 如果文件夹不存在就创建文件夹if not os.path.exists(output_path):    os.makedirs(output_path)# 保存模型model.save_pretrained(output_path)# 保存tokenizertokenizer.save_pretrained(output_path)print('Saving checkpoint to:', output_path)

   

2.6 模型评估

基于BERT实现机器阅读理解 - 创想鸟        

每训练一个epoch时,程序通过evaluate()调用paddlenlp.metrics.squad中的squad_evaluate(), compute_prediction()评估当前模型训练的效果,其中:

compute_prediction()用于生成模型的预测结果;squad_evaluate()用于返回评价指标。模型的评估使用了rouge-L,ROUGE-L 中的 L 指最长公共子序列 (longest common subsequence, LCS),ROUGE-L 计算的时候使用了机器答案C和参考答案S的最长公共子序列,计算公式如下:

基于BERT实现机器阅读理解 - 创想鸟        

举个例子:

C : 热带.

S :热带气候.

基于BERT实现机器阅读理解 - 创想鸟        

In [ ]

from paddlenlp.metrics.squad import squad_evaluate, compute_prediction

   In [ ]

dev_batch_sampler = paddle.io.BatchSampler(            dev_ds, batch_size=batch_size, shuffle=False)dev_batchify_fn = lambda samples, fn=Dict({            "input_ids": Pad(axis=0, pad_val=tokenizer.pad_token_id),            "token_type_ids": Pad(axis=0, pad_val=tokenizer.pad_token_type_id)        }): fn(samples)dev_data_loader = DataLoader(            dataset=dev_ds,            batch_sampler=dev_batch_sampler,            collate_fn=dev_batchify_fn,            return_list=True)

   In [ ]

@paddle.no_grad()def evaluate(model, data_loader):    # 切换为预测模式    model.eval()    n_best_size=20    max_answer_length=30    all_start_logits = []    all_end_logits = []    tic_eval = time.time()    # 遍历每个batch数据    for batch in data_loader:        input_ids, token_type_ids = batch        # 调用模型,得到模型的输出        start_logits_tensor, end_logits_tensor = model(input_ids,                                                       token_type_ids)        # 解析模型的输出        for idx in range(start_logits_tensor.shape[0]):            if len(all_start_logits) % 1000 == 0 and len(all_start_logits):                print("Processing example: %d" % len(all_start_logits))                print('time per 1000:', time.time() - tic_eval)                tic_eval = time.time()            all_start_logits.append(start_logits_tensor.numpy()[idx])            all_end_logits.append(end_logits_tensor.numpy()[idx])    # 预测    all_predictions, _, _ = compute_prediction(        data_loader.dataset.data, data_loader.dataset.new_data,        (all_start_logits, all_end_logits), False, n_best_size,        max_answer_length)    # 预测的结果写入到文件    with open('prediction.json', "w", encoding='utf-8') as writer:        writer.write(            json.dumps(                all_predictions, ensure_ascii=False, indent=4) + "n")    # 评估结果    squad_evaluate(        examples=data_loader.dataset.data,        preds=all_predictions,        is_whitespace_splited=False)    # 切换为训练模式    model.train()

   In [ ]

evaluate(model, dev_data_loader)

   

总共评估的样本数为1417(对应上述的total和HasAns_total),每1000个样本评估的时间8.143768072128296秒,上述的结果F1和HasAns_f1的值为84.31761822783372,exact和HasAns_exact计算结果也一样,这是因为他们的计算方式是完全一样的,F1对应的是Rouge-L的FLCSF_{LCS}F-LCS。

3. 实验总结

在这篇文章中,我们选择了BERT问答模型,在DuReader Robust数据集上实现了阅读理解。通过本实验,同学们不但回顾了《人工智能导论:模型与算法》- 6.4 循环神经网络这一节中介绍的相关原理,加深了对bert模型的理解,并且学习了通过飞桨深度学习开源框架实现本实验模型的机器阅读理解任务。大家可以在此实验的基础上,尝试开发自己感兴趣的机器阅读理解任务。

以上就是基于BERT实现机器阅读理解的详细内容,更多请关注创想鸟其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/63092.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2025年11月11日 10:34:05
下一篇 2025年11月11日 10:48:05

相关推荐

  • Uniapp 中如何不拉伸不裁剪地展示图片?

    灵活展示图片:如何不拉伸不裁剪 在界面设计中,常常需要以原尺寸展示用户上传的图片。本文将介绍一种在 uniapp 框架中实现该功能的简单方法。 对于不同尺寸的图片,可以采用以下处理方式: 极端宽高比:撑满屏幕宽度或高度,再等比缩放居中。非极端宽高比:居中显示,若能撑满则撑满。 然而,如果需要不拉伸不…

    2025年12月24日
    400
  • 如何让小说网站控制台显示乱码,同时网页内容正常显示?

    如何在不影响用户界面的情况下实现控制台乱码? 当在小说网站上下载小说时,大家可能会遇到一个问题:网站上的文本在网页内正常显示,但是在控制台中却是乱码。如何实现此类操作,从而在不影响用户界面(UI)的情况下保持控制台乱码呢? 答案在于使用自定义字体。网站可以通过在服务器端配置自定义字体,并通过在客户端…

    2025年12月24日
    800
  • 如何在地图上轻松创建气泡信息框?

    地图上气泡信息框的巧妙生成 地图上气泡信息框是一种常用的交互功能,它简便易用,能够为用户提供额外信息。本文将探讨如何借助地图库的功能轻松创建这一功能。 利用地图库的原生功能 大多数地图库,如高德地图,都提供了现成的信息窗体和右键菜单功能。这些功能可以通过以下途径实现: 高德地图 JS API 参考文…

    2025年12月24日
    400
  • 如何使用 scroll-behavior 属性实现元素scrollLeft变化时的平滑动画?

    如何实现元素scrollleft变化时的平滑动画效果? 在许多网页应用中,滚动容器的水平滚动条(scrollleft)需要频繁使用。为了让滚动动作更加自然,你希望给scrollleft的变化添加动画效果。 解决方案:scroll-behavior 属性 要实现scrollleft变化时的平滑动画效果…

    2025年12月24日
    000
  • 如何为滚动元素添加平滑过渡,使滚动条滑动时更自然流畅?

    给滚动元素平滑过渡 如何在滚动条属性(scrollleft)发生改变时为元素添加平滑的过渡效果? 解决方案:scroll-behavior 属性 为滚动容器设置 scroll-behavior 属性可以实现平滑滚动。 html 代码: click the button to slide right!…

    2025年12月24日
    500
  • 为什么设置 `overflow: hidden` 会导致 `inline-block` 元素错位?

    overflow 导致 inline-block 元素错位解析 当多个 inline-block 元素并列排列时,可能会出现错位显示的问题。这通常是由于其中一个元素设置了 overflow 属性引起的。 问题现象 在不设置 overflow 属性时,元素按预期显示在同一水平线上: 不设置 overf…

    2025年12月24日 好文分享
    400
  • 如何选择元素个数不固定的指定类名子元素?

    灵活选择元素个数不固定的指定类名子元素 在网页布局中,有时需要选择特定类名的子元素,但这些元素的数量并不固定。例如,下面这段 html 代码中,activebar 和 item 元素的数量均不固定: *n *n 如果需要选择第一个 item元素,可以使用 css 选择器 :nth-child()。该…

    2025年12月24日
    200
  • 使用 SVG 如何实现自定义宽度、间距和半径的虚线边框?

    使用 svg 实现自定义虚线边框 如何实现一个具有自定义宽度、间距和半径的虚线边框是一个常见的前端开发问题。传统的解决方案通常涉及使用 border-image 引入切片图片,但是这种方法存在引入外部资源、性能低下的缺点。 为了避免上述问题,可以使用 svg(可缩放矢量图形)来创建纯代码实现。一种方…

    2025年12月24日
    100
  • 微信小程序文本省略后如何避免背景色溢出?

    去掉单行文本溢出多余背景色 在编写微信小程序时,如果希望文本超出宽度后省略显示并在末尾显示省略号,但同时还需要文本带有背景色,可能会遇到如下问题:文本末尾出现多余的背景色块。这是因为文本本身超出部分被省略并用省略号代替,但其背景色依然存在。 要解决这个问题,可以采用以下方法: 给 text 元素添加…

    2025年12月24日
    000
  • 如何让“元素跟随文本高度,而不是撑高父容器?

    如何让 元素跟随文本高度,而不是撑高父容器 在页面布局中,经常遇到父容器高度被子元素撑开的问题。在图例所示的案例中,父容器被较高的图片撑开,而文本的高度没有被考虑。本问答将提供纯css解决方案,让图片跟随文本高度,确保父容器的高度不会被图片影响。 解决方法 为了解决这个问题,需要将图片从文档流中脱离…

    2025年12月24日
    000
  • Flex 布局左右同高怎么实现?

    flex布局左右同高 在flex布局中,左右布局的元素高度不一致时,想要让边框延伸到最大高度,可以采用以下方法: 基于当前结构的方法: 给.rht和.lft盒子添加: .rht { height: min-content;} 这样可以使弹性盒子被子盒子内容撑开。 使用javascript获取.rht…

    2025年12月24日
    000
  • inline-block元素错位了,是为什么?

    inline-block元素错位背后的原因 inline-block元素是一种特殊类型的块级元素,它可以与其他元素行内排列。但是,在某些情况下,inline-block元素可能会出现错位显示的问题。 错位的原因 当inline-block元素设置了overflow:hidden属性时,它会影响元素的…

    2025年12月24日
    000
  • 为什么 CSS mask 属性未请求指定图片?

    解决 css mask 属性未请求图片的问题 在使用 css mask 属性时,指定了图片地址,但网络面板显示未请求获取该图片,这可能是由于浏览器兼容性问题造成的。 问题 如下代码所示: 立即学习“前端免费学习笔记(深入)”; icon [data-icon=”cloud”] { –icon-cl…

    2025年12月24日
    200
  • 为什么使用 inline-block 元素时会错位?

    inline-block 元素错位成因剖析 在使用 inline-block 元素时,可能会遇到它们错位显示的问题。如代码 demo 所示,当设置了 overflow 属性时,a 标签就会错位下沉,而未设置时却不会。 问题根源: overflow:hidden 属性影响了 inline-block …

    2025年12月24日
    000
  • 如何去除带有背景色的文本单行溢出时的多余背景色?

    带背景色的文字单行溢出处理:去除多余的背景色 当一个带有背景色的文本因单行溢出而被省略时,可能会出现最后一个背景色块多余的情况。针对这种情况,可以通过以下方式进行处理: 在示例代码中,问题在于当文本溢出时,overflow: hidden 属性会导致所有文本元素(包括最后一个)都隐藏。为了解决该问题…

    2025年12月24日
    000
  • 如何利用 CSS 选中激活标签并影响相邻元素的样式?

    如何利用 css 选中激活标签并影响相邻元素? 为了实现激活标签影响相邻元素的样式需求,可以通过 :has 选择器来实现。以下是如何具体操作: 对于激活标签相邻后的元素,可以在 css 中使用以下代码进行设置: li:has(+li.active) { border-radius: 0 0 10px…

    2025年12月24日
    100
  • 如何解决 CSS 中文本溢出时背景色也溢出的问题?

    文字单行溢出省略号时,去掉多余背景色的方法 在使用 css 中的 text-overflow: ellipsis 属性时,如果文本内容过长导致一行溢出,且文本带有背景色,溢出的部分也会保留背景色。但如果想要去掉最后多余的背景色,可以采用以下方法: 给 text 元素添加一个 display: inl…

    2025年12月24日
    200
  • 如何模拟Windows 10 设置界面中的鼠标悬浮放大效果?

    win10设置界面的鼠标移动显示周边的样式(探照灯效果)的实现方式 在windows设置界面的鼠标悬浮效果中,光标周围会显示一个放大区域。在前端开发中,可以通过多种方式实现类似的效果。 使用css 使用css的transform和box-shadow属性。通过将transform: scale(1.…

    2025年12月24日
    200
  • 为什么我的 Safari 自定义样式表在百度页面上失效了?

    为什么在 Safari 中自定义样式表未能正常工作? 在 Safari 的偏好设置中设置自定义样式表后,您对其进行测试却发现效果不同。在您自己的网页中,样式有效,而在百度页面中却失效。 造成这种情况的原因是,第一个访问的项目使用了文件协议,可以访问本地目录中的图片文件。而第二个访问的百度使用了 ht…

    2025年12月24日
    000
  • 如何用前端实现 Windows 10 设置界面的鼠标移动探照灯效果?

    如何在前端实现 Windows 10 设置界面中的鼠标移动探照灯效果 想要在前端开发中实现 Windows 10 设置界面中类似的鼠标移动探照灯效果,可以通过以下途径: CSS 解决方案 DEMO 1: Windows 10 网格悬停效果:https://codepen.io/tr4553r7/pe…

    2025年12月24日
    000

发表回复

登录后才能评论
关注微信