如何训练与部署自定义AI生成的格言:使用GPT2、FastAPI和ReactJS

问题

好的格言能让我们更坚强。格言真正鼓舞人心的并非其语调或满足感,而是分享者所反映的、真正有益于他人的生活经验。上面这段关于格言的话(格言套娃)并不是我写的,而是我训练的一个AI模型生成的。而且它说得比我可能表达的更好。格言对不同的人意味着不同的事物。有时它们激励我们,鼓舞我们。另一些时候,它们让我们思考生活、宗教,有时它们只是让我们发笑。

那么,我们能否训练一个AI模型来生成更多格言,让我们思考、发笑或受到启发?这就是我开始这段旅程的动机。

太长不看版
我已经在具有特定风格的格言上微调了GPT2模型,风格包括励志/鼓舞人心、有趣以及严肃/哲学,并部署在一个可即用的网站上:AI格言生成器。

模型

2020年6月3日,某中心发布了GPT3,这是一个在570GB互联网文本上训练的巨型语言模型。人们将这个多才多艺的模型应用于各种场景,从创建应用设计、网站,到实现近乎魔法的Excel函数。但存在一个问题——模型权重从未公开。我们访问它的唯一方式是通过付费的API。

所以,让我们时光倒流到2019年11月,那时GPT-2发布了。GPT-2虽然不如GPT-3强大,但在文本生成领域引发了一场革命。我记得当我阅读模型生成的演示文本时,惊讶得下巴都要掉下来了——其连贯性和语法结构近乎完美。我对模型的要求不是成为魔术师,而是能够生成结构完美的英语句子。为此,GPT2已经绰绰有余。

数据集

首先我需要一个数据集。从网上爬取格言是一个选择,但在那之前我想看看是否已经有人做过这件事。果然!Quotes-500k是一个包含近50万条格言的数据集,所有格言都是从网上爬取的,并附有诸如知识、爱情、友谊等标签。

现在我希望模型能够根据特定主题生成格言。由于我计划使用预训练模型,条件性文本生成并不容易实现。PPLM是某机构发布的一个模型,或者更确切地说,是一种使用预训练模型的方法,旨在实现这一目标(一篇非常有趣的论文,一定要看看),但最终我选择了另一条路。我决定训练三个模型,每个都针对特定类型的格言进行微调。

我考虑了三种类型——励志型、严肃型和有趣型。我使用Quotes500k数据集及其标签,根据标签将格言分离到这三个类别中。对于励志数据集,我使用了诸如爱情、生活、鼓舞人心、励志、人生教训、梦想等标签。对于严肃型,我选择了哲学、生活、上帝、时间、信仰、恐惧等标签。最后,对于有趣型,我只用了幽默和搞笑这类标签。

预处理

这里没什么特别的;只是基本的清理工作。例如:

  • 转换为小写
  • 替换缩写,如将"wasn’t"替换为完整形式"was not"。
  • 移除HTML特殊实体(因为这是一个从网上抓取的语料库)
  • 移除多余的空格
  • 在单词和标点符号之间插入空格
  • 拼写检查和纠正(使用pyenchant)

你可能会有疑问:那停用词呢?词形还原呢?那些步骤在哪里?删除停用词和词形还原并不是你在每个NLP任务中都必须执行的强制性步骤。这真的取决于任务。如果我们使用TF-IDF这类模型进行文本分类,那么,是的,做所有这些是有意义的。但对于使用上下文(如神经网络模型或N-Gram模型)的文本分类来说,必要性就小一些。如果你在进行文本生成或机器翻译,那么删除停用词和词形还原实际上可能会损害性能,因为我们正在从语料库中丢失有价值的上下文信息。

句首/句尾标记与最大长度

我还希望生成的格言不要太长。所以我查看了数据集,绘制了格言长度的频率分布图,并决定了一个适当的截断长度。在励志语料库中,我丢弃了所有超过100个单词的格言。

格言长度频率(励志型)
另一个使格言简短的重要方面是模型预测句尾标记的能力。因此,我们用句首标记和句尾标记来包装每条格言。

tokenizer = AutoTokenizer.from_pretrained('gpt2')
MAX_LEN = 100
train_m = ""
with open(train_path, "r", encoding='utf-8') as f:
    for line in f.readlines():
        if len(line.split())>MAX_LEN:
            continue
        train_m += (tokenizer.special_tokens_map['bos_token']+line.rstrip()+tokenizer.special_tokens_map['eos_token'])
with open(train_mod_path, "w", encoding='utf-8') as f:
    f.write(train_m)

test_m = ""
with open(test_path, "r", encoding='utf-8') as f:
    for line in f.readlines():
        if len(line.split())>MAX_LEN:
            continue
        test_m += (tokenizer.special_tokens_map['bos_token']+line.rstrip()+tokenizer.special_tokens_map['eos_token'])
with open(test_mod_path, "w", encoding='utf-8') as f:
    f.write(test_m)

训练

现在我们已经准备好并处理了数据集,让我们开始训练模型。来自某机构的Transformers库因其易于使用的API和惊人的预训练权重模型集合而成为显而易见的选择。得益于某机构,训练或微调模型是一件轻而易举的事。

