训练一个自己的文本生成模型

3 阅读7分钟

省流:本人企图拿自己历年创作的同人文合并作为训练集用 GPT2-Chinese 炼出一个定制文风的模型,因产出太少导致文本训练集过小,训练无果 __ (:з」∠) __

故本文依旧是雷打不动的踩坑记录。

模型选择

本次尝试选择的是 GPT2-Chinese 模型,一款 2019 年开源的优秀中文大模型。尽管模型作者已经在今年 4 月 11 日的时候在 README.md 文件里表示无意继续对此项目长期维护更新,但对我这种菜鸟来说,不更新意味着足够稳定,那我也正好试一试、玩一玩呀!(人工智能好难,损失函数看不懂,前向函数也看不懂)

仓库地址:GPT2-Chinese GPT2-中文

本次尝试主要有两个阶段,第一阶段,我照着仓库指引整理训练数据、启动训练以及最终生成文本。这个阶段里除了数据集是我自己的文本整合之外,其余的词汇表、训练参数、生成参数等等都是按照默认配置。在本阶段历经千辛万苦( python 环境 + 下载 GPU 驱动)后终于启动,我生成了一个啄木鸟 ↓

初次生成文本

在差点把自己笑死之后,我开始仔细看参数、找教程、问AI,以 AI 之道还治 AI 之身(?)之后便有了第二阶段:整理数据集,并生成自己的词汇表,再用新的词汇表替换项目中自带的词汇表文件,启动训练,最终测试生成。

这个阶段参考的文章是 GPT2-Chinese 文本生成,训练AI写小说,AI写小说2

数据集整理

整理自吾自己的WPS云文档,此处便是AI之道还治AI之身第一步:给我写个遍历docx文件并把换行符替换成 \n 字符串再全部写入一个数组里保存为一个 json 文件。AI 回答好,然后我心安理得地拷贝运行,发现它只识别到了三四篇,我其他十几篇没被处理?!原来是我那十几篇写的太早,保存时还是 doc 后缀的文件。

image.png

遂在识别文件后缀的地方将补上or filename.endswith(".doc")解决。

import os
import json
from docx import Document

def read_docx(file_path,total_words):
    """读取Word文档的内容并返回"""
    doc = Document(file_path)
    full_text = []
    for para in doc.paragraphs:
        full_text.append(para.text)
    total_words['num']+=len(''.join(full_text))
    return '\n'.join(full_text)

def merge_word_files(directory, output_json_path):
    """合并指定目录下的所有Word文档,并将内容存储为JSON格式"""
    all_texts = []
    total_words={'num':0,}
    # 遍历目录中的所有Word文件
    for filename in os.listdir(directory):
        if filename.endswith(".docx") or filename.endswith(".doc"):
            file_path = os.path.join(directory, filename)
            text = read_docx(file_path,total_words)
            all_texts.append(text)
    print(f"总字数:{total_words}")
    # 将结果写入JSON文件
    with open(output_json_path, 'w', encoding='utf-8') as json_file:
        json.dump(all_texts, json_file, ensure_ascii=False, indent=4)

if __name__ == "__main__":
    total_words = 0
    # 指定Word文档所在的目录和输出的JSON文件路径
    word_directory = "./"  # 替换为你的Word文档目录路径
    output_json = "train.json"  # 输出JSON文件的路径
    
    merge_word_files(word_directory, output_json)
    print(f"合并完成,结果已保存到 {output_json}")

原理也很简单,用的 python-docx 包也是我工作中经常接触的,遍历文件夹、判断是否 docx 或 doc 后缀、读取文档、遍历 paragraph 行内容、文字添加到数组、数组用.join()方法以\n为连接字符合并成字符串、返回上一层循环、将文档字符串添加到文档数组末尾、最后保存到 train.json 。这个文件名是模型作者在 README.md 里写的数据集名称。

那么第一步,就有惊无险地搞定了。

image.png

词汇表

这一步在模型源码中并未强制要求,源码使用的词汇表是cache/vocab_small.txt,里面各种乱七八糟的字符已经涵盖得很全面,就我个人的两个阶段的体验下来,我这种个人的小体量文字模型是不需要额外制作词汇表的,应该古诗文或近代白话小说会比较需要。

