From fe9eee3d9f83dc7655f428ff030e29d7f3fc859b Mon Sep 17 00:00:00 2001
From: yangjianxin1 <995462226@qq.com>
Date: Tue, 15 Jun 2021 16:49:41 +0800
Subject: [PATCH] Add files via upload
first commit
---
README.md | 319 +
config/cpm-medium.json | 33 +
config/cpm-small.json | 31 +
data_parallel.py | 109 +
dataset.py | 21 +
generate.py | 124 +
http_service.py | 108 +
preprocess.py | 73 +
train.py | 298 +
utils.py | 76 +
vocab/chinese_vocab.model | Bin 0 -> 713229 bytes
vocab/chinese_vocab.vocab | 30000 ++++++++++++++++++++++++++++++++++++
vocab/vocab.json | 1 +
13 files changed, 31193 insertions(+)
create mode 100644 README.md
create mode 100644 config/cpm-medium.json
create mode 100644 config/cpm-small.json
create mode 100644 data_parallel.py
create mode 100644 dataset.py
create mode 100644 generate.py
create mode 100644 http_service.py
create mode 100644 preprocess.py
create mode 100644 train.py
create mode 100644 utils.py
create mode 100644 vocab/chinese_vocab.model
create mode 100644 vocab/chinese_vocab.vocab
create mode 100644 vocab/vocab.json
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..f9db5e9
--- /dev/null
+++ b/README.md
@@ -0,0 +1,319 @@
+# CPM
+
+## 项目描述
+CPM(Chinese Pretrained Models)模型是北京智源人工智能研究院和清华大学发布的中文大规模预训练模型。官方发布了三种规模的模型,参数量分别为109M、334M、2.6B,用户需申请与通过审核,方可下载。
+由于原项目需要考虑大模型的训练和使用,需要安装较为复杂的环境依赖,使用上也较为复杂。
+本项目采用了109M的CPM模型(若资源允许也可以考虑334M的模型),并且简化了模型的训练和使用。
+
+本项目是基于CPM模型的中文文本生成项目,可用于作文、小说、新闻、古诗等中文生成任务,并且训练和分享了[中文作文生成模型](#model_share),取得了不错的[生成效果](#sample)。
+本项目提供了数据预处理、模型训练、文本生成、Http服务等代码模块。
+详情可参考[CPM模型论文](https://arxiv.org/abs/2012.00413), [CPM官网](https://cpm.baai.ac.cn/), [项目源码](https://github.com/TsinghuaAI/CPM-Generate) 。
+
+
+## 运行环境
+python==3.6、transformers==4.6.0、sentencepiece==0.1.94、torch==1.7.0、Flask==1.1.2
+
+
+## 项目结构
+用户可自行创建以下目录。
+- config:存放模型的配置文件
+- data:存放训练数据
+- model:存放模型
+- log:存放日志文件
+- vocab:
+ - chinese_voca.model:sentencepiece模型
+ - vocab.json:分词与id的键值对
+- data_parallel.py:解决pytorch的GPU负载不均衡的问题
+- generate.py:生成代码
+- http_service.py:封装成http服务,支持post与get请求
+- preprocess.py:数据预处理代码
+- utils.py:存放一些工具代码
+
+## 模型参数与训练细节
+由于GPU资源有限,本项目使用cpm-small.json中的模型参数,若资源充足,可尝试cpm-medium.json中的参数配置。
+
+本项目的部分模型参数如下:
+- n_ctx: 1024
+- n_embd: 768
+- n_head: 12
+- n_layer: 12
+- n_positions: 1024
+- vocab_size: 30000
+
+对26w篇作文进行预处理之后,得到60w+长度为200的训练数据。显卡为三张GTX 1080Ti,batch_size=50,三张卡显存满载,一轮训练大约需要3个小时。训练40轮之后,loss降到2.1左右,单词预测准确率大约为54%。
+
+## 使用方法
+### Quick Start
+在[模型分享](#model_share)中下载模型,将模型文件夹zuowen_epoch40放到model目录下,执行如下命令,指定作文标题、作文开头和长度,进行生成。
+```
+python generate.py --model_path model/zuowen_epoch40 --title 家乡的四季 --context 家乡的四季,最美不过了 --max_len 200
+```
+
+### 数据预处理
+每篇作文对应一个txt文件,txt内容格式如下:
+```
+---
+标题:徜徉在书籍的阳光世界
+日期:xxxx-xx-xx xx:xx:xx
+作者:佚名
+---
+
+
+一本书是一个人的眼睛,它可以让你看到另一个世界的奇妙;一本书是一个人的耳朵,它可以让你听到大自然的呼唤,听到社会的声音。
+
+《森林报》是苏联著名科普作家维。比安基的代表作品,他以春夏秋冬四季为序,有层次、有类别地将森林里动植物的新鲜事描写得栩栩如生,引人入胜。这本书教会我们如何去观察、认识大自然。这本书教会我们感悟生命,体验生动的愉快探速之旅,激发我们热爱科学的兴趣。
+
+《三字经》、《弟子规》、《论语》这样的国学经典,我也会拿来阅读,虽然似懂非懂,但读起来朗朗上口,觉得挺有趣。读着读着,好似开始了一场时空之旅,与古代圣贤结为知己,进行心与心之间的倾听与问候。这些书籍让我们在阅读中品味高雅。
+
+在成长的过程中,每个人都有着自己不一样的心路历程。阳光姐姐写的《成长的秘密》一书让我们获益不浅。作者用简单生动的文字,把温馨感人、新鲜快乐、爆笑的校园生活展现在我们眼前。书中的人物宁佳鑫看上去弱小,但她实际却很坚强,在她身上,我看到了她散发出的正能量和她在逆境中奋起的精神。她的经历告诉我:无论遇到什么样的挫折与坎坷,都不要气馁。阳光总在风雨后,只要我们坚持不懈地去想办法克服困难,并付诸行动,就一定会柳暗花明!
+
+法国作家德尔伦曾说过“智慧可以转化成力量,完成你认为不可能完成的事。”是啊,智慧的力量很强大,这些力量隐藏在书中。当我们在阅读之际,这些知识就偷偷地跑进我们的脑海里,渐渐地,渐渐地,它们就永远地保存下来,显示出无穷的魅力,让我们的未来畅通无阻。
+
+书籍,用爱和勇气唤醒每个孩子的心灵;书籍让我们感受到温暖与力量;书籍,教我们用心灵在文字间快乐舞蹈。
+
+让我们走进书籍的阳光世界,获取成长的力量。
+```
+对于每个txt文件,首先取出标题与内容,将标题与内容按照"title[sep]content[eod]"的方式拼接起来,然后对其进行tokenize,最后使用滑动窗口对内容进行截断,得到训练数据。
+运行如下命令,进行数据预处理。注:预处理之后的数据保存为train.pkl,这是一个list,list中每个元素表示一条训练数据。
+```
+python preprocess.py --data_path data/zuowen --save_path data/train.pkl --win_size 200 --step 200
+```
+超参数说明:
+- vocab_file:sentencepiece模型路径,用于tokenize
+- log_path:日志存放位置
+- data_path:数据集存放位置
+- save_path:对训练数据集进行tokenize之后的数据存放位置
+- win_size:滑动窗口的大小,相当于每条数据的最大长度
+- step:滑动窗口的滑动步幅
+
+用户也可以根据自身的需求,对预处理的代码进行相应的修改。后续将更新项目代码,以便用于处理各种数据集。
+
+### 训练模型
+运行如下命令,使用预处理后的数据训练模型。
+```
+python train.py --epochs 100 --batch_size 16 --device 0,1 --gpu0_bsz 5 --train_path data/train.pkl
+```
+超参数说明:
+- device:设置使用哪些GPU
+- no_cuda:设为True时,不使用GPU
+- vocab_path:sentencepiece模型路径,用于tokenize
+- model_config:需要从头训练一个模型时,模型参数的配置文件
+- train_path:经过预处理之后的数据存放路径
+- max_len:训练时,输入数据的最大长度。
+- log_path:训练日志存放位置
+- ignore_index:对于该token_id,不计算loss,默认为-100
+- epochs:训练的最大轮次
+- batch_size:训练的batch size
+- gpu0_bsz:pytorch使用多GPU并行训练时,存在负载不均衡的问题,即0号卡满载了,其他卡还存在很多空间,抛出OOM异常。该参数可以设置分配到0号卡上的数据数量。
+- lr:学习率
+- eps:AdamW优化器的衰减率
+- log_step:多少步汇报一次loss
+- gradient_accumulation_steps:梯度累计的步数。当显存空间不足,batch_size无法设置为较大时,通过梯度累计,缓解batch_size较小的问题。
+- save_model_path:模型输出路径
+- pretrained_model:预训练的模型的路径
+- num_workers:dataloader加载数据时使用的线程数量
+- warmup_steps:训练时的warm up步数
+
+
+### 文本生成
+运行如下命令,进行文本生成。
+```
+python generate.py --device 0 --max_len 200 --title 家乡的四季 --context 家乡的四季,最美不过了
+```
+超参数说明:
+- device:使用哪个GPU进行生成
+- temperature:详情可参考temperature sampling的思想
+- topk:top-k采样(注:topp为0,topk不为0时采用top-k采样)
+- topp:核采样(注:topk为0,topp不为0时,采用核采样)
+- max_len:生成的最长长度
+- log_path:生成日志存放位置
+- no_cuda:设为True时,不使用GPU
+- model_path:模型存放路径
+- title:作文标题
+- context:作文上文
+
+### Http服务
+将模型生成能力封装成Http服务,支持Post与Get请求。运行如下命令,启动服务。
+```
+python http_service.py --port 8085 --model_path model/zuowen_epoch40
+```
+Get请求:
+```
+http://localhost:8085/zuowen?title="家乡的四季"&context="家乡的四季,最美不过了"&max_len=200
+```
+Post请求
+```
+localhost:8085/zuowen
+{
+ 'title':'家乡的四季',
+ 'context':'家乡的四季,最美不过了',
+ 'max_len':200
+}
+```
+
+超参数说明:
+- device:使用哪个GPU进行生成
+- temperature:详情可参考temperature sampling的思想
+- topk:top-k采样(注:topp为0,topk不为0时采用top-k采样)
+- topp:核采样(注:topk为0,topp不为0时,采用核采样)
+- port:服务绑定的端口号
+- log_path:生成日志存放位置
+- no_cuda:设为True时,不使用GPU
+- model_path:模型存放路径
+
+
+
模型分享
+
+|模型 | 共享地址 |模型描述|
+|---------|--------|--------|
+|zuowen_epoch40 | [百度网盘【提取码:803v】](https://pan.baidu.com/s/1nwyqQ6WyE0mE0U6OVlThEQ) |使用26w篇中文作文语料训练了40个epoch,loss降到2.1左右,单词预测准确率大约为54%|
+
+## Future Work
+- 使用3张1080Ti进行训练,由于显卡资源有限,在数据预处理时,使用了大小为200的滑动窗口对数据进行截断,batch_size设为50。没有充分使用模型1024的最大输入长度,导致训练不够充分。若有充足的显卡资源,可以使用1024的滑动窗口对数据进行截断,提高模型的生成效果。
+- 当前代码主要针对作文数据集进行数据预处理、训练、生成。后续将会更新代码,以便用于处理各种数据集。
+
+生成样例
+以下生成样例,生成长度默认为200。
+
+### 家乡的四季
+```
+title:家乡的四季
+context:家乡的四季,最美不过了
+
+result:
+家乡的四季,最美不过了。家乡的四季,是令人沉醉的。
+春天,万物复苏,冰雪融化,万物复苏。树枝抽出了嫩芽,花朵绽放了笑脸,树木吐出了嫩芽,春笋也破土而出,像是迎接春天的到来。小鸟们也在枝头唱起了动听的歌曲,周围的一切都变成了春的样子。
+夏天,荷塘里的荷花开了,散发出阵阵清香。远处,山的颜色深浅不一,像是穿着一件翠绿的长裙,在荷塘的衬托下显得更加美,更加翠绿。微风拂过,荷花轻轻地摆动着,像是在和我打招呼呢!
+秋天,
+
+result:
+家乡的四季,最美不过了。
+家乡的春天,柳树发芽了,小草从泥土里探出头来,小花也张开了笑脸,小草偷偷地探出头来。我小时候,经常到那里玩,在那里捉迷藏,去田野里捉迷藏。到了晚上,爷爷便去田野里找蟋蟀,等到第二天早上,爷爷就去捉蟋蟀了。
+家乡的夏天,荷塘里开满了荷花,碧绿的荷叶,荷花都开了,荷叶上还有青蛙王子,他们正在开大会呢!
+家乡的秋天,果实累累,果园里更是瓜果飘香。你看,农民伯伯正忙着摘果实呢!爷爷会摘苹果,苹果熟了,
+
+result:
+家乡的四季,最美不过了。
+春天,嫩芽破土而出,焕发出生机。每当春姑娘来临之际,小草就会脱下旧衣服,冲出家门,迫不及待地站在土地上,感受春风亲吻着自己的脸庞,贪婪地吸吮着甘甜的露水。春姑娘来到田野里,到处都是一片嫩绿,一派盎然的景象。柳树姑娘刚刚梳理好头发,甩动着长长的头发,伴随着阵阵春风,跳起了欢快的舞蹈。此时此刻,春雨也来凑热闹了,她滴落在溪水中,随着春风舞动起来,漾起一圈圈水纹。在河边,长满了一串串一串串鲜艳的鲜花,
+
+result:
+家乡的四季,最美不过了,四季各有特色。
+春天,小草探出了它那绿绿的小脑袋,柳树的枝条随风飘动,好像正在给春姑娘梳头发。桃花、杏花、梨花争先恐后的开放,如同一个个粉红的小精灵在枝头跳着美丽的舞蹈。小燕子从南方飞来,在空中快乐的飞来飞去,非常动听。
+夏天,骄阳似火,树木葱葱笼,在骄阳的照耀下,鸟儿也在树上唱着动听的歌。小孩子们穿着短袖,在大树下坐着乘凉,偶尔会出现几个小朋友在那里捉迷藏,嬉戏。
+秋天,
+
+result:
+家乡的四季,最美不过了,我家乡的四季是如此美丽。
+春天到了,小草从泥土里钻出来了,正东张西望地观察着四周,像是在寻找着什么。大树也绽开了笑脸,开出了许多颜色各异的花,有黄色、红色、紫色、绿色,真是色色俱全啊!花儿在春雨的滋润下,绽放出了自己美丽的花朵,散发出了迷人的芳香,那花儿就像一位位亭亭玉立的少女,娇艳迷人,美丽极了。那嫩绿的小草,铺满了大地,让我们感到生命的希望。
+夏天,小草长得郁郁葱葱,到处都是绿茵茵的,走在路上,
+
+result:
+家乡的四季,最美不过了。
+春天,到处充满了生机勃勃。风和日丽,万物复苏,柳树那碧绿的头发被风吹得翩翩起舞,像一个亭亭玉立的少女在对我招手。
+夏天,太阳高高地挂在天空,灿烂的阳光照耀着大地,我看见农民伯伯忙碌的身影,心想:这么热的天,还要干什么?我要帮助他们干农活。我想着想着,又想到了奶奶家,我就跑到奶奶家的西瓜地里,去拿奶奶的小锄头,把小锄头递到奶奶的手里,奶奶一边干活,一边说:“你可真棒!”
+秋天,
+
+result:
+家乡的四季,最美不过了。
+春天到了,花儿苏醒了,小草冒出了头,树木抽出了新的枝条,燕子又飞回来了。远处,连绵起伏的高山一座连着一座,就像一座座大山。山下有一条清澈的小溪,水哗啦啦地流着,就像一匹美丽的蓝丝绸。山上的树木也抽出了新的枝条,长出了嫩绿的叶子,叶子好像一块块绿宝石。燕子从南方飞回来了,站在枝头上,叽叽喳喳地叫着,好像在唱着春天的赞歌。
+夏天到了,太阳像个大火球,照着地面,我和小伙伴们经常到小溪里游泳、打水仗,
+
+result:
+家乡的四季,最美不过了。
+春天,那条河流解冻了。岸边的柳树随风飘动,那优美的身姿,加上那动人的歌喉,简直是春姑娘的杰作。小燕子从南方赶来,叽叽喳喳的叫着,好像在和柳树说:“柳树姐姐,你的头发可真美呀!”小草偷偷的从土里钻了出来,那嫩嫩的、绿绿的,就像刚打过仗的战士一样。花儿们,现在都刚刚长出来,那些花,就像刚睡醒的小婴儿,那些花,有红的、黄的、紫的......五彩缤纷,美丽极了。夏天,
+
+result:
+家乡的四季,最美不过了,无论是万物复苏的春天,还是烈日炎炎的夏天,抑或是硕果累累的秋天,每一个季节都有它们的特点。
+春姑娘刚走,夏姐姐就来了。太阳公公热情的烘烤着大地,知了在树上“知了知了”的叫着,仿佛在说:“好热啊!好热啊!”荷花池塘也“出淤泥而不染”,荷叶“接天莲叶无穷碧”的的清香随风飘来,整个家乡弥漫着沁人心脾的花香,让人陶醉。
+春姑娘刚刚走,夏姑娘就来了。太阳像一个又大又圆的火球挂在天空中,
+```
+
+### 徜徉在书籍的阳光世界
+```
+title: 徜徉在书籍的阳光世界
+contetx: 一本书是一个人的眼睛,它可以让你看到另一个世界的奇妙
+
+result:
+一本书是一个人的眼睛,它可以让你看到另一个世界的奇妙。
+你可曾品尝过文学的魅力?
+唐诗宋词,群星璀璨,给我们无尽的想象与愉悦。
+“明月几时有,把酒问青天。不知天上宫阙,今夕是何年。”它描绘了一个美好的时代。苏轼在赤壁赏月时,不禁为这美景感叹。“明月几时有,把酒问青天。”它告诉了我们人生的哲理。
+文学作品,不但丰富了我们的知识,也为我们描绘了一幅幅优美的山水画。
+语文书中的婉约柔情,让我感受到世间的人情冷暖,
+
+result:
+一本书是一个人的眼睛,它可以让你看到另一个世界的奇妙;一本好书是一个人的眸子,它可以让你看清世界的脉络;一本好书是一把钥匙,它可以打开你心灵的窗户。我徜徉在书的世界里,在阅读中,我找到了梦想。
+一本好书,犹如一泓清泉,流入我干渴的心田;一本好书,犹如一只小舟,载着我遨游在知识的海洋;一本好书,犹如一缕阳光,照亮我的心房。
+记得在我很小的时候,我每天都要缠着妈妈给我讲故事,每次妈妈讲完故事,我都会依偎在妈妈的怀里,
+
+result:
+一本书是一个人的眼睛,它可以让你看到另一个世界的奇妙;一本书是一场细雨,滋润你的心田;一本书是你的拐杖,带你走进这个美妙的世界。
+在我很小的时候,就开始接触书籍了,我有一个非常要好的朋友,叫做书。在我很小的时候,书还是不可缺少的。
+在我不认字的时候,我就会捧着《格林童话》,开始认真地看书,我看的津津有味。《格林童话》让我明白了做人的道理,《白雪公主》让我知道了善良的重要;《卖火柴的小女孩》让我明白了人间的幸福是美好的,
+
+result:
+一本书是一个人的眼睛,它可以让你看到另一个世界的奇妙。书就像是一颗闪烁的星星,给你引航;书就像一汪清泉,给你洗涤心灵;书就像一束阳光,给你带来无穷的温暖......
+我从小就喜欢读书。一个冬天的下午,我在家楼下的小广场上坐着,静静地享受着小时候的乐趣。突然,一位老爷爷从远处走了过来,手里拿着一本厚厚的《安徒生童话》,我拿起这本书,心想:这书可是我的心爱之物啊!
+于是,我跑到他身边,与他交谈起来。原来,这位老爷爷就是在我六岁时,
+
+reslut:
+一本书是一个人的眼睛,它可以让你看到另一个世界的奇妙,每一本都有着不一样的内涵。
+——题记
+在某个宁静的午后,沉醉在书本的世界里,沉醉在阅读的魅力里,沉醉在阅读的心灵深处。
+坐在一望无际的草原上,静静地读书。我像一匹饿狼,贪婪地读着,不一会儿,我就沉浸在书中。不知不觉,太阳已落下去,不知不觉,天色已晚,我们只好依依不舍地收起书本。
+夕阳西下,落日把天空染成了红色,火烧云像一只只巨象,汹涌澎湃,在天空中横飞,
+
+result:
+一本书是一个人的眼睛,它可以让你看到另一个世界的奇妙之处,更能让你认识到书本的神奇之处,而我则常常沉浸在那散发着淡淡书香的阅读之中。
+从幼儿园开始,我便爱上了读书,最开始是读绘本,后来爱上了古诗文。记得,一开始是爸爸妈妈带我去书店买诗。“床前明月光,疑是地上霜。举头望明月,低头思故乡。”那时,我对诗的理解是这样的:月光皎洁的夜晚,举头望明月,低头思故乡。“水光,山色空蒙雨亦奇。”那时的我对诗的理解是“帘卷西风寒鸦知人不识月,
+
+result1:
+一本书是一个人的眼睛,它可以让你看到另一个世界的奇妙。书,是一个精神世界的源泉,在我们的精神世界里,书是一位无声的老师,也是一个最忠实的朋友。
+书是人们的良师益友,是精神世界的指南针,有了书,我们便知道了知识,有了知识,才会使我们做事情变得更有趣味,有了书,我们才能做更多的事。书是我的伴侣,书是我的好老师。
+高尔基曾经说过:"书籍是人类进步的阶梯。莎士比亚曾经说过"书籍是全世界的营养品"
+
+```
+
+### 我最敬佩的一个人
+```
+title: 我最敬佩的一个人
+context:在我的生活中,有外冷内热的妈妈,有拼命工作的爸爸
+
+result:
+在我的生活中,有外冷内热的妈妈,有拼命工作的爸爸,还有勤劳朴素的爷爷奶奶。但是,我最敬佩的是我的妈妈。
+我的妈妈,有一双炯炯有神的眼睛,高高的鼻梁上架着一副眼镜,她很爱笑,很爱笑。妈妈的头发非常多,细长的柳叶眉下镶嵌着一双炯炯有神的眼睛,好像一个宝石。妈妈长的高高的个子,鼻梁上架着一副眼镜。
+
+result:
+在我的生活中,有外冷内热的妈妈,有拼命工作的爸爸,有工作努力的奶奶,有勤奋好学的姐姐......但是,我最敬佩的是那位平凡的清洁工人。
+一天,我和妈妈一起乘坐公交车回家。到达了车站,我和妈妈下了车,就急匆匆地跑向家附近的早餐店。吃完早餐后,我们正准备上公交车,可是我发现有一位清洁工老爷爷正在寒风中扫地。他身穿一件单薄的衣服,衣服上沾满了灰尘,他却是用手把垃圾一点一点地扫起来,垃圾车井然有序地行驶着。我心想:这位清洁工真不容易啊!
+
+result:
+在我的生活中,有外冷内热的妈妈,有拼命工作的爸爸,有幽默搞笑的爷爷,有坚持不懈的老师......但是,我最敬佩的人是我的奶奶。
+我的奶奶非常爱美,喜欢穿红色衣服。有一天,奶奶过生日,我早早地起了床,迫不及待地对奶奶说:“奶奶,奶奶,祝你生日快乐,身体健康,万事如意!”奶奶开心地说:“谢谢你,宝贝,你真是长大了。”
+我的奶奶又很勤劳。她很会做家务活,做家务活,她把家里打扫得干干净净,就像一个小家一样。她不仅把家里打扫得干净,
+
+result:
+在我的生活中,有外冷内热的妈妈,有拼命工作的爸爸,有知识渊博的爷爷,但是我最敬佩的还是我的妈妈。
+我的妈妈长着一头乌黑发亮的头发,又短又黑,一双炯炯有神的大眼睛,笑起来特别好看,小小的嘴巴一笑起来就露出两个甜甜的酒窝,非常迷人。
+妈妈的个子比较高,还稍微有点胖,这都是为什么妈妈很胖的原因。但是妈妈每天都非常累,她是一个非常勤劳的人,每天都要很早起床,为了家人做出更多的早餐,她总是天不亮就起床,然后再叫醒我。
+
+result:
+在我的生活中,有外冷内热的妈妈,有拼命工作的爸爸,还有日夜奔波的老师......而我最敬佩的人就是我的英语老师--彭老师。
+她有一头乌黑亮丽的长发,弯弯的眉毛下面是一双炯炯有神的大眼睛。上课时,她的声音很小,经常在黑板上点出枯燥的英语单词。如果我们在写字,她就用眼睛一遍一遍地望着我们,就像在注视着自己的孩子一样。彭老师十分严格。
+
+result:
+在我的生活中,有外冷内热的妈妈,有拼命工作的爸爸,还有一个吃苦耐劳的爷爷,但是最令我敬佩的是我的爷爷。
+爷爷的身高已经一米七了,已经有80多岁了,他虽然已经退休了,但仍然坚持每天给我做可口的饭菜,陪我玩耍,照顾我的生活起居,而且还坚持每天接送我上下学,爸爸妈妈很爱我。
+我的爷爷长着一张圆圆的脸,一双炯炯有神的眼睛,
+
+result:
+在我的生活中,有外冷内热的妈妈,有拼命工作的爸爸"我最敬佩的一个人",其中,有一个人我最敬佩。
+她长着一双炯炯有神的眼睛,高高的鼻梁上架着一副黑色的眼镜,给人一种文雅大气的感觉,一张樱桃小嘴一张一合,给人一种读书的感觉,她就是我的妈妈。
+我的妈妈是一个喜欢化妆的人。她每次都会把自己打扮得漂漂亮亮的,
+
+result:
+在我的生活中,有外冷内热的妈妈,有拼命工作的爸爸,但最敬佩我的哥哥。
+哥哥是一个人见人爱,花见花开,车见车爆胎的卖菜小贩。哥哥的头上会扎成一个三角形,眉毛下面长着一双明亮的大眼睛。鼻子很小巧,还有一个樱桃小嘴。他的嘴巴虽然小,但是能说会道。你看,他的脸蛋上还长了一对小酒窝。
+```
+
+
diff --git a/config/cpm-medium.json b/config/cpm-medium.json
new file mode 100644
index 0000000..83741ba
--- /dev/null
+++ b/config/cpm-medium.json
@@ -0,0 +1,33 @@
+{
+ "activation_function": "gelu_new",
+ "architectures": [
+ "GPT2LMHeadModel"
+ ],
+ "attn_pdrop": 0.1,
+ "bos_token_id": 1,
+ "embd_pdrop": 0.1,
+ "eos_token_id": 2,
+ "initializer_range": 0.02,
+ "layer_norm_epsilon": 1e-05,
+ "model_type": "gpt2",
+ "n_ctx": 1024,
+ "n_embd": 1024,
+ "n_head": 16,
+ "n_layer": 24,
+ "n_positions": 1024,
+ "n_special": 0,
+ "predict_special_tokens": true,
+ "resid_pdrop": 0.1,
+ "summary_activation": null,
+ "summary_first_dropout": 0.1,
+ "summary_proj_to_labels": true,
+ "summary_type": "cls_index",
+ "summary_use_proj": true,
+ "task_specific_params": {
+ "text-generation": {
+ "do_sample": true,
+ "max_length": 50
+ }
+ },
+ "vocab_size": 30000
+}
\ No newline at end of file
diff --git a/config/cpm-small.json b/config/cpm-small.json
new file mode 100644
index 0000000..55da239
--- /dev/null
+++ b/config/cpm-small.json
@@ -0,0 +1,31 @@
+{
+ "activation_function": "gelu_new",
+ "architectures": [
+ "GPT2LMHeadModel"
+ ],
+ "attn_pdrop": 0.1,
+ "bos_token_id": 50256,
+ "embd_pdrop": 0.1,
+ "eos_token_id": 50256,
+ "initializer_range": 0.02,
+ "layer_norm_epsilon": 1e-05,
+ "model_type": "gpt2",
+ "n_ctx": 1024,
+ "n_embd": 768,
+ "n_head": 12,
+ "n_layer": 12,
+ "n_positions": 1024,
+ "resid_pdrop": 0.1,
+ "summary_activation": null,
+ "summary_first_dropout": 0.1,
+ "summary_proj_to_labels": true,
+ "summary_type": "cls_index",
+ "summary_use_proj": true,
+ "task_specific_params": {
+ "text-generation": {
+ "do_sample": true,
+ "max_length": 50
+ }
+ },
+ "vocab_size": 30000
+}
\ No newline at end of file
diff --git a/data_parallel.py b/data_parallel.py
new file mode 100644
index 0000000..85020b2
--- /dev/null
+++ b/data_parallel.py
@@ -0,0 +1,109 @@
+
+from torch.nn.parallel import DataParallel
+import torch
+from torch.nn.parallel._functions import Scatter
+from torch.nn.parallel.parallel_apply import parallel_apply
+
+def scatter(inputs, target_gpus, chunk_sizes, dim=0):
+ r"""
+ Slices tensors into approximately equal chunks and
+ distributes them across given GPUs. Duplicates
+ references to objects that are not tensors.
+ """
+ def scatter_map(obj):
+ if isinstance(obj, torch.Tensor):
+ try:
+ return Scatter.apply(target_gpus, chunk_sizes, dim, obj)
+ except:
+ print('obj', obj.size())
+ print('dim', dim)
+ print('chunk_sizes', chunk_sizes)
+ quit()
+ if isinstance(obj, tuple) and len(obj) > 0:
+ return list(zip(*map(scatter_map, obj)))
+ if isinstance(obj, list) and len(obj) > 0:
+ return list(map(list, zip(*map(scatter_map, obj))))
+ if isinstance(obj, dict) and len(obj) > 0:
+ return list(map(type(obj), zip(*map(scatter_map, obj.items()))))
+ return [obj for targets in target_gpus]
+
+ # After scatter_map is called, a scatter_map cell will exist. This cell
+ # has a reference to the actual function scatter_map, which has references
+ # to a closure that has a reference to the scatter_map cell (because the
+ # fn is recursive). To avoid this reference cycle, we set the function to
+ # None, clearing the cell
+ try:
+ return scatter_map(inputs)
+ finally:
+ scatter_map = None
+
+def scatter_kwargs(inputs, kwargs, target_gpus, chunk_sizes, dim=0):
+ r"""Scatter with support for kwargs dictionary"""
+ inputs = scatter(inputs, target_gpus, chunk_sizes, dim) if inputs else []
+ kwargs = scatter(kwargs, target_gpus, chunk_sizes, dim) if kwargs else []
+ if len(inputs) < len(kwargs):
+ inputs.extend([() for _ in range(len(kwargs) - len(inputs))])
+ elif len(kwargs) < len(inputs):
+ kwargs.extend([{} for _ in range(len(inputs) - len(kwargs))])
+ inputs = tuple(inputs)
+ kwargs = tuple(kwargs)
+ return inputs, kwargs
+
+class BalancedDataParallel(DataParallel):
+ def __init__(self, gpu0_bsz, *args, **kwargs):
+ self.gpu0_bsz = gpu0_bsz
+ super().__init__(*args, **kwargs)
+
+ def forward(self, *inputs, **kwargs):
+ if not self.device_ids:
+ return self.module(*inputs, **kwargs)
+ if self.gpu0_bsz == 0:
+ device_ids = self.device_ids[1:]
+ else:
+ device_ids = self.device_ids
+ inputs, kwargs = self.scatter(inputs, kwargs, device_ids)
+
+ # print('len(inputs): ', str(len(inputs)))
+ # print('self.device_ids[:len(inputs)]', str(self.device_ids[:len(inputs)]))
+
+ if len(self.device_ids) == 1:
+ return self.module(*inputs[0], **kwargs[0])
+ if self.gpu0_bsz == 0:
+ replicas = self.replicate(self.module, self.device_ids)
+ else:
+ replicas = self.replicate(self.module, self.device_ids[:len(inputs)])
+
+ # replicas = self.replicate(self.module, device_ids[:len(inputs)])
+ if self.gpu0_bsz == 0:
+ replicas = replicas[1:]
+
+ # print('replicas:', str(len(replicas)))
+
+ outputs = self.parallel_apply(replicas, device_ids, inputs, kwargs)
+ return self.gather(outputs, self.output_device)
+
+ def parallel_apply(self, replicas, device_ids, inputs, kwargs):
+ return parallel_apply(replicas, inputs, kwargs, device_ids[:len(inputs)])
+
+ def scatter(self, inputs, kwargs, device_ids):
+ bsz = inputs[0].size(self.dim)
+ num_dev = len(self.device_ids)
+ gpu0_bsz = self.gpu0_bsz
+ bsz_unit = (bsz - gpu0_bsz) // (num_dev - 1)
+ if gpu0_bsz < bsz_unit:
+ chunk_sizes = [gpu0_bsz] + [bsz_unit] * (num_dev - 1)
+ delta = bsz - sum(chunk_sizes)
+ for i in range(delta):
+ chunk_sizes[i + 1] += 1
+ if gpu0_bsz == 0:
+ chunk_sizes = chunk_sizes[1:]
+ else:
+ return super().scatter(inputs, kwargs, device_ids)
+
+ # print('bsz: ', bsz)
+ # print('num_dev: ', num_dev)
+ # print('gpu0_bsz: ', gpu0_bsz)
+ # print('bsz_unit: ', bsz_unit)
+ # print('chunk_sizes: ', chunk_sizes)
+ return scatter_kwargs(inputs, kwargs, device_ids, chunk_sizes, dim=self.dim)
+
diff --git a/dataset.py b/dataset.py
new file mode 100644
index 0000000..681ec8f
--- /dev/null
+++ b/dataset.py
@@ -0,0 +1,21 @@
+from torch.utils.data import Dataset
+import torch
+
+
+class CPMDataset(Dataset):
+ """
+
+ """
+
+ def __init__(self, input_list, max_len):
+ self.input_list = input_list
+ self.max_len = max_len
+
+ def __getitem__(self, index):
+ input_ids = self.input_list[index]
+ input_ids = input_ids[:self.max_len]
+ input_ids = torch.tensor(input_ids, dtype=torch.long)
+ return input_ids
+
+ def __len__(self):
+ return len(self.input_list)
diff --git a/generate.py b/generate.py
new file mode 100644
index 0000000..ab88095
--- /dev/null
+++ b/generate.py
@@ -0,0 +1,124 @@
+import torch
+import torch.nn.functional as F
+import os
+import argparse
+from tqdm import trange
+from transformers import GPT2LMHeadModel, GPT2Config, CpmTokenizer
+from utils import top_k_top_p_filtering, set_logger
+from os.path import join, exists
+
+
+def generate_next_token(input_ids):
+ """
+ 对于给定的上文,生成下一个单词
+ """
+ outputs = model(input_ids=input_ids)
+ logits = outputs.logits
+ # next_token_logits表示最后一个token的hidden_state对应的prediction_scores,也就是模型要预测的下一个token的概率
+ next_token_logits = logits[0, -1, :]
+ next_token_logits = next_token_logits / args.temperature
+ # 对于的概率设为无穷小,也就是说模型的预测结果不可能是[UNK]这个token
+ next_token_logits[unk_id] = -float('Inf')
+ filtered_logits = top_k_top_p_filtering(next_token_logits, top_k=args.topk, top_p=args.topp)
+ # torch.multinomial表示从候选集合中选出无放回地进行抽取num_samples个元素,权重越高,抽到的几率越高,返回元素的下标
+ next_token_id = torch.multinomial(F.softmax(filtered_logits, dim=-1), num_samples=1)
+ return next_token_id
+
+
+def generate(max_len):
+ # 对title与context进行tokenize
+ title_ids = tokenizer.encode(title, add_special_tokens=False)
+ context_ids = tokenizer.encode(context, add_special_tokens=False)
+ input_ids = title_ids + [sep_id] + context_ids
+ cur_len = len(input_ids)
+ last_token_id = input_ids[-1] # 已生成的内容的最后一个token
+ input_ids = torch.tensor([input_ids], dtype=torch.long, device=device)
+
+ while True:
+ next_token_id = generate_next_token(input_ids)
+ input_ids = torch.cat((input_ids, next_token_id.unsqueeze(0)), dim=1)
+ cur_len += 1
+ word = tokenizer.convert_ids_to_tokens(next_token_id.item())
+ # if cur_len >= max_len:
+ # break
+ # 超过最大长度,并且换行
+ if cur_len >= max_len and last_token_id == 8 and next_token_id == 3:
+ break
+ # 超过最大长度,并且生成标点符号
+ if cur_len >= max_len and word in [".", "。", "!", "!", "?", "?", ",", ","]:
+ break
+ # 生成结束符
+ if next_token_id == eod_id:
+ break
+ result = tokenizer.decode(input_ids.squeeze(0))
+ return result
+
+
+if __name__ == '__main__':
+ # 参数设置
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--device', default='0', type=str, required=False, help='生成设备')
+ parser.add_argument('--temperature', default=1, type=float, required=False, help='生成温度')
+ parser.add_argument('--topk', default=0, type=int, required=False, help='最高几选一')
+ parser.add_argument('--topp', default=0.85, type=float, required=False, help='最高积累概率')
+ parser.add_argument('--repetition_penalty', default=1.0, type=float, required=False, help='重复惩罚参数')
+ parser.add_argument('--max_len', default=200, type=int, required=False, help='生成的最长长度')
+ parser.add_argument('--log_path', default='log/generate.log', type=str, required=False, help='日志存放位置')
+ parser.add_argument('--no_cuda', action='store_true', help='不使用GPU进行预测')
+ parser.add_argument('--model_path', type=str, default='model/zuowen_epoch40', help='模型存放位置')
+ # parser.add_argument('--title', type=str, default='徜徉在书籍的阳光世界', help='作文标题')
+ # parser.add_argument('--context', type=str, default='一本书是一个人的眼睛,它可以让你看到另一个世界的奇妙', help='作文上文')
+ parser.add_argument('--title', type=str, default='家乡的四季', help='作文标题')
+ parser.add_argument('--context', type=str, default='家乡的四季,最美不过了', help='作文上文')
+ args = parser.parse_args()
+
+ os.environ["CUDA_VISIBLE_DEVICES"] = args.device # 此处设置程序使用哪些显卡
+ args.cuda = torch.cuda.is_available() and not args.no_cuda # 当用户使用GPU,并且GPU可用时
+ device = 'cuda:0' if args.cuda else 'cpu'
+ # device = 'cpu'
+
+ # 创建日志对象
+ logger = set_logger(args.log_path)
+
+ # 初始化tokenizer
+ tokenizer = CpmTokenizer(vocab_file="vocab/chinese_vocab.model")
+ eod_id = tokenizer.convert_tokens_to_ids("") # 文档结束符
+ sep_id = tokenizer.sep_token_id
+ unk_id = tokenizer.unk_token_id
+
+ # 加载模型
+ model = GPT2LMHeadModel.from_pretrained(args.model_path)
+ model.eval()
+ model = model.to(device)
+
+ title = args.title
+ context = args.context
+ logger.info("title:{}".format(title))
+ logger.info("context:{}".format(context))
+
+ # 开始生成
+ result = generate(args.max_len)
+ result = result.split("")[1]
+ logger.info("result:{}\n".format(result))
+
+ # 通过控制台循环生成
+ # print('开始生成,输入CTRL + Z以退出')
+ # while True:
+ # try:
+ # # 用户输入title与context
+ # title = input("请输入作文标题:")
+ # context = input("请输入作文起始句子:")
+ #
+ # logger.info("title:{}".format(title))
+ # logger.info("context:{}".format(context))
+ #
+ # # 开始生成
+ # result = generate(args.max_len)
+ # result = result.split("")[1]
+ # logger.info("result:{}\n".format(result))
+ # break
+ #
+ # except KeyboardInterrupt:
+ # break
+
+
diff --git a/http_service.py b/http_service.py
new file mode 100644
index 0000000..0dedec8
--- /dev/null
+++ b/http_service.py
@@ -0,0 +1,108 @@
+import torch
+import torch.nn.functional as F
+import os
+import argparse
+from tqdm import trange
+from transformers import GPT2LMHeadModel, CpmTokenizer
+from utils import top_k_top_p_filtering, set_logger
+from os.path import join
+from flask import Flask, redirect, url_for, request
+app = Flask(__name__)
+app.config["JSON_AS_ASCII"] = False # 防止返回中文乱码
+
+
+def generate_next_token(input_ids):
+ """
+ 对于给定的上文,生成下一个单词
+ """
+ outputs = model(input_ids=input_ids)
+ logits = outputs.logits
+ # next_token_logits表示最后一个token的hidden_state对应的prediction_scores,也就是模型要预测的下一个token的概率
+ next_token_logits = logits[0, -1, :]
+ next_token_logits = next_token_logits / args.temperature
+ # 对于的概率设为无穷小,也就是说模型的预测结果不可能是[UNK]这个token
+ next_token_logits[unk_id] = -float('Inf')
+ filtered_logits = top_k_top_p_filtering(next_token_logits, top_k=args.topk, top_p=args.topp)
+ # torch.multinomial表示从候选集合中选出无放回地进行抽取num_samples个元素,权重越高,抽到的几率越高,返回元素的下标
+ next_token_id = torch.multinomial(F.softmax(filtered_logits, dim=-1), num_samples=1)
+ return next_token_id
+
+
+@app.route('/zuowen', methods=['POST', 'GET'])
+def zuowen():
+ if request.method == 'POST':
+ data = request.get_json()
+ title = data['title']
+ context = data['context']
+ max_len = data['max_len']
+ elif request.method == 'GET':
+ title = request.args.get('title', type=str)
+ context = request.args.get('context', type=str)
+ max_len = request.args.get('max_len', type=int)
+
+ # print("title:{}".format(title))
+ # print("context:{}".format(context))
+ logger.info("receive request,title:{}, context:{}".format(title, context))
+
+ title_ids = tokenizer.encode(title, add_special_tokens=False)
+ context_ids = tokenizer.encode(context, add_special_tokens=False)
+ input_ids = title_ids + [sep_id] + context_ids
+ cur_len = len(input_ids)
+ last_token_id = input_ids[-1] # 已生成的内容的最后一个token
+ input_ids = torch.tensor([input_ids], dtype=torch.long, device=device)
+
+ while True:
+ next_token_id = generate_next_token(input_ids)
+ input_ids = torch.cat((input_ids, next_token_id.unsqueeze(0)), dim=1)
+ cur_len += 1
+ word = tokenizer.convert_ids_to_tokens(next_token_id.item())
+ # 超过最大长度,并且换行
+ if cur_len >= max_len and last_token_id == 8 and next_token_id == 3:
+ break
+ # 超过最大长度,并且生成标点符号
+ if cur_len >= max_len and word in [".", "。", "!", "!", "?", "?", ",", ","]:
+ break
+ # 生成结束符
+ if next_token_id == eod_id:
+ break
+ result = tokenizer.decode(input_ids.squeeze(0))
+ content = result.split("")[1] # 生成的最终内容
+ result = {"title": title, "content": content}
+ logger.info("generated result:{}".format(result))
+ return result
+
+
+if __name__ == '__main__':
+ # 参数设置
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--device', default='1', type=str, required=False, help='生成设备')
+ parser.add_argument('--temperature', default=1, type=float, required=False, help='生成温度')
+ parser.add_argument('--topk', default=0, type=int, required=False, help='最高几选一')
+ parser.add_argument('--topp', default=0.85, type=float, required=False, help='最高积累概率')
+ # parser.add_argument('--repetition_penalty', default=1.0, type=float, required=False, help='重复惩罚参数')
+ parser.add_argument('--port', type=int, default=8085, help='服务绑定的端口号')
+ parser.add_argument('--log_path', default='log/http_service.log', type=str, required=False, help='日志存放位置')
+ parser.add_argument('--no_cuda', action='store_true', help='不使用GPU进行预测')
+ parser.add_argument('--model_path', type=str, default='model/zuowen_epoch40', help='模型存放位置')
+ args = parser.parse_args()
+
+ os.environ["CUDA_VISIBLE_DEVICES"] = args.device # 此处设置程序使用哪些显卡
+ args.cuda = torch.cuda.is_available() and not args.no_cuda # 当用户使用GPU,并且GPU可用时
+ device = 'cuda:0' if args.cuda else 'cpu'
+ # device = 'cpu'
+
+ # 创建日志对象
+ logger = set_logger(args.log_path)
+
+ # 加载tokenizer
+ tokenizer = CpmTokenizer(vocab_file="vocab/chinese_vocab.model")
+ eod_id = tokenizer.convert_tokens_to_ids("") # 文档结束符
+ sep_id = tokenizer.sep_token_id
+ unk_id = tokenizer.unk_token_id
+
+ # 加载模型
+ model = GPT2LMHeadModel.from_pretrained(args.model_path)
+ model.eval()
+ model = model.to(device)
+
+ app.run(debug=True, host="0.0.0.0", port=args.port)
diff --git a/preprocess.py b/preprocess.py
new file mode 100644
index 0000000..fd5b43d
--- /dev/null
+++ b/preprocess.py
@@ -0,0 +1,73 @@
+import argparse
+from utils import set_logger
+from transformers import CpmTokenizer
+import os
+import pickle
+from tqdm import tqdm
+
+
+def preprocess():
+ """
+ 对故事数据集进行预处理
+ """
+ # 设置参数
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--vocab_file', default='vocab/chinese_vocab.model', type=str, required=False,
+ help='词表路径')
+ parser.add_argument('--log_path', default='log/preprocess.log', type=str, required=False, help='日志存放位置')
+ parser.add_argument('--data_path', default='data/zuowen', type=str, required=False, help='数据集存放位置')
+ parser.add_argument('--save_path', default='data/train.pkl', type=str, required=False, help='对训练数据集进行tokenize之后的数据存放位置')
+ parser.add_argument('--win_size', default=200, type=int, required=False, help='滑动窗口的大小,相当于每条数据的最大长度')
+ parser.add_argument('--step', default=200, type=int, required=False, help='滑动窗口的滑动步幅')
+ args = parser.parse_args()
+
+ # 初始化日志对象
+ logger = set_logger(args.log_path)
+
+ # 初始化tokenizer
+ tokenizer = CpmTokenizer(vocab_file="vocab/chinese_vocab.model")
+ eod_id = tokenizer.convert_tokens_to_ids("") # 文档结束符
+ sep_id = tokenizer.sep_token_id
+
+ # 读取作文数据集目录下的所有文件
+ train_list = []
+ logger.info("start tokenizing data")
+ for file in tqdm(os.listdir(args.data_path)):
+ file = os.path.join(args.data_path, file)
+ with open(file, "r", encoding="utf8")as reader:
+ lines = reader.readlines()
+ title = lines[1][3:].strip() # 取出标题
+ lines = lines[7:] # 取出正文内容
+ article = ""
+ for line in lines:
+ if line.strip() != "": # 去除换行
+ article += line
+ title_ids = tokenizer.encode(title, add_special_tokens=False)
+ article_ids = tokenizer.encode(article, add_special_tokens=False)
+ token_ids = title_ids + [sep_id] + article_ids + [eod_id]
+ # train_list.append(token_ids)
+
+ # 对于每条数据,使用滑动窗口对其进行截断
+ win_size = args.win_size
+ step = args.step
+ start_index = 0
+ end_index = win_size
+ data = token_ids[start_index:end_index]
+ train_list.append(data)
+ start_index += step
+ end_index += step
+ while end_index+50 < len(token_ids): # 剩下的数据长度,大于或等于50,才加入训练数据集
+ data = token_ids[start_index:end_index]
+ train_list.append(data)
+ start_index += step
+ end_index += step
+
+ # 序列化训练数据
+ with open(args.save_path, "wb") as f:
+ pickle.dump(train_list, f)
+
+
+if __name__ == '__main__':
+ preprocess()
+
+
diff --git a/train.py b/train.py
new file mode 100644
index 0000000..b677aeb
--- /dev/null
+++ b/train.py
@@ -0,0 +1,298 @@
+import argparse
+import math
+import time
+import torch
+import torch.nn.functional as F
+import torch.optim as optim
+import logging
+from datetime import datetime
+import os
+from torch.utils.data import Dataset, DataLoader
+from os.path import join, exists
+from torch.nn import CrossEntropyLoss
+from tqdm import tqdm
+from torch.nn import DataParallel
+import transformers
+import pickle
+import sys
+from utils import set_logger, set_random_seed
+from sklearn.model_selection import train_test_split
+from data_parallel import BalancedDataParallel
+from transformers import GPT2LMHeadModel, GPT2Config, CpmTokenizer
+import pandas as pd
+import torch.nn.utils.rnn as rnn_utils
+import numpy as np
+from dataset import CPMDataset
+
+
+def set_args():
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--device', default='0,1', type=str, required=False, help='设置使用哪些显卡')
+ parser.add_argument('--no_cuda', action='store_true', help='不使用GPU进行训练')
+ parser.add_argument('--vocab_path', default='vocab/chinese_vocab.model', type=str, required=False,
+ help='sp模型路径')
+ parser.add_argument('--model_config', default='config/cpm-small.json', type=str, required=False,
+ help='需要从头训练一个模型时,模型参数的配置文件')
+ parser.add_argument('--train_path', default='data/train.pkl', type=str, required=False, help='经过预处理之后的数据存放路径')
+ parser.add_argument('--max_len', default=200, type=int, required=False, help='训练时,输入数据的最大长度')
+
+ parser.add_argument('--log_path', default='log/train.log', type=str, required=False, help='训练日志存放位置')
+ parser.add_argument('--ignore_index', default=-100, type=int, required=False, help='对于ignore_index的label token不计算梯度')
+ parser.add_argument('--epochs', default=100, type=int, required=False, help='训练的最大轮次')
+ parser.add_argument('--batch_size', default=16, type=int, required=False, help='训练的batch size')
+ parser.add_argument('--gpu0_bsz', default=6, type=int, required=False, help='0号卡的batch size')
+ parser.add_argument('--lr', default=1.5e-4, type=float, required=False, help='学习率')
+ parser.add_argument('--eps', default=1.0e-09, type=float, required=False, help='AdamW优化器的衰减率')
+ parser.add_argument('--log_step', default=1, type=int, required=False, help='多少步汇报一次loss')
+ parser.add_argument('--gradient_accumulation_steps', default=6, type=int, required=False, help='梯度积累的步数')
+ parser.add_argument('--max_grad_norm', default=1.0, type=float, required=False)
+ parser.add_argument('--save_model_path', default='model', type=str, required=False,
+ help='模型输出路径')
+ parser.add_argument('--pretrained_model', default='model/zuowen_epoch40', type=str, required=False,
+ help='预训练的模型的路径')
+ parser.add_argument('--seed', type=int, default=1234, help='设置随机种子')
+ parser.add_argument('--num_workers', type=int, default=0, help="dataloader加载数据时使用的线程数量")
+ # parser.add_argument('--patience', type=int, default=0, help="用于early stopping,设为0时,不进行early stopping.early stop得到的模型的生成效果不一定会更好。")
+ parser.add_argument('--warmup_steps', type=int, default=4000, help='warm up步数')
+ # parser.add_argument('--label_smoothing', default=True, action='store_true', help='是否进行标签平滑')
+ args = parser.parse_args()
+ return args
+
+
+def collate_fn(batch):
+ input_ids = rnn_utils.pad_sequence(batch, batch_first=True, padding_value=5)
+ labels = rnn_utils.pad_sequence(batch, batch_first=True, padding_value=-100)
+ return input_ids, labels
+
+
+def load_dataset(logger, args):
+ """
+ 加载训练集
+ """
+ logger.info("loading training dataset")
+ train_path = args.train_path
+
+ with open(train_path, "rb") as f:
+ train_list = pickle.load(f)
+
+ # test
+ # train_list = train_list[:24]
+
+ train_dataset = CPMDataset(train_list, args.max_len)
+
+ return train_dataset
+
+
+def train_epoch(model, train_dataloader, optimizer, scheduler, logger,
+ epoch, args):
+ model.train()
+ device = args.device
+ ignore_index = args.ignore_index
+ epoch_start_time = datetime.now()
+
+ total_loss = 0 # 记录下整个epoch的loss的总和
+ epoch_correct_num = 0 # 每个epoch中,预测正确的word的数量
+ epoch_total_num = 0 # 每个epoch中,预测的word的总数量
+
+ for batch_idx, (input_ids, labels) in enumerate(train_dataloader):
+ # 捕获cuda out of memory exception
+ try:
+ input_ids = input_ids.to(device)
+ labels = labels.to(device)
+ outputs = model.forward(input_ids, labels=labels)
+ logits = outputs.logits
+ loss = outputs.loss
+ loss = loss.mean()
+
+ # 统计该batch的预测token的正确数与总数
+ batch_correct_num, batch_total_num = calculate_acc(logits, labels, ignore_index=ignore_index)
+ # 统计该epoch的预测token的正确数与总数
+ epoch_correct_num += batch_correct_num
+ epoch_total_num += batch_total_num
+ # 计算该batch的accuracy
+ batch_acc = batch_correct_num / batch_total_num
+
+ total_loss += loss.item()
+ if args.gradient_accumulation_steps > 1:
+ loss = loss / args.gradient_accumulation_steps
+
+ loss.backward()
+ # 梯度裁剪
+ torch.nn.utils.clip_grad_norm_(model.parameters(), args.max_grad_norm)
+
+ # 进行一定step的梯度累计之后,更新参数
+ if (batch_idx + 1) % args.gradient_accumulation_steps == 0:
+ # 更新参数
+ optimizer.step()
+ # 更新学习率
+ scheduler.step()
+ # 清空梯度信息
+ optimizer.zero_grad()
+
+ if (batch_idx + 1) % args.log_step == 0:
+ logger.info(
+ "batch {} of epoch {}, loss {}, batch_acc {}, lr {}".format(
+ batch_idx + 1, epoch + 1, loss.item() * args.gradient_accumulation_steps, batch_acc, scheduler.get_lr()))
+
+ del input_ids, outputs
+
+ except RuntimeError as exception:
+ if "out of memory" in str(exception):
+ logger.info("WARNING: ran out of memory")
+ if hasattr(torch.cuda, 'empty_cache'):
+ torch.cuda.empty_cache()
+ else:
+ logger.info(str(exception))
+ raise exception
+
+ # 记录当前epoch的平均loss与accuracy
+ epoch_mean_loss = total_loss / len(train_dataloader)
+ epoch_mean_acc = epoch_correct_num / epoch_total_num
+ logger.info(
+ "epoch {}: loss {}, predict_acc {}".format(epoch + 1, epoch_mean_loss, epoch_mean_acc))
+
+ # save model
+ logger.info('saving model for epoch {}'.format(epoch + 1))
+ model_path = join(args.save_model_path, 'epoch{}'.format(epoch + 1))
+ if not os.path.exists(model_path):
+ os.mkdir(model_path)
+ model_to_save = model.module if hasattr(model, 'module') else model
+ model_to_save.save_pretrained(model_path)
+ logger.info('epoch {} finished'.format(epoch + 1))
+ epoch_finish_time = datetime.now()
+ logger.info('time for one epoch: {}'.format(epoch_finish_time - epoch_start_time))
+
+ return epoch_mean_loss
+
+
+def train(model, logger, train_dataset, args):
+ train_dataloader = DataLoader(
+ train_dataset, batch_size=args.batch_size, shuffle=True, num_workers=args.num_workers, collate_fn=collate_fn,
+ drop_last=True
+ )
+ t_total = len(train_dataloader) // args.gradient_accumulation_steps * args.epochs
+ optimizer = transformers.AdamW(model.parameters(), lr=args.lr, eps=args.eps)
+ scheduler = transformers.get_linear_schedule_with_warmup(
+ optimizer, num_warmup_steps=args.warmup_steps, num_training_steps=t_total
+ )
+
+ logger.info('start training')
+
+ train_losses = [] # 记录每个epoch的平均loss
+ # ========== start training ========== #
+ for epoch in range(args.epochs):
+ train_loss = train_epoch(
+ model=model, train_dataloader=train_dataloader,
+ optimizer=optimizer, scheduler=scheduler,
+ logger=logger, epoch=epoch, args=args)
+ train_losses.append(round(train_loss, 4))
+ logger.info("train loss list:{}".format(train_losses))
+
+ logger.info('training finished')
+ logger.info("train_losses:{}".format(train_losses))
+
+
+def caculate_loss(logit, target, pad_idx, smoothing=True):
+ if smoothing:
+ logit = logit[..., :-1, :].contiguous().view(-1, logit.size(2))
+ target = target[..., 1:].contiguous().view(-1)
+
+ eps = 0.1
+ n_class = logit.size(-1)
+
+ one_hot = torch.zeros_like(logit).scatter(1, target.view(-1, 1), 1)
+ one_hot = one_hot * (1 - eps) + (1 - one_hot) * eps / (n_class - 1)
+ log_prb = F.log_softmax(logit, dim=1)
+
+ non_pad_mask = target.ne(pad_idx)
+ loss = -(one_hot * log_prb).sum(dim=1)
+ loss = loss.masked_select(non_pad_mask).mean() # average later
+ else:
+ # loss = F.cross_entropy(predict_logit, target, ignore_index=pad_idx)
+ logit = logit[..., :-1, :].contiguous().view(-1, logit.size(-1))
+ labels = target[..., 1:].contiguous().view(-1)
+ loss = F.cross_entropy(logit, labels, ignore_index=pad_idx)
+ return loss
+
+
+def calculate_acc(logit, labels, ignore_index=-100):
+ logit = logit[..., :-1, :].contiguous().view(-1, logit.size(-1))
+ labels = labels[..., 1:].contiguous().view(-1)
+
+ _, logit = logit.max(dim=-1) # 对于每条数据,返回最大的index
+ # 进行非运算,返回一个tensor,若labels的第i个位置为pad_id,则置为0,否则为1
+ non_pad_mask = labels.ne(ignore_index)
+ n_correct = logit.eq(labels).masked_select(non_pad_mask).sum().item()
+ n_word = non_pad_mask.sum().item()
+ return n_correct, n_word
+
+
+def main():
+ # 初始化参数
+ args = set_args()
+
+ # 设置使用哪些显卡进行训练
+ os.environ["CUDA_VISIBLE_DEVICES"] = args.device
+ args.cuda = not args.no_cuda
+
+ # if args.batch_size < 2048 and args.warmup_steps <= 4000:
+ # print('[Warning] The warmup steps may be not enough.\n' \
+ # '(sz_b, warmup) = (2048, 4000) is the official setting.\n' \
+ # 'Using smaller batch w/o longer warmup may cause ' \
+ # 'the warmup stage ends with only little data trained.')
+
+ # 创建日志对象
+ logger = set_logger(args.log_path)
+ # 当用户使用GPU,并且GPU可用时
+ args.cuda = torch.cuda.is_available() and not args.no_cuda
+ device = 'cuda:0' if args.cuda else 'cpu'
+ args.device = device
+ logger.info('using device:{}'.format(device))
+
+ # 设置随机种子
+ set_random_seed(args.seed, args.cuda)
+
+ # 初始化tokenizer
+ tokenizer = CpmTokenizer(vocab_file="vocab/chinese_vocab.model")
+ args.eod_id = tokenizer.convert_tokens_to_ids("") # 文档结束符
+ args.pad_id = tokenizer.pad_token_id
+
+ # 创建模型的输出目录
+ if not os.path.exists(args.save_model_path):
+ os.mkdir(args.save_model_path)
+
+ # 创建模型
+ if args.pretrained_model: # 加载预训练模型
+ model = GPT2LMHeadModel.from_pretrained(args.pretrained_model)
+ else: # 初始化模型
+ model_config = GPT2Config.from_json_file(args.model_config)
+ model = GPT2LMHeadModel(config=model_config)
+ model = model.to(device)
+ logger.info('model config:\n{}'.format(model.config.to_json_string()))
+ assert model.config.vocab_size == tokenizer.vocab_size
+
+ # 多卡并行训练模型
+ if args.cuda and torch.cuda.device_count() > 1:
+ # model = DataParallel(model).cuda()
+ model = BalancedDataParallel(args.gpu0_bsz, model, dim=0).cuda()
+ logger.info("use GPU {} to train".format(args.device))
+
+ # 计算模型参数数量
+ num_parameters = 0
+ parameters = model.parameters()
+ for parameter in parameters:
+ num_parameters += parameter.numel()
+ logger.info('number of model parameters: {}'.format(num_parameters))
+
+ # 记录参数设置
+ logger.info("args:{}".format(args))
+
+ # 加载训练集和验证集
+ # ========= Loading Dataset ========= #
+ train_dataset = load_dataset(logger, args)
+
+ train(model, logger, train_dataset, args)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/utils.py b/utils.py
new file mode 100644
index 0000000..fc3fa19
--- /dev/null
+++ b/utils.py
@@ -0,0 +1,76 @@
+import logging
+import torch
+import random
+import numpy as np
+import torch.nn.functional as F
+
+
+def set_logger(log_path):
+ """
+ 将日志输出到日志文件和控制台
+ """
+ logger = logging.getLogger(__name__)
+ logger.setLevel(logging.INFO)
+
+ formatter = logging.Formatter(
+ '%(asctime)s - %(levelname)s - %(message)s')
+
+ # 创建一个handler,用于写入日志文件
+ file_handler = logging.FileHandler(
+ filename=log_path)
+ file_handler.setFormatter(formatter)
+ file_handler.setLevel(logging.INFO)
+ logger.addHandler(file_handler)
+
+ # 创建一个handler,用于将日志输出到控制台
+ console = logging.StreamHandler()
+ console.setLevel(logging.DEBUG)
+ console.setFormatter(formatter)
+ logger.addHandler(console)
+ return logger
+
+
+def set_random_seed(seed, cuda):
+ """
+ 设置训练的随机种子
+ """
+ torch.manual_seed(seed)
+ random.seed(seed)
+ np.random.seed(seed)
+
+ if cuda:
+ torch.backends.cudnn.deterministic = True
+ torch.backends.cudnn.benchmark = False
+
+
+def top_k_top_p_filtering(logits, top_k=0, top_p=0.0, filter_value=-float('Inf')):
+ """ Filter a distribution of logits using top-k and/or nucleus (top-p) filtering
+ Args:
+ logits: logits distribution shape (vocabulary size)
+ top_k > 0: keep only top k tokens with highest probability (top-k filtering).
+ top_p > 0.0: keep the top tokens with cumulative probability >= top_p (nucleus filtering).
+ Nucleus filtering is described in Holtzman et al. (http://arxiv.org/abs/1904.09751)
+ From: https://gist.github.com/thomwolf/1a5a29f6962089e871b94cbd09daf317
+ """
+ assert logits.dim() == 1 # batch size 1 for now - could be updated for more but the code would be less clear
+ top_k = min(top_k, logits.size(-1)) # Safety check
+ if top_k > 0:
+ # Remove all tokens with a probability less than the last token of the top-k
+ # torch.topk()返回最后一维最大的top_k个元素,返回值为二维(values,indices)
+ # ...表示其他维度由计算机自行推断
+ indices_to_remove = logits < torch.topk(logits, top_k)[0][..., -1, None]
+ logits[indices_to_remove] = filter_value
+
+ if top_p > 0.0:
+ sorted_logits, sorted_indices = torch.sort(logits, descending=True)
+ cumulative_probs = torch.cumsum(F.softmax(sorted_logits, dim=-1), dim=-1)
+
+ # Remove tokens with cumulative probability above the threshold
+ sorted_indices_to_remove = cumulative_probs > top_p
+ # Shift the indices to the right to keep also the first token above the threshold
+ sorted_indices_to_remove[..., 1:] = sorted_indices_to_remove[..., :-1].clone()
+ sorted_indices_to_remove[..., 0] = 0
+
+ indices_to_remove = sorted_indices[sorted_indices_to_remove]
+ logits[indices_to_remove] = filter_value
+ return logits
\ No newline at end of file
diff --git a/vocab/chinese_vocab.model b/vocab/chinese_vocab.model
new file mode 100644
index 0000000000000000000000000000000000000000..86eb8223f141718acbb522b4e2e5885fd1e53397
GIT binary patch
literal 713229
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