a663dc1
change overview 456764059 2 years ago
2 changed file(s) with 835 addition(s) and 835 deletion(s). Raw diff Collapse all Expand all
0 # 基于大语言模型和高效微调的古诗生成模型
1
2 该古诗生成模型兼有丰富预训练语料和高效微调的优点,可以根据输入的提示(带有古诗主题标签的提示)生成该主题的古诗,还可以根据输入的文字生成藏头诗。
3
4 ## 生成结果展示
5
6 ### 根据输入的提示写诗
7
8 **写一首抒情诗:**
9
10 山色秋色清,山色晚晴晴。明月满空山,暮云暗掩山。
11
12 **写一首关于思乡的古诗:**
13
14 清明时节自有情,谁言青山与白云。梦里黄河清江水,落日黄河晚晴天。
15
16 **写一首关于咏物的古诗:**
17
18 秋风细雨绵绵起,江湖路边纷纷起。江山自有青山在,白云自有白云飞。
19
20
21 ### 藏头诗
22
23 **嘉怡:**嘉宾结觞酒,怡然笑百年。
24
25 **人工智能:**人间绝美境,工夫终不成。智慧空非善,能悟智有道。
26
27 **一二三四:**一鸟高飞倚江湖,二月春风秋江深。三百里外秋色绝,四十年来花落尽。
28
29 **奇思妙想:**奇观秋色绕江山,思念梅花满山城。妙趣更欲觅第一,想见长江源头头。
30
31 **三元牛奶:**三百里马驰骋空,元日晚来见梅花。牛羊鸣蛇觅石马,奶茶烹鹅做猪笼。
0 # 基于大语言模型和高效微调的古诗生成模型
1
2 ## 运行环境说明
3
4 - Python:`3.10.12`(安装miniconda管理Python环境)
5 - Pytorch:`1.13.0`
6 - peft: `0.4.0`
7
8 在该平台上需要创建虚拟环境以安装正确的python版本,可以使用miniconda或virtualenv,由于每次重启会删掉环境,因此将miniconda装在`/work`文件夹下,每次重启后,只需要将conda环境重新激活,方法
9
10 ```shell
11 # 安装miniconda(只需要进行一次)
12 sh Miniconda3-latest-Linux-x86_64.sh
13 conda create -n poem python==3.10.12
14 pip install peft==0.4.0
15 pip install transformers
16 # 激活conda(每次重启后都需要进行)
17 vim ~/.bashrc
18 export PATH="/home/jovyan/miniconda3/bin:$PATH"
19 source ~/.bashrc
20 conda activate poem
21 ```
22
23 virtualenv方法则更加方便,因为该平台自带virtualenv。运行下面的命令会在`/work`下创建一个环境,不会随着重启消失。
24
25 ```shell
26 virtualenv -p miniconda3/envs/poem/bin/python3 poem_env
27 source poem_env/bin/activate
28 ```
29
30 为了能在ipynb中使用配置好的环境,需要配置kernel。
31
32 ```shell
33 python -m ipykernel install --user --name=poem
34 ```
35
36 修改镜像源
37
38 ```shell
39 pip3 config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
40 ```
41
42 ## 项目结构说明
43
44 文件说明:
45 - data_clean 用于数据清洗
46 - poem_generate 第一阶段模型训练
47 - poem_generate_ft 第二阶段模型训练
48 - inference 模型推理
49 - app.py 部署到gradio的代码
50 - Jiayi_GPT2_v2.ipynb 与poem_generate代码一样
51
52 文件夹说明:
53 - data 存储清洗后的数据
54 - logs 记录模型训练日志
55 - checkpoint_lora_v4.1 保存模型
56
57 ## 数据处理
58
59 训练使用的数据包含两个部分,一个是各种类型的古诗词文本数据,另一个是带有标签等信息的古诗词数据。
60
61 文本数据来自[chinese-poetry/chinese-poetry](https://github.com/chinese-poetry/chinese-poetry),包含了 5.5 万首唐诗、26 万首宋诗、2.1 万首宋词和其他古典文集,诗词均以繁体字存储,使用[zhconv](https://github.com/gumblex/zhconv)库可以将繁体字转为简体字。
62
63 标签数据来自[yxcs/poems-db](https://github.com/yxcs/poems-db),通过爬取古诗文网,得到了22万首古诗词以及注释赏析等信息,但是信息不全面且包含许多噪声,需要数据清洗。
64
65 模型训练分成两个阶段,第一阶段直接进行Language Modelling,使用较大规模的古诗词进行训练,使模型理解古诗词的基本结构,第二阶段添加诗词相关的标签作为提示进行Language Modelling,由于有标签的古诗词较少,所以经过过滤之后使用少量样本微调。
66
67 ### chinese-poetry数据集数据处理
68
69 ```python
70 from pathlib import Path
71 import json
72 from tqdm import tqdm
73 import re
74 import matplotlib.pyplot as plt
75 import random
76
77
78 files = [i for i in Path("chinese-poetry/json").glob("poet*")]
79 files.sort(key=lambda x: int(x.stem.split('.')[2]))
80 poems = []
81 for f in tqdm(files):
82 for item in json.load(open(f, encoding='utf-8')):
83 del item['strains']
84 poem = ''.join(item['paragraphs'])
85 poem = re.sub(r'(.*?)', '', poem) # 删除中文括号内的注释
86 if len(poem) > 100 or len(poem) < 12: # 12: 日坐竹马桥,夜宿牧牛轩。
87 continue
88 poems.append(poem)
89
90 plt.hist([len(i) for i in poems], bins=30)
91 plt.show()
92
93 # output
94 with open("tang_song_poems.txt", "w+", encoding='utf-8') as f:
95 f.write('\n'.join(poems))
96
97 ```
98
99 由于chinese-poetry数据集的数据分散在多个json文件中,所以先将其汇总为一个txt文件,每一行是一首诗,同时先进行简单数据清洗,根据长度的分布去掉过长和过短的古诗。
100
101 ```python
102 from tqdm import tqdm
103 import re
104
105 filtered = []
106 total_cnt = 0
107 with open("tang_song_poems.txt", encoding='utf-8') as f:
108 for line in tqdm(f.readlines()):
109 total_cnt += 1
110
111 line = line.strip('。')
112 line = line.strip('\n')
113 sentences = re.split(r'[,。]', line)[:-1]
114 lens = [len(i) for i in sentences]
115
116 if "□" in line: # 奇怪的字符
117 continue
118 if len(set(lens)) > 1: # 长度不统一
119 continue
120 if lens[0] not in (5, 7): # 非5言或7言
121 continue
122 if len(sentences) % 2 != 0: # 句数不为偶数
123 continue
124 if len(sentences) < 4 or len(sentences) > 8: # 过长
125 continue
126
127 #line = re.sub(r'[,。?]', ' ', line).strip()
128 filtered.append(line)
129
130 print(f"{len(filtered)}/{total_cnt}")
131 with open("tang_song_poems_filter.txt", "w+", encoding='utf-8') as f:
132 f.write("\n".join(filtered))
133 ```
134
135 随后对数据进行进一步的清洗,对于一些编码错误的字符删去,同时为了降低训练难度,我们限制古诗为5言或7言,并且为2~4句。
136
137 之后根据9比1的比例分为训练集和测试集:
138
139 ```python
140 with open("tang_song_poems_filter.txt", encoding='utf-8') as src:
141 lines = src.readlines()
142 idx = int(0.9 * len(lines))
143 with open("train_poems.txt", "w+", encoding='utf-8') as f:
144 f.write("".join(lines[:idx]))
145 with open("test_poems.txt", "w+", encoding='utf-8') as f:
146 f.write("".join(lines[idx:])
147 ```
148
149 ### 带标签古诗数据处理
150
151 数据如下所示,包含较多的元信息,准备使用的是`content`和`tags`两个字段,
152
153 ```json
154
155 {'_id': {'$oid': '5c22086497880d3b825c968f'},
156 'content': ['\n渡远荆门外,来从楚国游。',
157 '山随平野尽,江入大荒流。',
158 '月下飞天镜,云生结海楼。',
159 '仍怜故乡水,万里送行舟。\n'],
160 'translate': ['乘船远行,路过荆门一带,来到楚国故地。',
161 '青山渐渐消失,平野一望无边。长江滔滔奔涌,流入广袤荒原。',
162 '月映江面,犹如明天飞镜;云彩升起,变幻无穷,结成了海市蜃楼。',
163 '故乡之水恋恋不舍,不远万里送我行舟。'],
164 'translate_res': ['张国举.唐诗精华注译评.长春:长春出版社,2010:128-129',
165 '裴斐.李白诗歌赏析集.成都:巴蜀书社,1988年2月:13-18',
166 '于海娣 等.唐诗鉴赏大全集.北京:中国华侨出版社,2010:116'],
167 'tags': ['唐诗三百首', '初中古诗', '长江', '送别', '思乡'],
168 'notes': ['渡远荆(jīng)门外,来从楚国游。荆门:山名,位于今湖北省宜都县西北长江南岸,与北岸虎牙三对峙,地势险要,自古即有楚蜀咽喉之称。远:远自。楚国:楚地,指湖北一带,春秋时期属楚国。',
169 '山随平野尽,江入大荒流。平野:平坦广阔的原野。江:长江。大荒:广阔无际的田野。',
170 '月下飞天镜,云生结海楼。月下飞天镜:明月映入江水,如同飞下的天镜。下:移下。海楼:海市蜃楼,这里形容江上云霞的美丽景象。',
171 '仍怜故乡水,万里送行舟。 仍:依然。怜:怜爱。一本作“连”。故乡水:指从四川流来的长江水。因诗人从小生活在四川,把四川称作故乡。万里:喻行程之远。'],
172 'reference': [],
173 'appreciation': ['\u3000\u3000这首诗是李白出蜀时所作。李白这次出蜀,由水路乘船远行,经巴渝,出三峡,直向荆门山之外驶去,目的是到湖北、湖南一带楚国故地游览。“渡远荆门外,来从楚国游”,指的就是这一壮游。这时候的青年诗人,兴致勃勃,坐在船上沿途纵情观赏巫山两岸高耸云霄的峻岭,一路看来,眼前景色逐渐变化,船过荆门一带,已是平原旷野,视域顿然开阔,别是一番景色:',
174 '\u3000\u3000“山随平野尽,江入大荒流。”',
175 '\u3000\u3000“山随平野尽”,形象地描绘了船出三峡、渡过荆门山后长江两岸的特有景色:山逐渐消失了,眼前是一望无际的低平的原野。著一“随”字,化静为动,将群山与平野的位置逐渐变换、推移,真切地表现出来。这句好比用电影镜头摄下的一组活动画面,给人以流动感与空间感,将静止的山岭摹状出活动的趋向来。',
176 '\u3000\u3000“江入大荒流”,写出江水奔腾直泻的气势,从荆门往远处望去,仿佛流入荒漠辽远的原野,显得天空寥廓,境界高远。后句著一“入”字,写出了气势的博大,充分表达了诗人的万丈豪情,充满了喜悦和昂扬的激情,力透纸背,用语贴切。景中蕴藏着诗人喜悦开朗的心情和青春的蓬勃朝气。',
177 '\u3000\u3000颔联这两句不仅由于写进“平野”、“大荒”这些辽阔原野的意象,而气势开阔;而且还由于动态的描写而十分生动。大江固然是流动的,而山脉却本来是凝固的,“随、尽”的动态感觉,完全是得自舟行的实际体验。在陡峭奇险,山峦叠嶂的三峡地带穿行多日后,突见壮阔之景,豁然开朗的心情可想而知。它用高度凝炼的语言。极其概括地写出了诗人整个行程的地理变化。',
178 '\u3000\u3000写完山势与流水,诗人又以移步换景手法,从不同角度描绘长江的近景与远景:',
179 '\u3000\u3000“月下飞天镜,云生结海楼。”',
180 '\u3000\u3000长江流过荆门以下,河道迂曲,流速减缓。晚上,江面平静时,俯视月亮在水中的倒影,好象天上飞来一面明镜似的;日间,仰望天空,云彩兴起,变幻无穷,结成了海市蜃楼般的奇景。这正是从荆门一带广阔平原的高空中和平静的江面上所观赏到的奇妙美景。如在崇山峻岭的三峡中,自非亭午夜分,不见曦月,夏水襄陵,江面水流湍急汹涌,那就很难有机会看到“月下飞天镜”的水中影像;在隐天蔽日的三峡空间,也无从望见“云生结海楼”的奇景。这一联以水中月明如圆镜反衬江水的平静,以天上云彩构成海市蜃楼衬托江岸的辽阔,天空的高远,艺术效果十分强烈。颔颈两联,把生活在蜀中的人,初次出峡,见到广大平原时的新鲜感受极其真切地写了出来。',
181 '\u3000\u3000颈联两句反衬江水平静,展现江岸辽阔,天空高远,充满了浪漫主义色彩。',
182 '\u3000\u3000李白在欣赏荆门一带风光的时候,面对那流经故乡的滔滔江水,不禁起了思乡之情:',
183 '\u3000\u3000“仍怜故乡水,万里送行舟。”',
184 '\u3000\u3000诗人顺着长江远渡荆门,江水流过的蜀地也就是曾经养育过他的故乡,初次离别,他怎能不无限留恋,依依难舍呢?但诗人不说自己思念故乡,而说故乡之水恋恋不舍地一路送我远行,怀着深情厚意,万里送行舟,从对面写来,越发显出自己思乡深情。诗以浓重的怀念惜别之情结尾,言有尽而情无穷。诗题中的“送别”应是告别故乡而不是送别朋友,诗中并无送别朋友的离情别绪。清沈德潜认为“诗中无送别意,题中二字可删”(《唐诗别裁》),这并不是没有道理的。',
185 '\u3000\u3000这首诗首尾行结,浑然一体,意境高远,风格雄健。“山随平野尽,江入大荒流”,写得逼真如画,有如一幅长江出峡渡荆门长轴山水图,成为脍炙人口的佳句。如果说优秀的山水画“咫尺应须论万里”,那么,这首形象壮美瑰玮的五律也可以说能以小见大,以一当十,容量丰富,包涵长江中游数万里山势与水流的景色,具有高度集中的艺术概括力。'],
186 'appreciation_res': ['何国治 等.唐诗鉴赏辞典.上海:上海辞书出版社,1983:302-303'],
187 'onlyId': '50b4388a212b8f42992a63458edbf3f7',
188 'name': '渡荆门送别',
189 'dynasty': '唐代',
190 'author': '李白',
191 'sourceLink': 'https://so.gushiwen.org/shiwenv_d50eb19399e6.aspx',
192 'type': '唐诗三百首',
193 'format': '五言律诗',
194 'updateAt': '2018-12-13T08:36:12.589Z'}
195 ```
196
197 标签数量分布统计和可视化结果如下图:
198
199 ![诗词标签-数据可视化.png](pics/诗词标签-数据可视化.png)
200
201 对无用标签进行清洗。去掉其中例如“唐诗三百首”、“高中必备古诗”这些无用的tag,只保留“送别”、“思乡”这类tag。
202
203 ```python
204 from collections import Counter
205 import re
206
207 cnt = Counter()
208 for i in tag_data:
209 cnt.update(i['tags'])
210
211 ban_tags = []
212 for tag in cnt.keys():
213 if re.search(r'\d', tag):
214 ban_tags.append(tag)
215 elif '唐' in tag or '宋' in tag:
216 ban_tags.append(tag)
217 elif len(tag) >= 5:
218 ban_tags.append(tag)
219 elif '小学' in tag or '中学' in tag or '高中' in tag or '初中' in tag:
220 ban_tags.append(tag)
221 elif '诗经' in tag:
222 ban_tags.append(tag)
223 ```
224
225 删除的标签如下:
226
227 ```text
228 ['唐诗三百首',
229 '早教古诗100首',
230 '小学生必背古诗70首',
231 '小学生必背古诗80首',
232 '初中古诗',
233 '小学古诗',
234 '古诗里的十二个月',
235 '高中古诗',
236 '写狗古诗18首',
237 '初中文言文',
238 '诗经',
239 '高中文言文',
240 '古诗十九首',
241 '宋词精选',
242 '小学文言文',
243 '古诗三百首',
244 '宋词三百首',
245 '春天|写人',
246 '离别|抒情|伤感|怀人']
247 ```
248
249 对于内容,删去非常长的古诗,并删去其中的特殊字符、注释、英文标点符号。
250
251 ```python
252 tag_poems = []
253 for item in tag_data:
254 # 太长的删去
255 if len(item['content']) > 6:
256 continue
257 if '诗经' in item['tags']:
258 continue
259 # 无意义tag删去
260 refined_tags = [i for i in item['tags'] if i not in ban_tags]
261 if len(refined_tags) == 0:
262 continue
263 refined_content = ''.join(item['content'])
264 refined_content = re.sub(r'[A-Za-z0-9\s/<>《》〔〕]', '', refined_content)
265 refined_content = re.sub(r'(.*?)', '', refined_content)
266 refined_content = re.sub(r'\(.*?\)', '', refined_content)
267 # refined_content = re.sub(r'[,。?,]', ' ', refined_content)
268 refined_content.replace(',', ',').replace('?', '?').replace('!', '!')
269 refined_content = refined_content.strip()
270 # 词删去(字数不一)
271 if len(set([len(i) for i in re.split(r'[,。?!]', refined_content)[:-1]])) > 1:
272 continue
273 # 太长的删去
274 if len(refined_content) > 150:
275 continue
276
277 tag_poems.append({
278 'tags': ' '.join(refined_tags),
279 'content': refined_content
280 })
281 ```
282
283 ## 第一阶段模型构建与训练
284
285 由于目前已有许多预训练语言模型,所以选择了使用大语料训练过的模型进行微调。
286
287 选择在HuggingFace上发布的`IDEA-CCNL/Wenzhong-GPT2-110M`模型,包含110M参数,使用BPE分词,在300G的悟道语料上进行预训练。该模型在封神榜系列模型中属于自然语言生成任务的通用模型。
288
289 ![模型分类](pics/fenshenbang-模型分类.png)
290
291 为了提升训练效率,使用peft库进行高效的微调,具体使用LoRA方法,最终仅训练1.02%的参数。
292
293 ### 导入相关库
294
295 ```python
296 import time
297 import math
298 import random
299
300 import numpy as np
301 import torch
302 import torch.nn as nn
303 from tqdm import tqdm
304 import transformers
305 from tensorboardX import SummaryWriter # 这个库提供Tensorboard的日志功能
306 from transformers import AutoTokenizer, AutoModelForCausalLM
307 from torch.utils.data import Dataset, DataLoader
308 from peft import TaskType, LoraConfig, get_peft_model
309 ```
310
311 ### 构建数据集和处理输入数据
312
313 PoemDataset用于构建诗歌数据集。
314
315 ```python
316 class PoemDataset(Dataset):
317 def __init__(self, path):
318 super().__init__()
319 self.poems = open(path, encoding='utf-8').readlines() # [:30000]
320
321 def __getitem__(self, idx):
322 text = self.poems[idx].strip()
323 return text
324
325 def __len__(self):
326 return len(self.poems)
327
328 ```
329
330 prepare_inputs用于处理输入的样本。首先使用随机选择的prompt作为输入的前缀,并使用tokenizer将其转换为模型可接受的输入格式。然后,将样本列表samples使用tokenizer转换为模型可接受的输入格式。最后,将prompt的输入和样本的输入拼接在一起,生成input_ids、label_ids和注意力掩码。需要注意prompt在训练过程不参与loss的计算,因此对其进行`*-100`的操作。
331
332 使用Hugging Face的transformers库中的AutoTokenizer类从预训练模型路径(MODEL_PATH)加载tokenizer。并且将填充标记设置为eos(end of sentence)标记,这样会使模型处理填充部分的表示更加连贯,效果会更好。
333
334 ```python
335 tokenizer = AutoTokenizer.from_pretrained(MODEL_PATH)
336 tokenizer.pad_token = tokenizer.eos_token
337
338 prompts = [
339 "写一首唐诗:",
340 "写一首古诗:",
341 "写一首绝句:",
342 "生成一首唐诗:",
343 "生成一首古诗:",
344 "生成一首绝句:",
345 "这是一首唐诗:",
346 "这是一首古诗:",
347 "这是一首古代诗歌:",
348 "生成一首压韵的古诗:",
349 ]
350
351 def prepare_inputs(samples):
352 prompt_inputs = tokenizer([random.choice(prompts)] * len(samples),
353 padding="longest", truncation=True, add_special_tokens=False, return_tensors='pt')
354 inputs = tokenizer(samples, padding="longest", truncation=True, add_special_tokens=False, return_tensors='pt')
355
356 input_ids = torch.cat([
357 prompt_inputs.input_ids,
358 inputs.input_ids
359 ], dim=1)
360 label_ids = torch.cat([
361 torch.ones_like(prompt_inputs.input_ids) * -100, # no loss for prompt
362 inputs.input_ids
363 ], dim=1)
364 attention_mask = torch.cat([prompt_inputs.attention_mask, inputs.attention_mask], dim=1)
365 return input_ids, attention_mask, label_ids
366 ```
367
368 调用诗歌数据集,构建训练验证数据集。
369
370 ```python
371 # prepare data
372 train_dataset = PoemDataset("train_poems_v2.txt")
373 test_dataset = PoemDataset("test_poems_v2.txt")
374
375 train_dataloader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
376 val_dataloader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)
377 ```
378
379 ### 定义训练模型函数
380
381 定义用于训练模型的train函数。使用tensorboard在训练过程中记录训练日志。
382
383 ```python
384 def train(model: nn.Module, dataloaders, optimizer, scheduler, **kwargs):
385 """训练的主函数"""
386 train_loader, test_loader = dataloaders
387 device = kwargs['device']
388 writer = SummaryWriter(kwargs['logger_name'])
389 model = model.to(device)
390
391 for epoch in range(kwargs['max_epochs']):
392 # ========== Train ==========
393 loss_list = []
394 model.train()
395 last_time = time.time()
396 for local_step, inputs in enumerate(train_loader):
397 step = epoch * kwargs['steps_per_epoch'] + local_step
398 input_ids, attention_mask, label_ids = prepare_inputs(inputs)
399
400 optimizer.zero_grad()
401
402 with torch.autocast(device_type='cuda'): # fp16
403 outputs = model(
404 input_ids=input_ids.to(device),
405 attention_mask=attention_mask.to(device),
406 labels=label_ids.to(device),
407 return_dict=True,
408 )
409 loss = outputs.loss
410
411 loss.backward()
412 optimizer.step()
413 scheduler.step()
414
415 # log
416 loss_list.append(loss.detach().cpu().item())
417 if (local_step % 50 == 0 and local_step != 0) or local_step == kwargs['steps_per_epoch'] - 1:
418 avg_loss = sum(loss_list) / len(loss_list)
419 n_step_time = time.time() - last_time
420 left_time = (kwargs['steps_per_epoch'] - local_step) // 50 * n_step_time
421 print("Epoch {}/{} | Step {}/{} | loss:{:.5f} time:{:.1f}s left:{:.1f}m".format(
422 epoch, kwargs['max_epochs'], local_step, kwargs['steps_per_epoch'],
423 avg_loss, n_step_time, left_time / 60
424 ))
425 last_time = time.time()
426 writer.add_scalar('Train/Loss', loss, step)
427 writer.add_scalar('Epoch', epoch, step)
428
429 # if local_step %
430 # torch.save(model.named_parameters(), "checkpoint.pth")
431 model.save_pretrained("checkpoint_lora_v4")
432 # ========== Eval ==========
433 evaluate(model, test_loader, epoch, **kwargs)
434 print("=" * 53)
435 # ========== Inference ==========
436 inference(model)
437 ```
438
439 ### 定义验证模型函数
440
441 定义用于验证的evaluate函数,并计算loss和perplexity(PPL)。erplexity的中文是困惑度,困惑度一般来说是用来评价语言模型好坏的指标。
442
443 困惑度与测试集上的句子概率相关,其基本思想是:给测试集的句子赋予较高概率值的语言模型较好,当语言模型训练完之后,测试集中的句子都是正常的句子,那么训练好的模型就是在测试集上的概率越高越好,公式如下:
444
445 ![PPL](pics/PPL公式.png)
446
447 其中S表示句子,w表示词语。
448
449 ```python
450 @torch.no_grad()
451 def evaluate(model, test_loader, epoch, **kwargs):
452 loss_list = []
453 ppl_list = []
454 print("-" * 20 + " Evaluating " + "-" * 20)
455 model.eval()
456 with torch.no_grad():
457 for local_step, inputs in enumerate(tqdm(test_loader)):
458 input_ids, attention_mask, label_ids = prepare_inputs(inputs)
459
460 outputs = model(
461 input_ids=input_ids.to(device),
462 attention_mask=attention_mask.to(device),
463 labels=label_ids.to(device),
464 return_dict=True,
465 )
466 loss_list.append(outputs.loss.cpu().item())
467 # PPL
468 probs = torch.softmax(outputs.logits, dim=-1).max(dim=-1)[0] # BLC->BL
469 ppl = torch.exp(-probs.log().mean(-1))
470 ppl_list.append(ppl.mean().cpu().item())
471 # log
472 avg_loss = sum(loss_list) / len(loss_list)
473 avg_ppl = sum(ppl_list) / len(ppl_list)
474 print(f"Epoch {epoch}/{kwargs['max_epochs']} | loss:{avg_loss:.5f} | ppl: {avg_ppl: 5f}")
475 # writer.add_scalar('Test/Loss', sum(loss_list) / len(loss_list), epoch * kwargs['steps_per_epoch'])
476 ```
477
478 ### 定义模型推理函数
479
480 输入提示,并将其进行tokenizer,即可生成古诗。模型可以设置最大输出长度,top-p等
481
482 top-p是一种用于在文本生成中控制输出的策略,控制生成结果的多样性。top-p算法保留累计概率之和达到一个给定阈值p的概率分布中的词汇,然后在这个分布中进行随机采样,从而生成一个单词。
483
484 ```python
485 @torch.no_grad()
486 def inference(model):
487 inputs = tokenizer("写一首唐诗:折戟沉沙铁未销,自将磨洗认前朝。", add_special_tokens=False, return_tensors='pt')
488 inputs = inputs.to(device)
489 outputs = model.generate(
490 **inputs,
491 return_dict_in_generate=True,
492 max_length=150,
493 do_sample=True,
494 top_p=0.6,
495 num_return_sequences=5
496 )
497 for line in tokenizer.batch_decode(outputs.sequences, skip_special_tokens=True):
498 print(line)
499 ```
500
501 ### 构建模型
502
503 准备预训练模型并进行模型微调。gpt2_model是从预训练模型中加载的,peft_config指定了任务类型、推理模式、r、lora_alpha、lora_dropout和偏置。
504
505 - r (int): Lora矩阵的最小维度
506 - lora_dropout (float): Lora层的dropout概率
507 - lora_alpha (float): LoRA 缩放因子
508 - bias:指定是否应训练参数。
509
510 为了使微调更有效, LoRA通过低秩分解,用两个较小的权重更新来表示权重更新矩阵。这些新矩阵可以被训练以适应新数据,同时保持较低的更改总数。原始权重矩阵保持冻结状态,不会接收任何进一步的调整。
511
512 ![lora](pics/lora.png)
513
514 ```python
515 MODEL_PATH = r"IDEA-CCNL/Wenzhong-GPT2-110M"
516
517 # prepare model
518 gpt2_model = AutoModelForCausalLM.from_pretrained(MODEL_PATH)
519 peft_config = LoraConfig(
520 task_type=TaskType.CAUSAL_LM,
521 inference_mode=False,
522 r=32, lora_alpha=16, lora_dropout=0.1, bias='all'
523 )
524 gpt2_model = get_peft_model(gpt2_model, peft_config)
525 gpt2_model.print_trainable_parameters()
526 ```
527
528 ### 训练模型
529
530 设置超参数、优化器
531
532 ```python
533 # Hyperparameter
534 batch_size = 32
535 lr = 1e-4
536 device = torch.device('cuda')
537 max_epochs = 4
538 num_warmup_steps = 200
539 num_training_steps = max_epochs * math.ceil(len(train_dataset) / batch_size)
540 seed = 2023
541 logger_name = "Jiayi_GPT2_202307091512"
542
543 # prepare optimizer
544 optim = torch.optim.AdamW(filter(lambda p: p.requires_grad, gpt2_model.parameters()), lr=lr)
545 sche = transformers.get_linear_schedule_with_warmup(optim, num_warmup_steps, num_training_steps)
546 ```
547
548 训练模型
549
550 ```python
551 # train
552 train(
553 gpt2_model,
554 (train_dataloader, val_dataloader),
555 optimizer=optim,
556 scheduler=sche,
557 device=device,
558 max_epochs=max_epochs,
559 logger_name=logger_name,
560 steps_per_epoch=len(train_dataloader)
561 )
562 ```
563
564 训练过程截图如下:
565
566 ![训练过程](pics/训练过程.jpg)
567
568 训练过程中的loss:
569
570 ![loss](pics/loss.png)
571
572 ## 第二阶段模型构建与训练
573
574 第一阶段训练的模型没有诗词标签,无法指定特定类型的诗歌。因此,利用带标签数据集进行第二阶段训练。
575
576 ### 构建数据集和处理输入数据
577
578 与第一阶段不同的是数据需要构造关于tag的提示。
579
580 ```python
581 class TagPoemDataset(Dataset):
582 def __init__(self, path):
583 super().__init__()
584 self.poems = open(path, encoding='utf-8').readlines()
585
586 def __getitem__(self, idx):
587 tag, poem = self.poems[idx].strip().split('|')
588 tag = "写一首关于" + tag.replace(' ', '、') + "的古诗:"
589 return tag, poem
590
591 def __len__(self):
592 return len(self.poems)
593 ```
594
595 处理输入数据,同时处理tag和诗歌。
596
597 ```python
598 def prepare_inputs(samples):
599 tags, poems = samples
600 bs = len(tags)
601
602 prompt_inputs = tokenizer(tags, add_special_tokens=False)
603 poems_inputs = tokenizer(poems, add_special_tokens=False)
604 prompt_len = [len(i) for i in prompt_inputs.input_ids]
605 input_ids_list = [x1 + x2 for x1, x2 in zip(prompt_inputs.input_ids, poems_inputs.input_ids)]
606 max_len = max([len(i) for i in input_ids_list])
607
608 input_ids = torch.ones(bs, max_len, dtype=torch.long) * tokenizer.pad_token_id
609 attention_mask = torch.zeros(bs, max_len, dtype=torch.long)
610 for i in range(bs):
611 input_ids[i, :len(input_ids_list[i])] = torch.tensor(input_ids_list[i])
612 attention_mask[i, :len(input_ids_list[i])] = 1
613 label_ids = input_ids.clone()
614 for i in range(bs):
615 label_ids[i, :prompt_len[i]] = -100
616
617 return input_ids, attention_mask, label_ids
618 ```
619
620 模型训练和模型推理的过程和第一阶段很类似,在此不再赘述。
621
622 ### 构建模型
623
624 这里选择了更大的WenZhong模型,其他过程与第一阶段类似。
625
626 ```python
627 MODEL_PATH = r"IDEA-CCNL/Wenzhong2.0-GPT2-3.5B-chinese"
628 ```
629
630 ### 训练模型
631
632 设置超参数:这里的训练数据少,因此batchsize更小。由于属于第二阶段训练,学习率也提高了。
633
634 ```python
635 # Hyperparameter
636 batch_size = 4
637 lr = 5e-4
638 device = torch.device('cuda')
639 max_epochs = 10
640 num_warmup_steps = 50
641 num_training_steps = max_epochs * math.ceil(len(train_dataset) / batch_size)
642 seed = 2023
643 ```
644
645 ## 根据提示生成藏头诗
646
647 通过限制第一个字的输入生成藏头诗,并且设置停止符号为中文标点`,。`,从而控制一句诗的结束。
648
649 ```python
650 class ChineseCharacterStop(StoppingCriteria):
651 def __init__(self, chars: list[str]):
652 self.chars = [
653 tokenizer(i, add_special_tokens=False, return_tensors='pt').input_ids
654 for i in chars
655 ]
656 # for chars, tokens in zip(chars, self.chars):
657 # print(f"'{chars}':{tokens}")
658
659 def __call__(self, input_ids: torch.LongTensor,
660 scores: torch.FloatTensor, **kwargs) -> bool:
661 for c in self.chars:
662 c = c.to(input_ids.device)
663 match = torch.eq(input_ids[..., -c.shape[1]:], c)
664 if torch.any(torch.all(match, dim=1)):
665 return True
666 return False
667
668
669 tokenizer = AutoTokenizer.from_pretrained("IDEA-CCNL/Wenzhong-GPT2-110M")
670 tokenizer.pad_token = tokenizer.eos_token
671 gpt2_model = AutoModelForCausalLM.from_pretrained("IDEA-CCNL/Wenzhong-GPT2-110M")
672 model = PeftModel.from_pretrained(gpt2_model, 'checkpoint_lora_v4.1')
673
674
675 def cang_tou(tou: str):
676 poem_now = "写一首唐诗:"
677 for c in tou:
678 poem_now += c
679 print(poem_now)
680 inputs = tokenizer(poem_now, return_tensors='pt')
681 outputs = model.generate(
682 **inputs,
683 return_dict_in_generate=True,
684 max_length=150,
685 do_sample=True,
686 top_p=0.4,
687 num_beams=1,
688 num_return_sequences=1,
689 stopping_criteria=[ChineseCharacterStop(['。', ','])],
690 pad_token_id=tokenizer.pad_token_id
691 )
692 poem_now = tokenizer.batch_decode(outputs.sequences, skip_special_tokens=True)[0]
693 print(poem_now)
694 return poem_now[6:]
695
696
697 def prompt_gen(prompt):
698 inputs = tokenizer(prompt, return_tensors='pt')
699 outputs = model.generate(
700 **inputs,
701 return_dict_in_generate=True,
702 max_length=200,
703 do_sample=True,
704 top_p=0.8,
705 num_beams=5,
706 num_return_sequences=3,
707 # stopping_criteria=[ChineseCharacterStop(['。', ',', ''])],
708 pad_token_id=tokenizer.pad_token_id
709 )
710 res = ''
711 for line in tokenizer.batch_decode(outputs.sequences, skip_special_tokens=True):
712 line = line[len(prompt):]
713 res = res+line+'\n'
714 return res
715
716 ```
717
718 ## 设计交互界面
719
720 将输入提示和模型返回结果的过程设计成gradio的交互界面,已经部署在gradio上,链接为[huggingface.co/spaces/Wendyy/poem-generate](https://huggingface.co/spaces/Wendyy/poem-generate):
721
722 ![提示古诗生成](pics/提示古诗生成.png)
723
724 ![藏头诗生成](pics/藏头诗生成.png)
725
726 ```python
727 css = """
728 #col-container {max-width: 510px; margin-left: auto; margin-right: auto;}
729 a {text-decoration-line: underline; font-weight: 600;}
730 .animate-spin {
731 animation: spin 1s linear infinite;
732 }
733 """
734
735 with gr.Blocks(css=css) as demo:
736 with gr.Column(elem_id="col-container"):
737 gr.Markdown(
738 """
739 <h1 style="text-align: center;">✨古诗生成</h1>
740 <p style="text-align: center;">
741 根据输入的提示生成古诗、藏头诗<br />
742 </p>
743 """
744 )
745 with gr.Tab("提示"):
746 prompt_in = gr.Textbox(label="Prompt", placeholder="写一首关于思乡的古诗:", elem_id="prompt-in")
747 submit_btn = gr.Button("Submit")
748 poetry_result = gr.Textbox(label="Output", elem_id="poetry-output")
749
750 submit_btn.click(fn=prompt_gen,
751 inputs=[prompt_in],
752 outputs=[poetry_result])
753
754 with gr.Tab("藏头诗"):
755 tou_in = gr.Textbox(label="Prompt", placeholder="一见如故", elem_id="tou-in")
756 submit_btn = gr.Button("Submit")
757 cangtou_result = gr.Textbox(label="Output", elem_id="cangtou-output")
758 submit_btn.click(fn=cang_tou,
759 inputs=[tou_in],
760 outputs=[cangtou_result])
761
762
763
764 demo.queue(max_size=12).launch()
765 ```
766
767 ## 生成结果展示
768
769 ### 根据输入的提示写诗
770
771 **写一首抒情诗:**
772
773 山色秋色清,山色晚晴晴。明月满空山,暮云暗掩山。
774
775 **写一首关于思乡的古诗:**
776
777 清明时节自有情,谁言青山与白云。梦里黄河清江水,落日黄河晚晴天。
778
779 **写一首关于咏物的古诗:**
780
781 秋风细雨绵绵起,江湖路边纷纷起。江山自有青山在,白云自有白云飞。
782
783
784 ### 藏头诗
785
786 **嘉怡:**嘉宾结觞酒,怡然笑百年。
787
788 **人工智能:**人间绝美境,工夫终不成。智慧空非善,能悟智有道。
789
790 **一二三四:**一鸟高飞倚江湖,二月春风秋江深。三百里外秋色绝,四十年来花落尽。
791
792 **奇思妙想:**奇观秋色绕江山,思念梅花满山城。妙趣更欲觅第一,想见长江源头头。
793
794 **三元牛奶:**三百里马驰骋空,元日晚来见梅花。牛羊鸣蛇觅石马,奶茶烹鹅做猪笼。
795
796 通过多次尝试,可以看出藏头诗的生成结果要比根据提示写诗的效果好。这是因为带有标签的数据集是较少的,噪声也很大。藏头诗通过开头的字总领全诗,规范整首诗的写作,更为通顺流畅和语义丰富。
797
798 ## 总结和展望
799
800 该古诗生成模型兼有丰富预训练语料和高效微调的优点,可以根据输入的提示(带有古诗主题标签的提示)生成该主题的古诗,还可以根据输入的文字生成藏头诗。虽然尚未进行定量的生成效果评估,但从主观评价的角度,生成结果具有语句流畅、语义丰富、贴合主题等优点。
801
802 未来可以对这个模型进行指标评价,并通过设置更多规则(如抑扬顿挫、押韵等)来提升生成结果。此外,模型生成结果的意蕴还有待提升,缺乏真正的思想和情感,通过更多的语料、提示、标注信息将有可能对此进行提升。
0 # 基于大语言模型和高效微调的古诗生成模型
1
2 ## 运行环境说明
3
4 - Python:`3.10.12`(安装miniconda管理Python环境)
5 - Pytorch:`1.13.0`
6 - peft: `0.4.0`
7
8 在该平台上需要创建虚拟环境以安装正确的python版本,可以使用miniconda或virtualenv,由于每次重启会删掉环境,因此将miniconda装在`/work`文件夹下,每次重启后,只需要将conda环境重新激活,方法
9
10 ```shell
11 # 安装miniconda(只需要进行一次)
12 sh Miniconda3-latest-Linux-x86_64.sh
13 conda create -n poem python==3.10.12
14 pip install peft==0.4.0
15 pip install transformers
16 # 激活conda(每次重启后都需要进行)
17 vim ~/.bashrc
18 export PATH="/home/jovyan/miniconda3/bin:$PATH"
19 source ~/.bashrc
20 conda activate poem
21 ```
22
23 virtualenv方法则更加方便,因为该平台自带virtualenv。运行下面的命令会在`/work`下创建一个环境,不会随着重启消失。
24
25 ```shell
26 virtualenv -p miniconda3/envs/poem/bin/python3 poem_env
27 source poem_env/bin/activate
28 ```
29
30 为了能在ipynb中使用配置好的环境,需要配置kernel。
31
32 ```shell
33 python -m ipykernel install --user --name=poem
34 ```
35
36 修改镜像源
37
38 ```shell
39 pip3 config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
40 ```
41
42 ## 项目结构说明
43
44 文件说明:
45 - data_clean 用于数据清洗
46 - poem_generate 第一阶段模型训练
47 - poem_generate_ft 第二阶段模型训练
48 - inference 模型推理
49 - app.py 部署到gradio的代码
50 - Jiayi_GPT2_v2.ipynb 与poem_generate代码一样
51
52 文件夹说明:
53 - data 存储清洗后的数据
54 - logs 记录模型训练日志
55 - checkpoint_lora_v4.1 保存模型
56
57 ## 数据处理
58
59 训练使用的数据包含两个部分,一个是各种类型的古诗词文本数据,另一个是带有标签等信息的古诗词数据。
60
61 文本数据来自[chinese-poetry/chinese-poetry](https://github.com/chinese-poetry/chinese-poetry),包含了 5.5 万首唐诗、26 万首宋诗、2.1 万首宋词和其他古典文集,诗词均以繁体字存储,使用[zhconv](https://github.com/gumblex/zhconv)库可以将繁体字转为简体字。
62
63 标签数据来自[yxcs/poems-db](https://github.com/yxcs/poems-db),通过爬取古诗文网,得到了22万首古诗词以及注释赏析等信息,但是信息不全面且包含许多噪声,需要数据清洗。
64
65 模型训练分成两个阶段,第一阶段直接进行Language Modelling,使用较大规模的古诗词进行训练,使模型理解古诗词的基本结构,第二阶段添加诗词相关的标签作为提示进行Language Modelling,由于有标签的古诗词较少,所以经过过滤之后使用少量样本微调。
66
67 ### chinese-poetry数据集数据处理
68
69 ```python
70 from pathlib import Path
71 import json
72 from tqdm import tqdm
73 import re
74 import matplotlib.pyplot as plt
75 import random
76
77
78 files = [i for i in Path("chinese-poetry/json").glob("poet*")]
79 files.sort(key=lambda x: int(x.stem.split('.')[2]))
80 poems = []
81 for f in tqdm(files):
82 for item in json.load(open(f, encoding='utf-8')):
83 del item['strains']
84 poem = ''.join(item['paragraphs'])
85 poem = re.sub(r'(.*?)', '', poem) # 删除中文括号内的注释
86 if len(poem) > 100 or len(poem) < 12: # 12: 日坐竹马桥,夜宿牧牛轩。
87 continue
88 poems.append(poem)
89
90 plt.hist([len(i) for i in poems], bins=30)
91 plt.show()
92
93 # output
94 with open("tang_song_poems.txt", "w+", encoding='utf-8') as f:
95 f.write('\n'.join(poems))
96
97 ```
98
99 由于chinese-poetry数据集的数据分散在多个json文件中,所以先将其汇总为一个txt文件,每一行是一首诗,同时先进行简单数据清洗,根据长度的分布去掉过长和过短的古诗。
100
101 ```python
102 from tqdm import tqdm
103 import re
104
105 filtered = []
106 total_cnt = 0
107 with open("tang_song_poems.txt", encoding='utf-8') as f:
108 for line in tqdm(f.readlines()):
109 total_cnt += 1
110
111 line = line.strip('。')
112 line = line.strip('\n')
113 sentences = re.split(r'[,。]', line)[:-1]
114 lens = [len(i) for i in sentences]
115
116 if "□" in line: # 奇怪的字符
117 continue
118 if len(set(lens)) > 1: # 长度不统一
119 continue
120 if lens[0] not in (5, 7): # 非5言或7言
121 continue
122 if len(sentences) % 2 != 0: # 句数不为偶数
123 continue
124 if len(sentences) < 4 or len(sentences) > 8: # 过长
125 continue
126
127 #line = re.sub(r'[,。?]', ' ', line).strip()
128 filtered.append(line)
129
130 print(f"{len(filtered)}/{total_cnt}")
131 with open("tang_song_poems_filter.txt", "w+", encoding='utf-8') as f:
132 f.write("\n".join(filtered))
133 ```
134
135 随后对数据进行进一步的清洗,对于一些编码错误的字符删去,同时为了降低训练难度,我们限制古诗为5言或7言,并且为2~4句。
136
137 之后根据9比1的比例分为训练集和测试集:
138
139 ```python
140 with open("tang_song_poems_filter.txt", encoding='utf-8') as src:
141 lines = src.readlines()
142 idx = int(0.9 * len(lines))
143 with open("train_poems.txt", "w+", encoding='utf-8') as f:
144 f.write("".join(lines[:idx]))
145 with open("test_poems.txt", "w+", encoding='utf-8') as f:
146 f.write("".join(lines[idx:])
147 ```
148
149 ### 带标签古诗数据处理
150
151 数据如下所示,包含较多的元信息,准备使用的是`content`和`tags`两个字段,
152
153 ```json
154
155 {'_id': {'$oid': '5c22086497880d3b825c968f'},
156 'content': ['\n渡远荆门外,来从楚国游。',
157 '山随平野尽,江入大荒流。',
158 '月下飞天镜,云生结海楼。',
159 '仍怜故乡水,万里送行舟。\n'],
160 'translate': ['乘船远行,路过荆门一带,来到楚国故地。',
161 '青山渐渐消失,平野一望无边。长江滔滔奔涌,流入广袤荒原。',
162 '月映江面,犹如明天飞镜;云彩升起,变幻无穷,结成了海市蜃楼。',
163 '故乡之水恋恋不舍,不远万里送我行舟。'],
164 'translate_res': ['张国举.唐诗精华注译评.长春:长春出版社,2010:128-129',
165 '裴斐.李白诗歌赏析集.成都:巴蜀书社,1988年2月:13-18',
166 '于海娣 等.唐诗鉴赏大全集.北京:中国华侨出版社,2010:116'],
167 'tags': ['唐诗三百首', '初中古诗', '长江', '送别', '思乡'],
168 'notes': ['渡远荆(jīng)门外,来从楚国游。荆门:山名,位于今湖北省宜都县西北长江南岸,与北岸虎牙三对峙,地势险要,自古即有楚蜀咽喉之称。远:远自。楚国:楚地,指湖北一带,春秋时期属楚国。',
169 '山随平野尽,江入大荒流。平野:平坦广阔的原野。江:长江。大荒:广阔无际的田野。',
170 '月下飞天镜,云生结海楼。月下飞天镜:明月映入江水,如同飞下的天镜。下:移下。海楼:海市蜃楼,这里形容江上云霞的美丽景象。',
171 '仍怜故乡水,万里送行舟。 仍:依然。怜:怜爱。一本作“连”。故乡水:指从四川流来的长江水。因诗人从小生活在四川,把四川称作故乡。万里:喻行程之远。'],
172 'reference': [],
173 'appreciation': ['\u3000\u3000这首诗是李白出蜀时所作。李白这次出蜀,由水路乘船远行,经巴渝,出三峡,直向荆门山之外驶去,目的是到湖北、湖南一带楚国故地游览。“渡远荆门外,来从楚国游”,指的就是这一壮游。这时候的青年诗人,兴致勃勃,坐在船上沿途纵情观赏巫山两岸高耸云霄的峻岭,一路看来,眼前景色逐渐变化,船过荆门一带,已是平原旷野,视域顿然开阔,别是一番景色:',
174 '\u3000\u3000“山随平野尽,江入大荒流。”',
175 '\u3000\u3000“山随平野尽”,形象地描绘了船出三峡、渡过荆门山后长江两岸的特有景色:山逐渐消失了,眼前是一望无际的低平的原野。著一“随”字,化静为动,将群山与平野的位置逐渐变换、推移,真切地表现出来。这句好比用电影镜头摄下的一组活动画面,给人以流动感与空间感,将静止的山岭摹状出活动的趋向来。',
176 '\u3000\u3000“江入大荒流”,写出江水奔腾直泻的气势,从荆门往远处望去,仿佛流入荒漠辽远的原野,显得天空寥廓,境界高远。后句著一“入”字,写出了气势的博大,充分表达了诗人的万丈豪情,充满了喜悦和昂扬的激情,力透纸背,用语贴切。景中蕴藏着诗人喜悦开朗的心情和青春的蓬勃朝气。',
177 '\u3000\u3000颔联这两句不仅由于写进“平野”、“大荒”这些辽阔原野的意象,而气势开阔;而且还由于动态的描写而十分生动。大江固然是流动的,而山脉却本来是凝固的,“随、尽”的动态感觉,完全是得自舟行的实际体验。在陡峭奇险,山峦叠嶂的三峡地带穿行多日后,突见壮阔之景,豁然开朗的心情可想而知。它用高度凝炼的语言。极其概括地写出了诗人整个行程的地理变化。',
178 '\u3000\u3000写完山势与流水,诗人又以移步换景手法,从不同角度描绘长江的近景与远景:',
179 '\u3000\u3000“月下飞天镜,云生结海楼。”',
180 '\u3000\u3000长江流过荆门以下,河道迂曲,流速减缓。晚上,江面平静时,俯视月亮在水中的倒影,好象天上飞来一面明镜似的;日间,仰望天空,云彩兴起,变幻无穷,结成了海市蜃楼般的奇景。这正是从荆门一带广阔平原的高空中和平静的江面上所观赏到的奇妙美景。如在崇山峻岭的三峡中,自非亭午夜分,不见曦月,夏水襄陵,江面水流湍急汹涌,那就很难有机会看到“月下飞天镜”的水中影像;在隐天蔽日的三峡空间,也无从望见“云生结海楼”的奇景。这一联以水中月明如圆镜反衬江水的平静,以天上云彩构成海市蜃楼衬托江岸的辽阔,天空的高远,艺术效果十分强烈。颔颈两联,把生活在蜀中的人,初次出峡,见到广大平原时的新鲜感受极其真切地写了出来。',
181 '\u3000\u3000颈联两句反衬江水平静,展现江岸辽阔,天空高远,充满了浪漫主义色彩。',
182 '\u3000\u3000李白在欣赏荆门一带风光的时候,面对那流经故乡的滔滔江水,不禁起了思乡之情:',
183 '\u3000\u3000“仍怜故乡水,万里送行舟。”',
184 '\u3000\u3000诗人顺着长江远渡荆门,江水流过的蜀地也就是曾经养育过他的故乡,初次离别,他怎能不无限留恋,依依难舍呢?但诗人不说自己思念故乡,而说故乡之水恋恋不舍地一路送我远行,怀着深情厚意,万里送行舟,从对面写来,越发显出自己思乡深情。诗以浓重的怀念惜别之情结尾,言有尽而情无穷。诗题中的“送别”应是告别故乡而不是送别朋友,诗中并无送别朋友的离情别绪。清沈德潜认为“诗中无送别意,题中二字可删”(《唐诗别裁》),这并不是没有道理的。',
185 '\u3000\u3000这首诗首尾行结,浑然一体,意境高远,风格雄健。“山随平野尽,江入大荒流”,写得逼真如画,有如一幅长江出峡渡荆门长轴山水图,成为脍炙人口的佳句。如果说优秀的山水画“咫尺应须论万里”,那么,这首形象壮美瑰玮的五律也可以说能以小见大,以一当十,容量丰富,包涵长江中游数万里山势与水流的景色,具有高度集中的艺术概括力。'],
186 'appreciation_res': ['何国治 等.唐诗鉴赏辞典.上海:上海辞书出版社,1983:302-303'],
187 'onlyId': '50b4388a212b8f42992a63458edbf3f7',
188 'name': '渡荆门送别',
189 'dynasty': '唐代',
190 'author': '李白',
191 'sourceLink': 'https://so.gushiwen.org/shiwenv_d50eb19399e6.aspx',
192 'type': '唐诗三百首',
193 'format': '五言律诗',
194 'updateAt': '2018-12-13T08:36:12.589Z'}
195 ```
196
197 标签数量分布统计和可视化结果如下图:
198
199 ![诗词标签-数据可视化.png](pics/诗词标签-数据可视化.png)
200
201 对无用标签进行清洗。去掉其中例如“唐诗三百首”、“高中必备古诗”这些无用的tag,只保留“送别”、“思乡”这类tag。
202
203 ```python
204 from collections import Counter
205 import re
206
207 cnt = Counter()
208 for i in tag_data:
209 cnt.update(i['tags'])
210
211 ban_tags = []
212 for tag in cnt.keys():
213 if re.search(r'\d', tag):
214 ban_tags.append(tag)
215 elif '唐' in tag or '宋' in tag:
216 ban_tags.append(tag)
217 elif len(tag) >= 5:
218 ban_tags.append(tag)
219 elif '小学' in tag or '中学' in tag or '高中' in tag or '初中' in tag:
220 ban_tags.append(tag)
221 elif '诗经' in tag:
222 ban_tags.append(tag)
223 ```
224
225 删除的标签如下:
226
227 ```text
228 ['唐诗三百首',
229 '早教古诗100首',
230 '小学生必背古诗70首',
231 '小学生必背古诗80首',
232 '初中古诗',
233 '小学古诗',
234 '古诗里的十二个月',
235 '高中古诗',
236 '写狗古诗18首',
237 '初中文言文',
238 '诗经',
239 '高中文言文',
240 '古诗十九首',
241 '宋词精选',
242 '小学文言文',
243 '古诗三百首',
244 '宋词三百首',
245 '春天|写人',
246 '离别|抒情|伤感|怀人']
247 ```
248
249 对于内容,删去非常长的古诗,并删去其中的特殊字符、注释、英文标点符号。
250
251 ```python
252 tag_poems = []
253 for item in tag_data:
254 # 太长的删去
255 if len(item['content']) > 6:
256 continue
257 if '诗经' in item['tags']:
258 continue
259 # 无意义tag删去
260 refined_tags = [i for i in item['tags'] if i not in ban_tags]
261 if len(refined_tags) == 0:
262 continue
263 refined_content = ''.join(item['content'])
264 refined_content = re.sub(r'[A-Za-z0-9\s/<>《》〔〕]', '', refined_content)
265 refined_content = re.sub(r'(.*?)', '', refined_content)
266 refined_content = re.sub(r'\(.*?\)', '', refined_content)
267 # refined_content = re.sub(r'[,。?,]', ' ', refined_content)
268 refined_content.replace(',', ',').replace('?', '?').replace('!', '!')
269 refined_content = refined_content.strip()
270 # 词删去(字数不一)
271 if len(set([len(i) for i in re.split(r'[,。?!]', refined_content)[:-1]])) > 1:
272 continue
273 # 太长的删去
274 if len(refined_content) > 150:
275 continue
276
277 tag_poems.append({
278 'tags': ' '.join(refined_tags),
279 'content': refined_content
280 })
281 ```
282
283 ## 第一阶段模型构建与训练
284
285 由于目前已有许多预训练语言模型,所以选择了使用大语料训练过的模型进行微调。
286
287 选择在HuggingFace上发布的`IDEA-CCNL/Wenzhong-GPT2-110M`模型,包含110M参数,使用BPE分词,在300G的悟道语料上进行预训练。该模型在封神榜系列模型中属于自然语言生成任务的通用模型。
288
289 ![模型分类](pics/fenshenbang-模型分类.png)
290
291 为了提升训练效率,使用peft库进行高效的微调,具体使用LoRA方法,最终仅训练1.02%的参数。
292
293 ### 导入相关库
294
295 ```python
296 import time
297 import math
298 import random
299
300 import numpy as np
301 import torch
302 import torch.nn as nn
303 from tqdm import tqdm
304 import transformers
305 from tensorboardX import SummaryWriter # 这个库提供Tensorboard的日志功能
306 from transformers import AutoTokenizer, AutoModelForCausalLM
307 from torch.utils.data import Dataset, DataLoader
308 from peft import TaskType, LoraConfig, get_peft_model
309 ```
310
311 ### 构建数据集和处理输入数据
312
313 PoemDataset用于构建诗歌数据集。
314
315 ```python
316 class PoemDataset(Dataset):
317 def __init__(self, path):
318 super().__init__()
319 self.poems = open(path, encoding='utf-8').readlines() # [:30000]
320
321 def __getitem__(self, idx):
322 text = self.poems[idx].strip()
323 return text
324
325 def __len__(self):
326 return len(self.poems)
327
328 ```
329
330 prepare_inputs用于处理输入的样本。首先使用随机选择的prompt作为输入的前缀,并使用tokenizer将其转换为模型可接受的输入格式。然后,将样本列表samples使用tokenizer转换为模型可接受的输入格式。最后,将prompt的输入和样本的输入拼接在一起,生成input_ids、label_ids和注意力掩码。需要注意prompt在训练过程不参与loss的计算,因此对其进行`*-100`的操作。
331
332 使用Hugging Face的transformers库中的AutoTokenizer类从预训练模型路径(MODEL_PATH)加载tokenizer。并且将填充标记设置为eos(end of sentence)标记,这样会使模型处理填充部分的表示更加连贯,效果会更好。
333
334 ```python
335 tokenizer = AutoTokenizer.from_pretrained(MODEL_PATH)
336 tokenizer.pad_token = tokenizer.eos_token
337
338 prompts = [
339 "写一首唐诗:",
340 "写一首古诗:",
341 "写一首绝句:",
342 "生成一首唐诗:",
343 "生成一首古诗:",
344 "生成一首绝句:",
345 "这是一首唐诗:",
346 "这是一首古诗:",
347 "这是一首古代诗歌:",
348 "生成一首压韵的古诗:",
349 ]
350
351 def prepare_inputs(samples):
352 prompt_inputs = tokenizer([random.choice(prompts)] * len(samples),
353 padding="longest", truncation=True, add_special_tokens=False, return_tensors='pt')
354 inputs = tokenizer(samples, padding="longest", truncation=True, add_special_tokens=False, return_tensors='pt')
355
356 input_ids = torch.cat([
357 prompt_inputs.input_ids,
358 inputs.input_ids
359 ], dim=1)
360 label_ids = torch.cat([
361 torch.ones_like(prompt_inputs.input_ids) * -100, # no loss for prompt
362 inputs.input_ids
363 ], dim=1)
364 attention_mask = torch.cat([prompt_inputs.attention_mask, inputs.attention_mask], dim=1)
365 return input_ids, attention_mask, label_ids
366 ```
367
368 调用诗歌数据集,构建训练验证数据集。
369
370 ```python
371 # prepare data
372 train_dataset = PoemDataset("train_poems_v2.txt")
373 test_dataset = PoemDataset("test_poems_v2.txt")
374
375 train_dataloader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
376 val_dataloader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)
377 ```
378
379 ### 定义训练模型函数
380
381 定义用于训练模型的train函数。使用tensorboard在训练过程中记录训练日志。
382
383 ```python
384 def train(model: nn.Module, dataloaders, optimizer, scheduler, **kwargs):
385 """训练的主函数"""
386 train_loader, test_loader = dataloaders
387 device = kwargs['device']
388 writer = SummaryWriter(kwargs['logger_name'])
389 model = model.to(device)
390
391 for epoch in range(kwargs['max_epochs']):
392 # ========== Train ==========
393 loss_list = []
394 model.train()
395 last_time = time.time()
396 for local_step, inputs in enumerate(train_loader):
397 step = epoch * kwargs['steps_per_epoch'] + local_step
398 input_ids, attention_mask, label_ids = prepare_inputs(inputs)
399
400 optimizer.zero_grad()
401
402 with torch.autocast(device_type='cuda'): # fp16
403 outputs = model(
404 input_ids=input_ids.to(device),
405 attention_mask=attention_mask.to(device),
406 labels=label_ids.to(device),
407 return_dict=True,
408 )
409 loss = outputs.loss
410
411 loss.backward()
412 optimizer.step()
413 scheduler.step()
414
415 # log
416 loss_list.append(loss.detach().cpu().item())
417 if (local_step % 50 == 0 and local_step != 0) or local_step == kwargs['steps_per_epoch'] - 1:
418 avg_loss = sum(loss_list) / len(loss_list)
419 n_step_time = time.time() - last_time
420 left_time = (kwargs['steps_per_epoch'] - local_step) // 50 * n_step_time
421 print("Epoch {}/{} | Step {}/{} | loss:{:.5f} time:{:.1f}s left:{:.1f}m".format(
422 epoch, kwargs['max_epochs'], local_step, kwargs['steps_per_epoch'],
423 avg_loss, n_step_time, left_time / 60
424 ))
425 last_time = time.time()
426 writer.add_scalar('Train/Loss', loss, step)
427 writer.add_scalar('Epoch', epoch, step)
428
429 # if local_step %
430 # torch.save(model.named_parameters(), "checkpoint.pth")
431 model.save_pretrained("checkpoint_lora_v4")
432 # ========== Eval ==========
433 evaluate(model, test_loader, epoch, **kwargs)
434 print("=" * 53)
435 # ========== Inference ==========
436 inference(model)
437 ```
438
439 ### 定义验证模型函数
440
441 定义用于验证的evaluate函数,并计算loss和perplexity(PPL)。erplexity的中文是困惑度,困惑度一般来说是用来评价语言模型好坏的指标。
442
443 困惑度与测试集上的句子概率相关,其基本思想是:给测试集的句子赋予较高概率值的语言模型较好,当语言模型训练完之后,测试集中的句子都是正常的句子,那么训练好的模型就是在测试集上的概率越高越好,公式如下:
444
445 ![PPL](pics/PPL公式.png)
446
447 其中S表示句子,w表示词语。
448
449 ```python
450 @torch.no_grad()
451 def evaluate(model, test_loader, epoch, **kwargs):
452 loss_list = []
453 ppl_list = []
454 print("-" * 20 + " Evaluating " + "-" * 20)
455 model.eval()
456 with torch.no_grad():
457 for local_step, inputs in enumerate(tqdm(test_loader)):
458 input_ids, attention_mask, label_ids = prepare_inputs(inputs)
459
460 outputs = model(
461 input_ids=input_ids.to(device),
462 attention_mask=attention_mask.to(device),
463 labels=label_ids.to(device),
464 return_dict=True,
465 )
466 loss_list.append(outputs.loss.cpu().item())
467 # PPL
468 probs = torch.softmax(outputs.logits, dim=-1).max(dim=-1)[0] # BLC->BL
469 ppl = torch.exp(-probs.log().mean(-1))
470 ppl_list.append(ppl.mean().cpu().item())
471 # log
472 avg_loss = sum(loss_list) / len(loss_list)
473 avg_ppl = sum(ppl_list) / len(ppl_list)
474 print(f"Epoch {epoch}/{kwargs['max_epochs']} | loss:{avg_loss:.5f} | ppl: {avg_ppl: 5f}")
475 # writer.add_scalar('Test/Loss', sum(loss_list) / len(loss_list), epoch * kwargs['steps_per_epoch'])
476 ```
477
478 ### 定义模型推理函数
479
480 输入提示,并将其进行tokenizer,即可生成古诗。模型可以设置最大输出长度,top-p等
481
482 top-p是一种用于在文本生成中控制输出的策略,控制生成结果的多样性。top-p算法保留累计概率之和达到一个给定阈值p的概率分布中的词汇,然后在这个分布中进行随机采样,从而生成一个单词。
483
484 ```python
485 @torch.no_grad()
486 def inference(model):
487 inputs = tokenizer("写一首唐诗:折戟沉沙铁未销,自将磨洗认前朝。", add_special_tokens=False, return_tensors='pt')
488 inputs = inputs.to(device)
489 outputs = model.generate(
490 **inputs,
491 return_dict_in_generate=True,
492 max_length=150,
493 do_sample=True,
494 top_p=0.6,
495 num_return_sequences=5
496 )
497 for line in tokenizer.batch_decode(outputs.sequences, skip_special_tokens=True):
498 print(line)
499 ```
500
501 ### 构建模型
502
503 准备预训练模型并进行模型微调。gpt2_model是从预训练模型中加载的,peft_config指定了任务类型、推理模式、r、lora_alpha、lora_dropout和偏置。
504
505 - r (int): Lora矩阵的最小维度
506 - lora_dropout (float): Lora层的dropout概率
507 - lora_alpha (float): LoRA 缩放因子
508 - bias:指定是否应训练参数。
509
510 为了使微调更有效, LoRA通过低秩分解,用两个较小的权重更新来表示权重更新矩阵。这些新矩阵可以被训练以适应新数据,同时保持较低的更改总数。原始权重矩阵保持冻结状态,不会接收任何进一步的调整。
511
512 ![lora](pics/lora.png)
513
514 ```python
515 MODEL_PATH = r"IDEA-CCNL/Wenzhong-GPT2-110M"
516
517 # prepare model
518 gpt2_model = AutoModelForCausalLM.from_pretrained(MODEL_PATH)
519 peft_config = LoraConfig(
520 task_type=TaskType.CAUSAL_LM,
521 inference_mode=False,
522 r=32, lora_alpha=16, lora_dropout=0.1, bias='all'
523 )
524 gpt2_model = get_peft_model(gpt2_model, peft_config)
525 gpt2_model.print_trainable_parameters()
526 ```
527
528 ### 训练模型
529
530 设置超参数、优化器
531
532 ```python
533 # Hyperparameter
534 batch_size = 32
535 lr = 1e-4
536 device = torch.device('cuda')
537 max_epochs = 4
538 num_warmup_steps = 200
539 num_training_steps = max_epochs * math.ceil(len(train_dataset) / batch_size)
540 seed = 2023
541 logger_name = "Jiayi_GPT2_202307091512"
542
543 # prepare optimizer
544 optim = torch.optim.AdamW(filter(lambda p: p.requires_grad, gpt2_model.parameters()), lr=lr)
545 sche = transformers.get_linear_schedule_with_warmup(optim, num_warmup_steps, num_training_steps)
546 ```
547
548 训练模型
549
550 ```python
551 # train
552 train(
553 gpt2_model,
554 (train_dataloader, val_dataloader),
555 optimizer=optim,
556 scheduler=sche,
557 device=device,
558 max_epochs=max_epochs,
559 logger_name=logger_name,
560 steps_per_epoch=len(train_dataloader)
561 )
562 ```
563
564 训练过程截图如下:
565
566 ![训练过程](pics/训练过程.jpg)
567
568 训练过程中的loss:
569
570 ![loss](pics/loss.png)
571
572 ## 第二阶段模型构建与训练
573
574 第一阶段训练的模型没有诗词标签,无法指定特定类型的诗歌。因此,利用带标签数据集进行第二阶段训练。
575
576 ### 构建数据集和处理输入数据
577
578 与第一阶段不同的是数据需要构造关于tag的提示。
579
580 ```python
581 class TagPoemDataset(Dataset):
582 def __init__(self, path):
583 super().__init__()
584 self.poems = open(path, encoding='utf-8').readlines()
585
586 def __getitem__(self, idx):
587 tag, poem = self.poems[idx].strip().split('|')
588 tag = "写一首关于" + tag.replace(' ', '、') + "的古诗:"
589 return tag, poem
590
591 def __len__(self):
592 return len(self.poems)
593 ```
594
595 处理输入数据,同时处理tag和诗歌。
596
597 ```python
598 def prepare_inputs(samples):
599 tags, poems = samples
600 bs = len(tags)
601
602 prompt_inputs = tokenizer(tags, add_special_tokens=False)
603 poems_inputs = tokenizer(poems, add_special_tokens=False)
604 prompt_len = [len(i) for i in prompt_inputs.input_ids]
605 input_ids_list = [x1 + x2 for x1, x2 in zip(prompt_inputs.input_ids, poems_inputs.input_ids)]
606 max_len = max([len(i) for i in input_ids_list])
607
608 input_ids = torch.ones(bs, max_len, dtype=torch.long) * tokenizer.pad_token_id
609 attention_mask = torch.zeros(bs, max_len, dtype=torch.long)
610 for i in range(bs):
611 input_ids[i, :len(input_ids_list[i])] = torch.tensor(input_ids_list[i])
612 attention_mask[i, :len(input_ids_list[i])] = 1
613 label_ids = input_ids.clone()
614 for i in range(bs):
615 label_ids[i, :prompt_len[i]] = -100
616
617 return input_ids, attention_mask, label_ids
618 ```
619
620 模型训练和模型推理的过程和第一阶段很类似,在此不再赘述。
621
622 ### 构建模型
623
624 这里选择了更大的WenZhong模型,其他过程与第一阶段类似。
625
626 ```python
627 MODEL_PATH = r"IDEA-CCNL/Wenzhong2.0-GPT2-3.5B-chinese"
628 ```
629
630 ### 训练模型
631
632 设置超参数:这里的训练数据少,因此batchsize更小。由于属于第二阶段训练,学习率也提高了。
633
634 ```python
635 # Hyperparameter
636 batch_size = 4
637 lr = 5e-4
638 device = torch.device('cuda')
639 max_epochs = 10
640 num_warmup_steps = 50
641 num_training_steps = max_epochs * math.ceil(len(train_dataset) / batch_size)
642 seed = 2023
643 ```
644
645 ## 根据提示生成藏头诗
646
647 通过限制第一个字的输入生成藏头诗,并且设置停止符号为中文标点`,。`,从而控制一句诗的结束。
648
649 ```python
650 class ChineseCharacterStop(StoppingCriteria):
651 def __init__(self, chars: list[str]):
652 self.chars = [
653 tokenizer(i, add_special_tokens=False, return_tensors='pt').input_ids
654 for i in chars
655 ]
656 # for chars, tokens in zip(chars, self.chars):
657 # print(f"'{chars}':{tokens}")
658
659 def __call__(self, input_ids: torch.LongTensor,
660 scores: torch.FloatTensor, **kwargs) -> bool:
661 for c in self.chars:
662 c = c.to(input_ids.device)
663 match = torch.eq(input_ids[..., -c.shape[1]:], c)
664 if torch.any(torch.all(match, dim=1)):
665 return True
666 return False
667
668
669 tokenizer = AutoTokenizer.from_pretrained("IDEA-CCNL/Wenzhong-GPT2-110M")
670 tokenizer.pad_token = tokenizer.eos_token
671 gpt2_model = AutoModelForCausalLM.from_pretrained("IDEA-CCNL/Wenzhong-GPT2-110M")
672 model = PeftModel.from_pretrained(gpt2_model, 'checkpoint_lora_v4.1')
673
674
675 def cang_tou(tou: str):
676 poem_now = "写一首唐诗:"
677 for c in tou:
678 poem_now += c
679 print(poem_now)
680 inputs = tokenizer(poem_now, return_tensors='pt')
681 outputs = model.generate(
682 **inputs,
683 return_dict_in_generate=True,
684 max_length=150,
685 do_sample=True,
686 top_p=0.4,
687 num_beams=1,
688 num_return_sequences=1,
689 stopping_criteria=[ChineseCharacterStop(['。', ','])],
690 pad_token_id=tokenizer.pad_token_id
691 )
692 poem_now = tokenizer.batch_decode(outputs.sequences, skip_special_tokens=True)[0]
693 print(poem_now)
694 return poem_now[6:]
695
696
697 def prompt_gen(prompt):
698 inputs = tokenizer(prompt, return_tensors='pt')
699 outputs = model.generate(
700 **inputs,
701 return_dict_in_generate=True,
702 max_length=200,
703 do_sample=True,
704 top_p=0.8,
705 num_beams=5,
706 num_return_sequences=3,
707 # stopping_criteria=[ChineseCharacterStop(['。', ',', ''])],
708 pad_token_id=tokenizer.pad_token_id
709 )
710 res = ''
711 for line in tokenizer.batch_decode(outputs.sequences, skip_special_tokens=True):
712 line = line[len(prompt):]
713 res = res+line+'\n'
714 return res
715
716 ```
717
718 ## 设计交互界面
719
720 将输入提示和模型返回结果的过程设计成gradio的交互界面,已经部署在gradio上,链接为[huggingface.co/spaces/Wendyy/poem-generate](https://huggingface.co/spaces/Wendyy/poem-generate):
721
722 ![提示古诗生成](pics/提示古诗生成.png)
723
724 ![藏头诗生成](pics/藏头诗生成.png)
725
726 ```python
727 css = """
728 #col-container {max-width: 510px; margin-left: auto; margin-right: auto;}
729 a {text-decoration-line: underline; font-weight: 600;}
730 .animate-spin {
731 animation: spin 1s linear infinite;
732 }
733 """
734
735 with gr.Blocks(css=css) as demo:
736 with gr.Column(elem_id="col-container"):
737 gr.Markdown(
738 """
739 <h1 style="text-align: center;">✨古诗生成</h1>
740 <p style="text-align: center;">
741 根据输入的提示生成古诗、藏头诗<br />
742 </p>
743 """
744 )
745 with gr.Tab("提示"):
746 prompt_in = gr.Textbox(label="Prompt", placeholder="写一首关于思乡的古诗:", elem_id="prompt-in")
747 submit_btn = gr.Button("Submit")
748 poetry_result = gr.Textbox(label="Output", elem_id="poetry-output")
749
750 submit_btn.click(fn=prompt_gen,
751 inputs=[prompt_in],
752 outputs=[poetry_result])
753
754 with gr.Tab("藏头诗"):
755 tou_in = gr.Textbox(label="Prompt", placeholder="一见如故", elem_id="tou-in")
756 submit_btn = gr.Button("Submit")
757 cangtou_result = gr.Textbox(label="Output", elem_id="cangtou-output")
758 submit_btn.click(fn=cang_tou,
759 inputs=[tou_in],
760 outputs=[cangtou_result])
761
762
763
764 demo.queue(max_size=12).launch()
765 ```
766
767 ## 生成结果展示
768
769 ### 根据输入的提示写诗
770
771 **写一首抒情诗:**
772
773 山色秋色清,山色晚晴晴。明月满空山,暮云暗掩山。
774
775 **写一首关于思乡的古诗:**
776
777 清明时节自有情,谁言青山与白云。梦里黄河清江水,落日黄河晚晴天。
778
779 **写一首关于咏物的古诗:**
780
781 秋风细雨绵绵起,江湖路边纷纷起。江山自有青山在,白云自有白云飞。
782
783
784 ### 藏头诗
785
786 **嘉怡:**嘉宾结觞酒,怡然笑百年。
787
788 **人工智能:**人间绝美境,工夫终不成。智慧空非善,能悟智有道。
789
790 **一二三四:**一鸟高飞倚江湖,二月春风秋江深。三百里外秋色绝,四十年来花落尽。
791
792 **奇思妙想:**奇观秋色绕江山,思念梅花满山城。妙趣更欲觅第一,想见长江源头头。
793
794 **三元牛奶:**三百里马驰骋空,元日晚来见梅花。牛羊鸣蛇觅石马,奶茶烹鹅做猪笼。
795
796 通过多次尝试,可以看出藏头诗的生成结果要比根据提示写诗的效果好。这是因为带有标签的数据集是较少的,噪声也很大。藏头诗通过开头的字总领全诗,规范整首诗的写作,更为通顺流畅和语义丰富。
797
798 ## 总结和展望
799
800 该古诗生成模型兼有丰富预训练语料和高效微调的优点,可以根据输入的提示(带有古诗主题标签的提示)生成该主题的古诗,还可以根据输入的文字生成藏头诗。虽然尚未进行定量的生成效果评估,但从主观评价的角度,生成结果具有语句流畅、语义丰富、贴合主题等优点。
801
802 未来可以对这个模型进行指标评价,并通过设置更多规则(如抑扬顿挫、押韵等)来提升生成结果。此外,模型生成结果的意蕴还有待提升,缺乏真正的思想和情感,通过更多的语料、提示、标注信息将有可能对此进行提升。
0 # 基于大语言模型和高效微调的古诗生成模型
1
2 该古诗生成模型兼有丰富预训练语料和高效微调的优点,可以根据输入的提示(带有古诗主题标签的提示)生成该主题的古诗,还可以根据输入的文字生成藏头诗。
3
4 ## 生成结果展示
5
6 ### 根据输入的提示写诗
7
8 **写一首抒情诗:**
9
10 山色秋色清,山色晚晴晴。明月满空山,暮云暗掩山。
11
12 **写一首关于思乡的古诗:**
13
14 清明时节自有情,谁言青山与白云。梦里黄河清江水,落日黄河晚晴天。
15
16 **写一首关于咏物的古诗:**
17
18 秋风细雨绵绵起,江湖路边纷纷起。江山自有青山在,白云自有白云飞。
19
20
21 ### 藏头诗
22
23 **嘉怡:**嘉宾结觞酒,怡然笑百年。
24
25 **人工智能:**人间绝美境,工夫终不成。智慧空非善,能悟智有道。
26
27 **一二三四:**一鸟高飞倚江湖,二月春风秋江深。三百里外秋色绝,四十年来花落尽。
28
29 **奇思妙想:**奇观秋色绕江山,思念梅花满山城。妙趣更欲觅第一,想见长江源头头。
30
31 **三元牛奶:**三百里马驰骋空,元日晚来见梅花。牛羊鸣蛇觅石马,奶茶烹鹅做猪笼。