我们首先加载GPT2的模型和分词器。

tokenizer = AutoTokenizer.from_pretrained('gpt2')
model = AutoModelWithLMHead.from_pretrained('gpt2')

就这样。在你编码时,巨大的预训练模型的全部力量就掌握在你手中。

现在,让我们定义一个加载数据集的函数。

def load_dataset(train_path,test_path,tokenizer):
    train_dataset = TextDataset(
          tokenizer=tokenizer,
          file_path=train_path,
          block_size=128)
    test_dataset = TextDataset(
          tokenizer=tokenizer,
          file_path=test_path,
          block_size=128)
    data_collator = DataCollatorForLanguageModeling(
        tokenizer=tokenizer, mlm=False,
    )
    return train_dataset,test_dataset,data_collator

train_dataset,test_dataset,data_collator = load_dataset(train_mod_path,test_mod_path,tokenizer)

现在我们有了数据集,让我们创建Trainer。这是处理所有训练过程的核心类。

from transformers import Trainer, TrainingArguments

training_args = TrainingArguments(
    output_dir="./storage/gpt2-motivational_v6", #输出目录
    overwrite_output_dir=True, #覆盖输出目录的内容
    num_train_epochs=10, #训练轮数
    per_gpu_train_batch_size=32, #训练批大小
    per_gpu_eval_batch_size=64,  #评估批大小
    logging_steps = 500, #两次评估之间的更新步数
    save_steps=500, #每#步后保存模型
    warmup_steps=500,#学习率调度器的预热步数
    )

trainer = Trainer(
    model=model,
    args=training_args,
    data_collator=data_collator,
    train_dataset=train_dataset,
    eval_dataset=test_dataset,
    prediction_loss_only=True,
)

到这个时候,我们已经拥有了开始训练所需的所有要素——模型、分词器和训练器。剩下要做的就是训练模型。训练完成后,我们只需要保存模型和分词器以备使用。

trainer.train()
trainer.save_model("./storage/gpt2-motivational_v6")
tokenizer.save_pretrained("./storage/gpt2-motivational_v6")

每个模型都在单个P5000 GPU上训练了约50轮,总共约12小时(不包括所有测试运行以及出现问题的运行)。

推理

我们已经训练并保存了模型。现在呢?为了进行推理,我们使用了某机构的另一个杰出特性——管道。这个很棒的特性让你只需几行代码就能将模型投入生产。

tokenizer = AutoTokenizer.from_pretrained("./storage/gpt2-motivational_v6")
model = AutoModelWithLMHead.from_pretrained("./storage/gpt2-motivational_v6")

gpt2_finetune = pipeline('text-generation', model=model, tokenizer=tokenizer)

# gen_kwargs有不同的选项,如max_length、beam_search选项、top-p、top-k等
gen_text = gpt2_finetune (seed, *gen_kwargs)

gen_kwargs配置文本生成。使用了k=50的top_k采样和p=0.95的top_p采样的混合方法。为了避免文本生成中的重复,使用了no_repeat_ngram_size = 3和repetition_penalty=1.2。

用户界面

现在我们已经训练好了核心模型,我们需要一种与它交互的方式。每次需要它生成内容时都写代码对用户不友好。所以,我们需要一个UI。我选择用ReactJS快速构建一个简单的UI。

虽然我无法将整个项目放到Github上,但我会将关键部分作为Github Gists发布。

出于UI的目的,我给三个模型起了代号——Inspiratobot(励志型)、Aristobot(严肃型)和FunnyBot(有趣型)。

UI的主要功能包括:

  • 能够在三种不同风格之间进行选择
  • 可以自己输入引语开头,也可以使用众多随机的起始种子之一(将自动填充)。如果不喜欢某个种子,点击随机按钮获取另一个种子。
  • 在高级设置中,可以调整引语的最小和最大长度。也可以调整多样性(或采样中的温度参数)
  • UI还使用了来自Unsplash的库存照片作为引语的背景,因为这是近来的潮流。它会从一系列适合当前生成引语风格的照片中进行选择。
  • 还可以让你以1到5的等级为引语评分。

UI项目的文件夹结构如下:

|                  
 +---public
 |       favicon.ico
 |       index.html
 |       logo.png
 |       logo.svg
 |       manifest.json
 |       
 ---src
         App.js
         customRatings.js # 心形评分组件
         debug.log
         index.js
         quotes.css # 页面的CSS
         quotes.js # 核心页面
         theme.js
 |
 |   .gitignore
 |   package-lock.json
 |   package.json

后端API服务器

为了托管和服务模型,我们需要一个服务器。为此,我选择了FastAPI,这是一个直截了当的Web框架,专门用于构建API。

由于它的极致简单性,我强烈推荐FastAPI。一个非常简单的API示例(来自文档)如下:

from typing import Optional
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def read_root():
    return {"Hello": "World"}

@app.get("/items/{item_id}")
def read_item(item_id: int, q: Optional[str] = None):
    return {"item_id": item_id, "q": q}

不到10行代码就能为你的API搭建一个Web服务器,这听起来好得不像真的,但它确实如此。

