基于中文GPT2训练一个属于自己的微信聊天机器人(Colab + Kaggle GPU)

(注:本专栏只讨论相关技术,不涉及任何其他目的,如侵删)
摘要
本专栏介绍了基于中文GPT2训练一个微信聊天机器人的方法,模型实现基于GPT2-chitchat和GPT2-Chinese,训练语料为两个人的对话聊天记录。微信聊天记录的划分比较复杂,因为两个人的对话在时间和内容上具有一定的连续性。我提出了一个较为简单的划分思路,并附上了相关的实现代码。我使用Colab和Kaggle的GPU进行训练,总共训练了差不多30h,最终实现了还算可以的效果。最后,我对本次复现的不足提出了一些思考。
前言
前段时间看到Andrej Karpathy出了实现简易版ChatGPT的教程,正好之前导出了大概12w条微信聊天记录,就想着能不能训练一个自己的微信聊天模型。在GitHub上搜了一下,已经有大佬提供了预训练的中文GPT2对话模型,接下来只需要划分一下微信聊天记录,丢入模型训练即可。
本次复现主要基于GPT2-chitchat模型,项目链接如下:
GPT2-chitchat:https://github.com/yangjianxin1/GPT2-chitchat
GPT2-Chinese:https://github.com/Morizeyao/GPT2-Chinese
两位作者的代码都写得很好,推荐感兴趣的同学阅读,读完之后会对如何进行NLP数据预处理、如何运行一个中文NLP模型、如何生成对话等有一个清晰的认识。
我这次复现的方法是使用两个人的微信聊天记录训练GPT2,希望它能学习到我的回答风格,方便以后和对象吵架。一个关键的问题在于如何划分训练语料,由于我本人不是做NLP的,下面提供的划分思路仅供参考,欢迎大家指正。
微信聊天记录提取
首先,这里简单提一下微信聊天记录的提取方式,我采用的是https://github.com/saturn-opposition/wechat_analysis中的提取思路,使用夜神安卓模拟器,不要使用腾讯手游助手,很垃圾!!!
上面的github链接中提供了两种导出数据的方法,我个人是按照第一种,即https://blog.csdn.net/weixin_41746317/article/details/104110161?spm=1001.2101.3001.6650.5&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EOPENSEARCH%7ERate-5-104110161-blog-126700288.pc_relevant_multi_platform_whitelistv3&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EOPENSEARCH%7ERate-5-104110161-blog-126700288.pc_relevant_multi_platform_whitelistv3&utm_relevant_index=6。
导出db文件后,需要通过MD5加密输出得到密码,我个人的尝试是IMEI = ‘1234567890ABCDEF’ ,即