不过这个流程也记录一下。参考的是前面说过的 GPT2-Chinese 文本生成,训练AI写小说,AI写小说2 这篇文章。这位大佬写得很详细,关联的包安装配置、配置文件里的字段意义、不同版本需要注意修改的源码等等都写得很详细。

制作词汇表的步骤整理下来有三步:

  1. 读取小说文件,平分成100份,100份后多余的部分舍弃.每份保存到一个文件
  2. 每份字符串用cut函数分词,单词之间用空格连接区分 ,然后把每份写到 word_segmentation 文件夹内,每份名称 word_segmentation_0.txt - word_segmentation_99.txt
  3. 把分词文件转换为int数字文件

整个步骤挺精妙的,特别是文章作者单独写的 Vocab 类,里面的用于中文分词的 cut 方法很值得学习,这里搬运贴一下。

def cut(self, input_str):
    """
    中文语句分词的算法  用python代码 cut函数 参数 词汇表文件 和 语句str
    词汇表文件 每行一个词语,
    1.词汇表字典的键为词汇,值为该词汇在词汇表中的行号-1,也即该词汇在词汇表中的索引位置。
    3.输入的中文语句,从左到右依次遍历每一个字符,以当前字符为起点尝试匹配一个词汇。具体匹配方式如下:
      a. 从当前字符开始,依次向后匹配,直到找到一个最长的词汇。如果该词汇存在于词典中,就将其作为一个分词结果,并将指针移动到该词汇的后面一个字符。
         如果该词汇不存在于词典中,则将当前字符作为一个单独的未知标记,同样将其作为一个分词结果,并将指针移动到下一个字符。
      b. 如果从当前字符开始,没有找到任何词汇,则将当前字符作为一个单独的未知标记,同样将其作为一个分词结果,并将指针移动到下一个字符。
    重复上述过程,直到遍历完整个输入的中文语句,得到所有的分词结果列表。
    """
    result = []
    i = 0
    while i < len(input_str):
        longest_word = input_str[i]
        for j in range(i + 1, len(input_str) + 1):
            if input_str[i:j] in self.token2id:
                longest_word = input_str[i:j]
        result.append(longest_word)
        i += len(longest_word)
    return result

开始训练

训练集与词汇表都弄完了,就可以开始训练了。训练可以用 CPU 也可以用 GPU,虽然我这笔记本也好几年了但好歹当初就冲着游戏本买的(虽然买回来没玩过主机游戏一直在模拟器里看录像带啊!)遂一直没弄那个自带的 GTX 1050,这次便顺带捣腾了一下。

然后被镜像踹了一脚……

GPU 驱动就不用说了,直接官网下载,对应 cuda 和 pytorch 版本需要对应。我参考的操作是 深度学习环境搭建(GPU)CUDA安装(完全版) 以及pytorch的官网 pytorch.org/get-started…

我自己需要安装的是这个版本 ↓

image.png

安装时注意,先清掉原本在conda配置的国内镜像,比如我配置的阿里云,它一直提示找不到对应包……

安装完毕,用torch.cuda.is_available()确认下是否可以识别到 GPU ,可以的话就直接进入对应的 python 环境,开始跑训练了。

python train.py --model_config ./config.json --tokenizer_path ./vocab.txt --tokenized_data_path ./tokenized/ --epochs 10 --batch_size 1

亲测GTX 1050用这个参数跑下来需要一小时左右,也没感觉很快啊(?

测试生成

测试使用的脚本是 generate.py ,里面可以指定要生成的文本字数。--prefix是指定提示词,最好不要带标点符号,容易出现奇怪的错误。

python ./generate.py --length=500 --nsamples=1 --prefix=什么时候能暴富 --tokenizer_path ./vocab.txt  --fast_pattern --save_samples --save_samples_path=./result/output  --weights_only True

这里面的--tokenizer_path是用来指定词汇表的,如果前面训练模型时有做了指定,那生成文本时也需要作相应的指定,不然就会出现我下面这种奇怪的乱码 ↓

指定词汇表后再运行,就能有正常的结果出来了,这里就不贴了毕竟训练集太小次数也不够,只知道不再是啄木鸟就行了 __ (:з」∠) __