不深入细节,API的关键部分是引语的生成,以下是相关代码。

def postprocess_gen_text(gen_text):
    sentences = gen_text.split(".")
    for i, sent in enumerate(sentences):
        sent = ". ".join([s.strip() for s in sent.split(".")]).capitalize().strip()
        sent = re.sub(r"\bi\b", "I", sent)
        sent = re.sub(r"\bgod\b", "God", sent)
        sent = re.sub(r"\bchrist\b", "Christ", sent)
        sentences[i] = sent
    return ". ".join(sentences).strip()

def generate_quote(model_name: ModelName, seed: str, gen_kwargs: dict):
    global model
    global tokenizer
    global current_model_name
    if model_name != current_model_name:
        _load_model(model_name)

    gpt2_finetune = pipeline(
        "text-generation", model=model, tokenizer=tokenizer, device=-1,
    )

    gen_text = gpt2_finetune(seed, **gen_kwargs)[0]["generated_text"]
    return postprocess_gen_text(gen_text)

项目的文件夹结构:

+---app
 |   |   api.py
 |   |   db_utils.py
 |   |   enums.py
 |   |   timer.py
 |   |   init.py
 |   |
 +---data
 |       funny_quotes.txt
 |       motivational_quotes.txt
 |       serious_quotes.txt
 |
 +---models
 |   +---gpt2-funny
 |   |
 |   +---gpt2-motivational
 |   |
 |   |---gpt2-serious
 |
 |   app_config.yml
 |   logger.py
 |   logging_config.json
 |   main.py
 |   memory_profiler.log
 |   pipeline.log
 |   requirements.txt

部署

将应用程序容器化,启动一个EC2实例并进行托管,这本来会很容易。但我想要一个便宜(如果不是免费的话)的方案。另一个挑战是每个模型大约500MB,将它们加载到内存中会使RAM消耗徘徊在1GB左右。所以EC2实例成本会很高。除此之外,还需要在云端存储三个模型,这也需要存储成本。最重要的是,还需要一个数据库来存储用户给出的评分。

带着这些需求,我开始寻找产品/服务。为了让事情更简单,我将应用分成了两部分——一个后端API服务器和一个内部调用后端API的前端UI。

在搜索过程中,我偶然发现了一个开发者可以使用的免费服务的超棒列表。在评估了几个选项之后,我最终确定了以下选项,以使部署的总成本尽可能低:

  • 后端API服务器 – Kintohub
  • 前端托管 – Firebase(免费)
  • 数据库 – MongoDB Atlas(免费)

KintoHub
“KintoHub是一个为全栈开发者设计的一体化部署平台”,主页上写道。如果成本不是问题,我本可以将整个应用程序部署在KintoHub上,因为这就是他们提供的服务——一个可以很好扩展的后端服务器、一个服务静态HTML文件的前端服务器以及一个数据库。在后台,他们通过一个非常易于使用的Web界面将应用程序容器化,并部署在选定配置的机器上。最棒的是,所有这些都可以通过Github完成。你将代码提交到Github仓库(公共或私有),并通过指向该仓库直接部署应用。

就必须要完成的最低限度设置而言,一个页面就足够了。

就是这样。当然还有更多可用设置,比如应用程序运行所需的内存等。虽然KintoHub有一个免费层,但我很快意识到,由于模型的内存消耗,我需要至少1.5GB才能使其运行而不崩溃。所以我转到了按需付费层,他们每月慷慨地赠送5美元信用额度。成本计算器显示应用程序托管将花费5.5美元,我可以接受(仍在等待月底看实际成本)。

Google Firebase
Firebase是很多东西的集合。它的主页上说它是一个完整的应用开发平台,事实也确实如此。但我们只对Firebase Hosting感兴趣,他们允许你免费托管单页网站。部署应用非常轻松。你需要做的就是:

  1. 安装firebase CLI
  2. 在你的React项目上运行npm build
  3. 从项目的根文件夹运行firebase init,并将源路径从public指向build
  4. 运行firebase deploy

只需要一个非常简短的教程就能完成。

MongoDB Atlas
MongoDB Atlas是一个云数据库即服务,在云上提供MongoDB,好处是他们提供了一个具有512MB存储空间的免费层。对于我们的用例来说,这绰绰有余。

创建一个新的集群是直截了当的,一旦解决了访问问题,就可以使用像pymongo这样的Python包装器来实现我们的数据库连接。

成果

所有这些工作的成果是一个网站,你可以在其中与模型互动,生成格言并假装是自己的作品 😄。

虽然模型并不完美,但它仍然能产生一些真正让你思考的好格言。这不就是你对任何格言的期望吗?玩得开心。

免责声明

本项目中提及的公司名称仅用于技术背景描述,不构成任何形式的商业推广。相关技术实现和部署方案的选择基于作者的个人评估和项目需求。
更多精彩内容 请关注我的个人公众号 公众号(办公AI智能小助手)或者 我的个人博客 https://blog.qife122.com/
对网络安全、黑客技术感兴趣的朋友可以关注我的安全公众号(网络安全技术点滴分享)

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