(注:int name=“_auth_uin” value="中,value不一定带有负号,加密字符串时需注意)
在sqlcipher.exe中直接导出message为message.csv文件,接下来我们对该文件进行处理。

数据预处理(微信文本划分)
(下文附上了数据处理的代码,可以直接处理前面导出的message.csv)
原文件有很多列,我们实际上只关心其中的四列:type,isSend,createTime和content。语言模型只能学习有效的文本消息,因此我们只需要type=1的消息,具体消息类型见https://blog.csdn.net/muzhicihe/article/details/109902849?spm=1001.2014.3001.5506。isSend=0/1 分别对应对方/己方发送的消息,createTime是发送时间;content是消息内容。
接下来我们需要将导出的文本消息转换成训练语料。GPT2-chitchat模型要求的训练文本格式长下面这样:

即每段训练文本由几句话组成,采用问答交替的形式(可以有多次问答),不同语料之间间隔一行(见GPT2-chitchat项目中提供的样例),所有训练语料存在一个train.txt文件中。
但关键的问题在于,两个人的聊天在时间和内容上是有一定连续性的(比较粗糙的描述,不知道语言学中是否有对应概念),不能直接按照一问一答来划分,很有可能出现答非所问的情况。另外微信聊天是一个比较轻松的环境,并不像辩论赛那样有较为严格的发言规则,说话人可能会连续说好几句话之后对方才回答,或是回答人很可能过了很久才看到对方的发言等等。因此想要正确地划分训练语料并不容易,划分效果会直接影响模型的训练结果。
这里我想了一个比较简单直接的划分方法,供大家参考。我主要考虑了以下两个点:
发言的连续性
假设在1min内,由同一个人发出的所有消息可以视为一句话(一次询问或一次回答)。
对话的实时性
假设在某一方发言结束(发言时间1min结束)后,若另一方在5min内作出回应,则对话成立,该段对话可以作为训练文本。若有一方在5min之内未作出回应,或是该段对话已经包含了3次问答(共6句话),则视为对话结束。
在处理csv文件之前,我们需要将message.csv另存为csv utf-8格式,否则无法顺利读入。
微信文本划分的完整代码如下,可以将该脚本复制到message.csv所在文件夹后直接运行,即可得到对应的train.txt文件。下面对代码部分细节作进一步解释,不想看的可以直接跳到下一部分)
在读入文件之后,我们需要根据talker筛选出属于对话双方的聊天记录,可以在message.csv文件中观察,找到对应的那个talker(以“wxid_”开头,不管是接收还是发送的消息都是同一个talker):
原csv文件并不是完全按照时间顺序排列,我们需要按照createTime对文本进行排序。createTime采用毫秒时间戳,但具体的时间只精确到分钟,同一个createTime是按照消息发送先后顺序升序排列。因此,要想得到正确的划分结果,我们需要使用稳定的排序方法(比如归并排序,sort_values默认采用快速排序),这一点非常关键!
下一步就是利用time库的函数将毫秒时间戳转换成年、月、日、小时、分钟,并将其concat回原来的数组:
接下来就是重头戏:训练语料划分。
由于判断对话状态涉及到时间的比较,这里我先定义了一个比较时间的函数,其中sample是当前对话所在list,包含isSend,content,年,月,日,小时,分钟,current_hour和current_min是要比较的小时和分钟,time_threshold是设定的阈值。由于current_hour和current_min是过去发送的信息,因此一定小于sample的时间。具体实现如下:
接下来是遍历文本消息。为了让机器尽量学习我回答时的风格,每段对话都以对方询问开始(isSend=0)。同一个人在1min内连续发的消息,用中文的逗号“,”连接,合并成同一个句子,而对方如果在5min内回答,则对话成立:
如果己方回答后,对方5min内没有回应,或是本段文本已经包含了6句话(3段问答),则判断对话中止,在下一行再加一个换行符:
下面这一部分代码是否添加视情况而定,主要是针对下面这种情况:
对方文本1
己方文本1
对方文本2
对方发送完文本2之后,己方没有及时回应,导致对话中断。如果添加下面这段代码,训练语料中会包含没有回答的对方文本2,否则只包含对方文本1和己方文本1。
最后,我们划分好的语料存入train.txt中,就可以开始训练了!
模型训练
终于说完了数据预处理,接下来是模型训练。GPT2-chitchat使用的是HuggingFace的transformers中的GPT2LMHeadModel,链接为https://huggingface.co/transformers/v3.5.1/model_doc/gpt2.html。
在训练之前,我们需要将train.txt中的文本利用BertTokenizerFast进行分词和编码,并给每一段对话加上[CLS]和[SEP],原项目没有使用预训练的分词方法,而是直接将每个汉字视为一个token。具体操作是运行项目的preprocess.py文件,生成的train.pkl会存储在data文件夹下。
我在训练前并未修改preprocess.py的代码,训练后模型出现了较为严重的过拟合(测试集的loss一直上升,训练集的loss一直下降),怀疑是划分训练集和验证集时并未打乱原数据集。一个可能的解决办法是对preprocess.py的代码做一些修改,在写入dialogue_list前加一行random.shuffle(dialogue_list)。我自己没有尝试修改之后的数据集,有兴趣的可以看看新数据集的结果。
下面正式开始训练,分别使用Colab和Kaggle提供的GPU。
Colab版
使用Colab进行训练可以直接访问Google Drive,不用下载模型到自己的电脑上,但缺点是只能训练几个小时,同时不能中断。
在训练前,我们在Google Drive中新建一个GPT2文件夹,将train.pkl上传到该文件夹中。

