【古诗生成AI实战】之三——任务加载器与预处理器

  本章内容属于数据处理阶段,将分别介绍任务加载器task和预处理器processor

在这里插入图片描述

[1] 数据集

  在深入探讨数据处理的具体步骤之前,让我们先了解一下我们将要使用的数据集的形式。

  本项目采用的是七绝数据集,总计83072条古诗,其形式如下:

偈颂一百二十三首  其二○:布袋头开天地大,翠岩眉毛在不在。一声鸿鴈海门秋,行人已在青山外。
偈颂一百二十三首  其二八:释迦老子弄精魂,死了依前似不曾。一树落花春过后,远山无限碧层层。
偈颂一百二十三首  其三○:一夜江风搅玉尘,孤峰不白转精神。从空放下从空看,彻骨寒来有几人。
...

  实际上我们只需要古诗的正文部分:

布袋头开天地大,翠岩眉毛在不在。一声鸿鴈海门秋,行人已在青山外。
释迦老子弄精魂,死了依前似不曾。一树落花春过后,远山无限碧层层。
一夜江风搅玉尘,孤峰不白转精神。从空放下从空看,彻骨寒来有几人。
...

  除了七绝数据集之外,我们的项目还包括其他类型的古诗数据集,如七律、五绝和五律。虽然在本项目的实战应用中我们并未使用这些数据集,但它们对于对不同古诗形式感兴趣的学习者来说是极好的实践资源。如果你有兴趣探索更多古诗风格,不妨尝试一下这些数据集。

  需要注意的一点是,这些数据集都是以繁体字呈现的。因此,在处理这些数据之前,需要将繁体字转换为简体字。为此,我们可以利用opencc库,这是一个专门用于中文繁简转换的工具。使用opencc,你可以轻松地完成繁简之间的转换,确保数据适合模型训练和文本生成。

  更多关于opencc库的信息和使用文档,可以参考其在GitHub上的官方页面:https://github.com/BYVoid/OpenCC。

[2] 任务加载器task

  在我们详细了解了数据集的格式之后,接下来我们将探讨项目中非常关键的组件之一:任务加载器task

  那么,什么是任务加载器task呢?简单来说,这是我自定义的一个数据加载类Task,它的主要职责是从各种格式的数据文件(如txt、csv、json)中加载数据。考虑到不同数据集的格式可能有所不同,Task类还负责提取文件中的指定部分数据(例如,仅从csv文件的某些列中提取数据),并进行后续的文本清洗工作。

  你可能会问,为什么我们要在项目中专门编写任务加载器task呢?其主要原因是为了增强项目代码的可扩展性,使得我们能够在不同任务和不同数据集之间灵活切换。这种设计理念在实际应用中极为重要。

当然,当读者亲自查看实战项目的代码后,就会直观感受到规范和高效代码的优势。

  下面,让我们来看一下实战项目中针对七绝古诗数据集定义的任务加载器QiJueTask的伪代码:

class QiJueTask(DataTask):
    """Processor for the Example data set."""

    def get_train_examples(self, data_path):
        return self._create_examples(data_path, "train")

    def get_dev_examples(self, data_path):
        return []

    def get_test_examples(self, data_path):
        return []

    def get_single_examples(self, text):
        # 定制id
        guid = "%s-%s" % ('test', 0)
        example = InputExample(guid=guid, text=text)
        return [example]

    def get_labels(self):
        return []

    def check_text(self, text):
        """检查预处理后文本"""
        if any(char in text for char in ['_', '(', '(', '《', '[', '□', 'C', ' ̄', 'w', 'p', '{', '}']):
            # logging.info(f'文本【{text}】中包含特殊字符,请修改预处理逻辑')
            return False
        if len(text) != 32:
            logging.info(f'文本【{text}】长度不为32,请修改预处理逻辑')
            return False
        return True

    def preprocess_text(self, text):
        """文本预处理"""
        text = text.replace(' ', '')
        return text


    def _create_examples(self, path: str, set_type: str) -> List[InputExample]:
        logging.info(f"加载{set_type}数据集中...")
        examples = []

        with open(path, "r", encoding='utf-8', ) as f:
            for i, line in tqdm(enumerate(f.readlines())):
                try:
                    title, content = line.strip().split(':')
                    content = self.preprocess_text(content)
                    if self.check_text(content):
                        # 定制id
                        guid = "%s-%s" % (set_type, i)
                        example = InputExample(guid=guid, text=content)
                        examples.append(example)
                except ValueError as e:
                    pass
        return examples

  在我们的代码中,我们定义了一个名为QiJueTask的类,这个类的核心作用是处理不同类型的数据集,比如训练集、验证集和测试集,以及对单个文本样本的特定处理。这个类包含总计8个方法,下面是对这些方法的简要介绍:

  · get_xxx_examples(self, data_path): 这些方法负责加载不同类型的训练数据集。它们通过调用内部方法_create_examples来完成这一任务,并传递数据路径和特定的标记(如"train""dev""test")来区分训练、验证和测试数据。

  · get_single_examples(self, text): 这个方法用于为单个文本样本创建InputExample对象。这在进行单个古诗文本的预测时显得非常重要。

  注意:InputExample类是一个用于存放文本数据的容器,我将在后面详细介绍。

  · get_labels(self): 这个方法返回一个空列表,因为在我们的任务中,标签是从输入文本中直接得到,而不是预先定义的。

  · check_text(self, text): 这个方法用于检查经过预处理的文本。虽然我们对数据进行了清洗,但有时仍可能存在一些遗漏,因此这个方法提供了一个机会来进行额外的过滤和检查。

  · preprocess_text(self, text): 这是文本预处理的方法,当前的主要操作是去除文本中的空格。

  · _create_examples(self, path: str, set_type: str): 这是一个内部方法,用于从数据文件中读取数据,并将其转换为InputExample对象的列表。它接受数据路径和数据集的类型(训练、验证或测试)作为参数。

  总的来说,任务加载器task的主要作用就是从数据集文件中读取文本、清洗文本、处理文本,然后将处理后的文本存放到List[InputExample]中。下面,我们将详细探讨InputExample类的结构和作用。

class InputExample(object):
    """原始样本类"""

    def __init__(self, guid, text, word_label=None, index_label=None, encode_label=None):
        """
        Create a new InputExample.

        :param guid: a unique textual identifier
        :param text: the sequence of text
        :param word_label: 单词形式的标签 如['sport','tech']
        :param index_label: 索引形式的标签 从总标签集里面取出word_label对应的索引 如[2,0]
        :param encode_label: 独热编码形式的标签 根据总标签集 把index_label转化成独热标签 如[0,1,1,0]
        """
        self.guid = guid
        self.text = text
        self.word_label = word_label
        self.index_label = index_label
        self.encode_label = encode_label

  为了确保我们的代码具有高度的拓展性,InputExample类被设计得相当灵活,其属性丰富多样。这种设计不仅适用于我们当前的古诗生成任务,还可以方便地应用于其他类型的文本任务,比如文本分类任务,或是适配transformers库中的模型(例如Bert、GPT)。

  那么,我们的任务加载器task是如何实现在不同任务和不同数据集之间灵活切换的能力呢?

  让我们通过一个例子来说明这一点。假设目前我们手头上有四个不同格式的数据集。为了处理这些不同的数据集,我们可以为每个数据集定义一个特定的任务加载器task。通过这种方式,当需要切换数据集时,我们只需简单地修改数据集的名称,就可以迅速切换到不同的数据集,如下所示:

TASKS = {
    "qijue": QiJueTask,
    "qilv": QiLvTask,
    "wujue": WuJueTask,
    "wulv": WuLvTask,
}

  之后想加载不同数据集的训练集,你只需要使用如下代码:

task = TASK['your_dataset_name']
train_data = task.get_train_examples('your_dataset_path')

[3] 预处理器processor

  让我们来探讨预处理器processor的作用。processor的主要职责是基于数据集构建词典,并向外提供分词器tokenizer的功能。

  对于那些有使用transformers库经验的人来说,他们会知道,在使用预训练模型处理文本数据之前,需要通过transformers.PreTrainedTokenizer将文本转换为向量形式。

  考虑到本项目代码可能需要对transformers库中的预训练模型进行拓展使用,我们决定将分词器集成到预处理器processor中。这样做的好处是,如果我们选择使用transformers库的预训练模型,就无需手动构建词典和分词器;但如果不使用这些预训练模型,我们则需要自己从数据集中读取数据并构建词典,以便将文本转化为向量。进一步地,我们可能还需要对嵌入层进行预训练的词向量初始化。

‘嵌入’、‘word2vec’是NLP领域非常基础的概念。以下两篇文章介绍的十分详细:
1、【文本分类】深入理解embedding层的模型、结构与文本表示:介绍了嵌入的相关理论基础;
2、一文了解Word2vec 阐述训练流程:介绍了嵌入的训练方式。

  现在,我们将详细讲解预处理器processor中词典的构建方式:

    def _build_vocab(cls, data: List[InputExample]):
        """构建词表"""

        if os.path.exists(cls.vocab_path):
            logging.info('加载词表中...')
            with open(cls.vocab_path, 'rb') as fh:
                words = pickle.load(fh)
            logging.info(f'词表大小={len(words)}')
            return words

        logging.info('构建词表中...')
        texts = []
        for example in tqdm(data):
            texts.append(cls.start_token + example.text + cls.end_token)

        all_words = [word for text in texts for word in text]
        counter = collections.Counter(all_words)
        words = sorted(counter.keys(), key=lambda x: counter[x], reverse=True)
        # 空格作为生词字符
        words.append(cls.unk_token)
        with open(cls.vocab_path, 'wb') as fh:
            pickle.dump(words, fh)
        logging.info(f'词表大小={len(words)} 内容={words}')
        return words

  在我们的项目中,预处理器processor在构建词汇表时会遵循一系列明确的步骤。首先,它会检查指定的词汇表文件路径是否存在。如果该路径上已有词汇表文件,预处理器将直接从中加载这个词汇表。这对于加速处理流程非常有帮助,尤其是在处理大型数据集时。

  如果词汇表文件不存在,预处理器则会开始从头构建一个新的词汇表。这个过程涉及处理传入的InputExample对象列表,从中提取所有文本内容。在构建词汇表的过程中,预处理器会在每个文本样本的前后分别添加开始和结束标记,这有助于模型理解文本的边界。接着,它会统计并记录每个唯一词汇的出现频率,并根据这些频率以降序的方式对词汇进行排列。

  完成这些步骤后,最终得到的词典就是一个包含所有不同词汇的字符串列表。这个词汇表是后续文本处理和模型训练的基础,它确保了每个文本样本都能以一致和标准的方式被模型理解和处理。如下:

在这里插入图片描述

  其次就是预处理器processor向外提供分词器:

    def tokenizer(cls, text, add_end=True):
        if add_end:
            # 加上前缀字符
            text = cls.start_token + text + cls.end_token

            if len(text) < cls.max_seq_len:
                text += cls.unk_token * (cls.max_seq_len - len(text))
            elif len(text) > cls.max_seq_len:
                text = text[:cls.max_seq_len - 1] + cls.end_token
        else:
            text = cls.start_token + text
            if len(text) < cls.max_seq_len:
                text += cls.unk_token * (cls.max_seq_len - len(text))
            elif len(text) > cls.max_seq_len:
                text = text[:cls.max_seq_len]
        # 转id
        ids = [cls.vocab.index(word) for word in text]

        return ids

  在我们的预处理器processor中,有一个特定的函数负责将文本字符串转换为模型能够理解的格式。这个函数接收两个参数:一个文本字符串和一个布尔值add_endadd_end参数用于指示是否在文本的末尾添加一个结束标记。

  在处理文本时,函数首先会在文本的开头添加一个开始标记。如果add_end被设为True,那么在文本的末尾也会添加一个结束标记。这一步是为了让模型能够更清楚地识别文本的开始和结束。随后,根据设定的最大序列长度max_seq_len,文本可能会被截断或者填充未知字符标记,以确保其长度与max_seq_len一致。最后,文本中的每个字符都会被转换成对应的索引值,这些索引值代表了字符在词汇表vocab列表中的位置。

  设置add_end参数的目的在于,当进行文本生成时,我们可以选择不在文本后面添加结束字符。这是因为如果文本末尾添加了结束字符,模型在预测下一个输出时有较大可能会生成结束字符,这可能不利于生成连续和流畅的文本内容。

[4] 提取word2vec

  在我们自定义的模型中,嵌入层(embedding layer)起着至关重要的作用。这个层的参数可以通过两种方式初始化:一种是随机初始化,另一种则是使用已有的官方预训练词向量,这后者通常被称作微调(Fine-tuning)。

self.embedding = nn.Embedding.from_pretrained(embedding_pretrained, freeze=False)

  举个例子,假设我们的输入中有一个字‘天’。如果我们选择随机初始化(比如全为0),那么在嵌入层中,‘天’对应的表征向量就会是一个全零向量[0,0,...,0]。这种情况下,项目初期所有词的表征向量都将是相同的,这显然不利于模型的有效学习。

  为了解决这个问题,有研究者基于大规模语料库进行无监督训练,以获取各种字、词的表征向量。这些表征向量最终形成了一组称为word2vec的向量库。

  然而,在中文处理中,尽管常用字可能只有几千或几万个,但通常我们会在词级别而非字级别进行分词,这就意味着常用的词组合非常庞大。因此,第三方提供的word2vec通常包含几十万个不同的字和词向量,其大小通常达到1GB甚至更大。

  在本项目中,由于我们只使用了大约7千个词,所以我们需要从这些庞大的word2vec库中提取出仅与我们词典中存在的字词相对应的向量。下面是实现这一目的的代码示例:

# ## 加载预训练词向量
def get_embed(file):
    print('加载词向量中...')
    model = KeyedVectors.load_word2vec_format(file, binary=False)

    print('加载加载文本构建的词汇表中...')
    vocabulary = pickle.load(open('./dataset/qijue/qijue_vocab.pkl', 'rb'))

    # 构建词汇-向量字典
    ebed = []
    for word in vocabulary:
        if word in model.key_to_index.keys():
            ebed.append(model[word])
        else:
            ebed.append(np.asarray([0 for i in range(0, 300)], dtype='float32'))

    # 储存词汇-向量字典
    np.savez(r'./dataset/qijue/sg.qijue.300d.npz',embeddings=np.asarray(ebed, dtype='float32'))

  第三方提供的word2vec的下载地址可以参考:https://github.com/Embedding/Chinese-Word-Vectors/blob/master/README_zh.md。

[5] 进行下一篇实战

  【古诗生成AI实战】之四——模型包装器与模型的训练

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

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

相关文章

单片机学习10——独立按键

独立按键输入检测&#xff1a; #include<reg52.h>sbit LED1P1^0; sbit KEY1P3^4;void main() {KEY11;while(1){if(KEY10) //KEY1按下{LED10; //LED1被点亮}else{LED11;}} } 按键 #include<reg52.h>#define uchar unsigned char #define uint unsigned intsbit …

Java LeetCode篇-深入了解关于数组的经典解法

&#x1f525;博客主页&#xff1a; 【小扳_-CSDN博客】 ❤感谢大家点赞&#x1f44d;收藏⭐评论✍ 文章目录 1.0 轮转数组 1.1 使用移位的方式 1.2 使用三次数组逆转法 2.0 消失的数字 2.1 使用相减法 2.2 使用异或的方式 3.0 合并两个有序数组 3.1 使用三指针方式 3.2 使用合…

浅谈UML的概念和模型之UML九种图

1、用例图&#xff08;use case diagrams&#xff09; 【概念】描述用户需求&#xff0c;从用户的角度描述系统的功能 【描述方式】椭圆表示某个用例&#xff1b;人形符号表示角色 【目的】帮组开发团队以一种可视化的方式理解系统的功能需求 【用例图】 2、静态图 类图&…

新形势下,2024年企业数字化转型该如何进行?

2023年已然接近尾声&#xff0c;各大企业都陆续开始了各种工作总结与来年规划。然而在大多传统企业中&#xff0c;负责数字化转型的部门人员则显得略微尴尬。 如何正确认识数字化、如何制定数字化年度目标以及如何快速体现数字化转型的价值&#xff0c;成为了数字化工作难以逾…

PlantUML语法(全)及使用教程-时序图

目录 1. 参与者1.1、参与者说明1.2、背景色1.3、参与者顺序 2. 消息和箭头2.1、 文本对其方式2.2、响应信息显示在箭头下面2.3、箭头设置2.4、修改箭头颜色2.5、对消息排序 3. 页面标题、眉角、页脚4. 分割页面5. 生命线6. 填充区设置7. 注释8. 移除脚注9. 组合信息9.1、alt/el…

传音荣获2023首届全国人工智能应用场景创新挑战赛“智能家居专项赛”三等奖

近日&#xff0c;中国人工智能学会与科技部新一代人工智能发展研究中心联合举办2023首届全国人工智能应用场景创新挑战赛&#xff08;2023 1st China’s Innovation Challenge on Artificial Intelligence Application Scene&#xff0c;以下简称CICAS 2023)&#xff0c;吸引了…

解决方案:新版WPS-右键粘贴值到可见单元格没有了

旧版WPS&#xff0c;右键就能出现 但是新版WPS不是在这里&#xff08;方法1&#xff09; 新版WPS&#xff08;方法2&#xff09; 视频详细教程链接&#xff1a;解决方案&#xff1a;新版WPS-右键粘贴值到可见单元格没有了 -- 筛选后复制公式粘贴为数值_哔哩哔哩_bilibili

Vue轻松入门,附带学习笔记和相关案例

目录 案例 一Vue基础 什么是Vue&#xff1f; 补充&#xff1a;mvvm框架 mvvm的组成 详解 Vue的使用方法 1.直接下载并引入 2.通过 CDN 使用 Vue 3.通过npm安装 4.使用Vue CLI创建项目 二插值表达式 什么是插值表达式&#xff1f; 插值表达式的缺点 解决方法 …

面对Spring 不支持java8的改变方法

接下来&#xff0c;就只有17与21了&#xff0c;JDK开发人员每隔半年&#xff0c;发布一个新的版本&#xff0c;但是新版本也只是维护一段时间&#xff08;一年/半年&#xff09;业务越小&#xff0c;升级越简单 1.如何创建Spring Boot项目,阿里云上去下载代码&#xff0c;然后使…

ESP32网络开发实例-Web页面控制直流电机

Web页面控制直流电机 文章目录 Web页面控制直流电机1、应用介绍2、软件准备3、硬件准备4、代码实现在这个 ESP32 Web务器应用中,我们将创建一个托管在 ESP32 上的网页,我们将使用该网页来控制使用 L298N 电机驱动器模块的直流电机的速度。 网页将包含一个 HTML 滑块,用于为直…

中低压MOSFET 2N7002KW 60V 300mA 双N通道 SOT-323封装