注意各个路径的设置,实验中设置每5个epoch保存一次模型,模型存储至epoch1中的model文件夹(Hugging Face的transformers要求保存路径为文件夹,里面包含config.json和pytorch_model.bin文件)。第一次训练时,需要从原项目链接中下载预训练模型,同样上传到drive中,别忘了修改config.json中的存储路径。

原模型是在50w条数据集上预训练的,使用了warmup scheduler,即学习率先上升再下降,训练结束时学习率为0。由于我的数据集只有9k条数据,实验中去掉了warmup scheduler,固定使用了一个较高的学习率5e-6,在600个epoch之后观察到training loss仍在不断下降。

链接如下:(防止连接中断的方法见https://blog.csdn.net/Thebest_jack/article/details/124565741)https://colab.research.google.com/drive/1qJXvBiPg8jMckXozwEgBWHGvmjpcMCgY#scrollTo=AQIQ3xkSt3RG
Kaggle版
用kaggle的GPU稍微麻烦一点,需要上传数据集,但好处是可以有40h的使用时间,同时训练时不需要一直保持连接(save version)。
首先,新建一个数据集GPT2_Wechatdata,将train.pkl上传至该数据集。(不熟悉上传数据集的同学可以看一下上一期专栏的对应部分)

然后再新建一个数据集GPT2_Model,将pytorch_model.bin和config.json所在文件夹压缩,然后上传压缩文件至该数据集:(不需要把train.txt也加进来)

随后我们需要将上一次训练的checkpoint上传至Google Drive,记下id,使用gdown下载。(第一次训练不需要这个操作)
(其实可以把checkpoint也上传到数据集,但checkpoint中保存了optimizer的信息,有600M大,上传速度较慢,因此选择上传至Google Drive)

设置参数和路径,随后可以开始训练:

链接如下:https://www.kaggle.com/littleweakweak/gpt2wechat。每次恢复训练时,都要重复更新GPT2_Model和Google Drive中的checkpoint。
训练结果
最终训练了600个epoch,准确率可以达到0.59,训练loss可降至1.8左右,若继续训练仍会下降,而测试误差一直在上升。
交互方式见原项目链接,因为涉及到隐私,这里就不把所有的问答放上来了,只放一些有意思(错误)的结果:


(我严重怀疑是原模型被喂了太多莫名其妙的屑语料,导致微调之后模型说话素质过低)
讨论
由于时间和训练语料的数量限制,模型并没有取得非常完美的效果,但还是出乎我的意料,有些回答确实像是我自己说出来的。下面简单谈谈我自己认为的可能的不足之处:
微信聊天记录的划分方式过于直接,只考虑了时间的连续性,忽略了说话人转换话题的情况,因此同一段语料可能包含多个话题。后续可能可以尝试通过谈话内容的主题进一步划分。
训练语料过少,模型出现较为严重的过拟合。这次我只采用了9k段训练语料,远少于原模型的50w条训练数据,后续可以考虑增加训练语料的数量。
提取出的部分聊天内容具有的有效信息过少,如以问号、省略号开头等,后续可以考虑筛选掉这些低效信息。
原始的聊天记录包含不少表情包和图片,为了简便我将它们都删除了,这样会导致部分语料的上下文不连续,影响模型的结果,后续可能可以改进这种处理方式。
原模型中没有采用预训练的分词方式,而是按照每个汉字来分词,不知道采用其他分词方式是否会对模型结果有影响。
心得
这是我第二个复现的项目,还是挺有意思的。这次其实最麻烦的地方在于聊天记录的划分方法,后面的训练很多是循规蹈矩的。总的来说还是收获很多,对于NLP也有了更多的理解,很多不足欢迎大家指正。