2N7002KW小电流双N通道MOSFET&#xff0c;电压60V电流300mA&#xff0c;采用SOT-323封装形式。超高密度电池设计&#xff0c;适用于极低的ros (on)&#xff0c;具有导通电阻和最大直流电流能力&#xff0c;ESD保护。可应用于笔记本中的电源管理&#xff0c;电池供电系统等产品应…

springBoot的实现原理;SpringBoot是什么;使用SpringBoot的核心功能;springBoot核心注解以及核心配置文件

文章目录 springBootspringBoot的实现原理什么是 Spring Boot&#xff1f;SpringBoot是什么为什么要使用springBootSpring Boot的核心功能Spring Boot 主要有如下优点&#xff1a; SpringBoot启动过程-流程Spring Boot 的核心注解是哪个&#xff1f;什么是 JavaConfig&#xff…

CMake语法解读 | Qt6需要用到

CMake 入门CMakeLists.txtmain.cpp编译示例cmake常用参数入门 Hello CMake CMake 是一个用于配置跨平台源代码项目应该如何配置的工具建立在给定的平台上。 ├── CMakeLists.txt # 希望运行的 CMake命令 ├── main.cpp # 带有main 的源文件 ├── include # 头文件目录 …

【全栈开发】Blitz.js与RedwoodJS

技术的不断发展是必然的。如果你仔细观察这片土地&#xff0c;你会注意到随着技术的成熟而出现的某些模式。特别是&#xff0c;开发人员一直在努力提高性能&#xff0c;简化开发过程&#xff0c;增强开发人员体验。 在本指南中&#xff0c;我们将分析两个帮助全栈应用程序世界…

【网安AIGC专题】46篇前沿代码大模型论文、24篇论文阅读笔记汇总

网安AIGC专题 写在最前面一些碎碎念课程简介 0、课程导论1、应用 - 代码生成2、应用 - 漏洞检测3、应用 - 程序修复4、应用 - 生成测试5、应用 - 其他6、模型介绍7、模型增强8、数据集9、模型安全 写在最前面 本文为邹德清教授的《网络安全专题》课堂笔记系列的文章&#xff0c…

nodejs+vue+elementui网上家电家用电器数码商城购物网站 多商家

基于vue.js的恒捷网上家电商城系统根据实际情况分为前后台两部分&#xff0c;前台部分主要是让用户购物使用的&#xff0c;包括用户的注册登录&#xff0c;查看公告&#xff0c;查看和搜索商品信息&#xff0c;根据分类定位不同类型的商品&#xff0c;将喜欢的商品加入购物车&a…

c语言:有关内存函数的模拟实现

memcpy函数&#xff1a; 功能&#xff1a; 复制任意类型的数据&#xff0c;存储到某一数组中。 代码模拟实现功能&#xff1a; #define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<string.h> #include <stdio.h> #include<assert.h> memcpy…

初识Spring (Spring 核心与设计思想)

文章目录 什么是 Spring什么是容器什么是 IoC理解 Spring IoCDI 概念 什么是 Spring Spring 官网 官方是这样说的: Spring 让每个人都能更快、更轻松、更安全地进行 Java 编程。春天的 专注于速度、简单性和生产力使其成为全球最受欢迎Java 框架。 我们通常所说的 Spring 指的…

避免手机无节制使用

手机使用情况分析 使用时间 我挑选了11月份某一周的统计数据&#xff0c;可以看到&#xff0c;我的日均手机手机时间达到了惊人的8个小时&#xff0c;每周总共余约57小时。 按照使用软件的类型来分类&#xff0c;其中约%50用于娱乐&#xff0c;主要使用软件为&#xff1a;哔哩…

Mac 搭建本地服务器

文章目录 1 启动服务器2 服务器目录3 手机访问服务器3.1 手机和电脑连上同一个局域网( 或WIFI)3.2 找到电脑的ip地址 如下图所示3.3 手机打开 http://192.168.10.5/1.txt 4 关闭服务器5 参考文章 1 启动服务器 sudo apachectl start启动后访问 http://localhost/ 如下图所示即…