【上市公司文本分析】Python+Pytorch微调BERT预训练模型,使用大语言模型完成文本分类任务——金星晔等(2024)《经济研究》大语言模型方法的复现
目录
- 0 背景介绍
- 1 环境及依赖库安装
- 2 使用示例数据集,跑通大语言模型代码
- 2.1 源代码下载
- 2.2 代码的运行步骤
- 3 获取上市公司年报文本,转换为txt
- 4 提取“MD&A”和董事会报告部分
- 5 确定词典,构建待标记语句库
- 6 开始人工标注工作
- 7 划分训练集、验证集、测试集,并按模型可读形式保存
- 8 微调预训练模型,计算常用指标,得到适应新任务的大语言模型
- 8.1 训练前操作
- 8.2 模型训练
- 8.3 测试集上的指标计算
- 8.4 模型评价与选优
- 8.4.1 验证集上的表现
- 8.4.2 测试集上的表现
- 9 批量文本预测
- 10 更换预训练模型,寻找最优模型
此篇博客实现的论文已发表在CSSCI期刊《科学决策》上,如该篇文章对您有帮助,欢迎引用。
梁宇奇,赵云辉,付星星,等.企业包容性创新的测度:基于大语言模型的新方法[J].科学决策,2024,(12):86~99
0 背景介绍
金星晔等在《经济研究》2024年第3期发表了一篇题为《企业数字化转型的测度难题:基于大语言模型的新方法与新发现》,使用ERNIE模型,替代了传统以词频为依据的企业数字化转型变量的测量方法,本篇博客的目的就在于复现该篇论文的方法。
注:如果仅仅是需要这篇论文的数字化转型数据,可以去聂辉华老师2024年8月20日的微博中寻找下载链接。
金星晔,左从江,方明月,等.企业数字化转型的测度难题:基于大语言模型的新方法与新发现[J].经济研究,2024,59(3):34-53.
这篇论文在方法上的创新确实解决了词频方法的种种弊端,不过技术实现难度不大。简而言之就是用成熟的预训练模型,导入自己打好标签的语料库进行微调,再用训练好的模型完成文本分类任务,其实里边人工标注的时间才是最大的。
论文中使用的ERNIE模型也不一定在所有情境下都是最优的,百度飞桨其实也做的不成熟,目前来看使用Google最原始的BERT模型,基于Pytorch框架来的成熟,网上教程也更多。
因此,本篇博客面向零基础、弱基础的经管学生,提供一套易上手方法,使用Pytorch框架和BERT模型对该论文进行复现,同时也提供基于ERNIE模型的复现思路。
1 环境及依赖库安装
我使用的是windows10,显卡是cpu版本,在Anaconda3中的spyder中运行,python版本是3.9.12,以上硬件配置和软件环境对代码运行的影响较小,自行下载Anaconda安装运行即可。
最主要的依赖库有3个,一个是Pytorch(2.0.0)版本,提供基本的模型训练,另一个是transformers(4.26.1),还需要一个openpyxl(3.0.9)。
注:以上依赖库建议使用我所使用的版本,其他新版本可能出错。
下载方式:打开Anaconda3目录下的Anaconda Powershell Prompt,依次输入下方命令即可完成依赖库的安装,会提示successfully install。
pip install torch==2.0.0 -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install torchvision==0.15.1 -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install torchaudio==2.0.1 -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install transformers==4.26.1 -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install openpyxl==3.0.9
如果已经提前安装过了这两个库,可以先用uninstall卸载掉这两个库。
其他依赖库,例如pandas、numpy、os等,一般使用Anaconda3自带的版本即可。
2 使用示例数据集,跑通大语言模型代码
做这个工作之前,我试过了网上很多方法,由于环境难配置、报错无法解决、流程不完整等原因,付出了大量的沉默成本,最后机缘巧合搜到了下方这篇博客,用一下午就完成了所有工作。
博客链接:【文本分类】利用bert-base-chinese训练自己的模型完成中文文本分类任务(pytorch实现)
下边复现上边这篇博客,如果此步跑通,基本就成功了一大半。
2.1 源代码下载
为不侵犯该博主的知识产权,请进入上述博客,从该博客里的链接中下载所有代码、示例数据集以及预训练的中文Bert模型。
具体下载方式为:
1.点击下载仓库,访问该博主的github主页,如果访问不了使用csdn加速器的github加速功能,进入后下载打包好后的zip压缩包,解压。
2.在解压后的文件夹里新建一个空文件夹,命名为bert-base-chinese,点击bert-base-chinese镜像下载,下载该页面所有文件(包含4个大文件,耐心等待)至该文件夹。
3.在解压后的文件夹里新建一个空文件夹,命名为bert_checkpoint。
示例数据还是比较大的,体量有20w,如果显卡不是gpu版本,运行速度会特别慢,没必要在示例数据上边浪费过多时间,因此可以进入THUCNews文件夹,打开里边的train.txt(训练集),test.txt(测试集),dev.txt(验证集),删除掉里边大部分的数据。
我的显卡是cpu,训练集保留了1500条左右,测试集和验证集保留了200条左右,epoch为5时训练时间在20分钟左右,准确率在0.84左右。
注:可以每种标签都保留一定数量,这样准确率不会太低
2.2 代码的运行步骤
- 1.使用Spyder打开bert_train.py,运行。
该文件里的训练超参数部分可以更改数值,建议只对epoch值进行修改。该过程可能要持续一段时间,控制台会输出训练过程。训练好的模型保存在bert_checkpoint文件夹中。其中best.pt是准确率最高的模型文件,last.pt是最后一轮 epoch得到的模型文件,如果最后一轮准确率最高,二者就完全相同。 - 2.打开bert_test.py,运行。
调用best.pt,运用验证集验证该模型的效果,输出该模型的准确率。 - 3.打开bert_tuili.py,运行。
调用best.pt,使用单个句子进行预测,验证该模型是否可以对其他句子进行分类,检验模型效果。如果出现了关于input函数的报错,可以直接将while True后的代码改为下方代码,在text里输入待预测句子即可。
text = '此处输入待预测的句子'
bert_input = tokenizer(text, padding='max_length',
max_length = 35,
truncation=True,
return_tensors="pt")
input_ids = bert_input['input_ids'].to(device)
masks = bert_input['attention_mask'].unsqueeze(1).to(device)
output = model(input_ids, masks)
pred = output.argmax(dim=1)
print(real_labels[pred])
3 获取上市公司年报文本,转换为txt
其实可以通过马克数据网、咸鱼等途径直接获取到txt文件,该步骤可以跳过。。
在此留下我基于巨潮资讯网获取年报pdf链接、批量下载和pdf转txt的博客地址,根据个人需求进行修改。
pdf链接获取:https://blog.csdn.net/weixin_43956523/article/details/137409841
从excel中批量下载:https://blog.csdn.net/weixin_43956523/article/details/136265883?spm=1001.2014.3001.5501
pdf转txt:https://blog.csdn.net/weixin_43956523/article/details/124217368
4 提取“MD&A”和董事会报告部分
其实也可以通过上述途径直接获取到txt文件,该步骤可以跳过。
但我感觉这些途径获取到的数据似乎忽略了董事会报告部分,所以写了一个代码,可以自行选择是否使用。一些做文本分析的经管类文章里在介绍时简单得用“MD&A”(即管理层讨论与分析)部分作为文本分析样本,但实际上在很多年报中并无叫该名的章节,可能还会叫董事会报告等一系列名称,所以按照下方文献的思路,重新编制代码,提取相应部分。
[1]姚加权,张锟澎,郭李鹏,等.人工智能如何提升企业生产效率?——基于劳动力技能结构调整的视角[J].管理世界,2024,40(2):101-116+133+117-122.
因代码较为复杂,完整代码的分模块分析见博客:
https://blog.csdn.net/weixin_43956523/article/details/140712274?spm=1001.2014.3001.5501
5 确定词典,构建待标记语句库
根据句号和分号分隔文本,并根据文献,确定自己的词典,借助Python内置的jieba中文分词库对语句进行分词,筛选出含有关键词的语句,构建待标记词库,保存为excel,具体代码思路详见注释。
import os
import pandas as pd
import jieba
import jieba.analyse
item=1 #保存到excel上的行号
fileList=os.listdir('finaltxt')#已提取完毕的txt年报文件目录
fileList.sort()#对年报按文件名进行排序
df=pd.DataFrame(columns=['code','year','sign','keyword','length','sentences'],index=range(1,45001))#预先设置列名,包括股票代码、年报年份、标记、提取出该句子所依据的关键词、文本字符长度、句子,,创建45000行的空表,行数可自行估计设定
Dict=['','','']#自行录入关键词词典,注意是英文符号
for i in Dict:
jieba.add_word(i)# 向jieba内加入这些词语,防止被拆分
for index,i in enumerate(fileList):
name=i[:-4].split('_')#根据txt文件名称进行切分,例如文件名为000001_2014_20140307_2013年年度报告.txt,name[0]为000001,name[1]为2014,name[3][:4]为2013。
with open('finaltxt\\'+i,'r',encoding='utf-8') as f:
text=f.read()
text=text.replace('\n','')#删除换行符
text=text.replace(';','。')#将分号统一换成句号
textList=text.split('。') #按句号分割
for i in textList:
words=jieba.lcut(i)
for word in words:
if word in Dict:#如果识别到有分词结果在关键词词典里,就录入信息
#向df里录入信息
df['code'][item]=name[0]
df['year'][item]=name[3][:4]
df['keyword'][item]=word
df['length'][item]=len(i)
df['sentences'][item]=i
item+=1
break
else:
continue
f.close()
print(str(index)+'完成')
df.to_excel('result.xlsx')
6 开始人工标注工作
这里只需要统一标记标准,安排研究人员标记即可。
切记要标记准确,不然会严重影响模型的分类性能。
篇幅最小,但费时最多。
7 划分训练集、验证集、测试集,并按模型可读形式保存
在训练前,需要按照一定比例(代码中为8:1:1),需要把py文件和excel放在同一文件夹下,生成的txt文件也会在同个文件夹下
import pandas as pd
import random
total= #输入总标记条数
dtype={'sentences':str,'sign':str}
df=pd.read_excel('result.xlsx',sheet_name='sheet',dtype=dtype)#读取语料库文件
L1 = random.sample(range(total),total*0.8)#训练集
lastList1 = [x for x in range(total) if x not in L1]
L2 = random.sample(lastList1,total*0.1)#验证集
L3 = [x for x in range(total) if x not in L1 and x not in L2] #测试集
train=''
test=''
dev=''
# 遍历每一行,按句子,\t,标记的格式保存到txt
for i in range(total):
if i in L1:
train=train+df['sentences'][i]+'\t'+str(df['sign'][i])+'\n'
elif i in L2:
test=test+df['sentences'][i]+'\t'+str(df['sign'][i])+'\n'
else:
dev=dev+df['sentences'][i]+'\t'+str(df['sign'][i])+'\n'
with open('train.txt','w',encoding='utf-8') as w:
w.write(train)
with open('test.txt','w',encoding='utf-8') as w:
w.write(test)
with open('dev.txt','w',encoding='utf-8') as w:
w.write(dev)
8 微调预训练模型,计算常用指标,得到适应新任务的大语言模型
8.1 训练前操作
复制粘贴一遍原THUCNews文件夹,改成自定义的名字,将上一步骤产生的txt文件剪切到新文件夹下,替换掉原先的train.txt、val.txt、test.txt,并将class.txt中的信息,按照0到N的标记顺序,修改为新标签的含义。
打开bert_get_data.py,将下图中打码的部分,改成新文件夹自定义的名字,保证训练集、验证集、测试集的路径对应准确。
8.2 模型训练
在bert_tarin.py中将epoch(迭代次数)改成自己想要的次数,这是为了让模型多训练几次从而观察参数选择预测性能相对最优的模型。
同时把保存最优的模型这一块代码注释掉(原代码仅根据验证集的准确率选择最优模型,未考虑loss值和在测试集上的表现,过于直接),改为将每个模型都保存下来,以备进行测试集上的参数评估。
注意:若如此操作,需要留足400M×epoch的存储空间
save_model(f'model{epoch_num + 1}.pt')
'''
# 保存最优的模型
if total_acc_val / len(dev_dataset) > best_dev_acc:
best_dev_acc = total_acc_val / len(dev_dataset)
save_model('best.pt')
'''
训练过程示例如下图,建议将每个epoch的四个参数都记录下来,作为模型评估标准
8.3 测试集上的指标计算
使用下述代码(适用于二分类,0-1) ,可以通过修改模型文件名依次计算每个epoch生成的模型的精准率、召回率、准确率、F1、F0.8五项指标,相关含义可以自行搜索,一般认为最重要的指标是精准率。
import os
from transformers import BertTokenizer
import torch
from bert_get_data import BertClassifier
import pandas as pd
bert_name = './bert-base-chinese'
tokenizer = BertTokenizer.from_pretrained(bert_name)
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
save_path = './bert_checkpoint'
model = BertClassifier()
model.load_state_dict(torch.load(os.path.join(save_path, 'model1.pt')))#此处修改模型文件名
model = model.to(device)
model.eval()
real_labels = []
with open('./(自己的文件名)/class.txt', 'r') as f:
for row in f.readlines():
real_labels.append(row.strip())
f.close()
total1=0 #实际1的个数
pre1=0 #被预测为1的个数
acc1=0 #预测1对的个数
acc0=0 #预测0对的个数
totalsen=0 #总语句数量
with open('./(自己的文件名)/test.txt', 'r', encoding='utf-8') as f:
lines=f.readlines()
for line in lines:
sentence,sign=line.split('\t')
if sign[0]=='1':
total1+=1
bert_input = tokenizer(sentence, padding='max_length',
max_length = 35,
truncation=True,
return_tensors="pt")
input_ids = bert_input['input_ids'].to(device)
masks = bert_input['attention_mask'].unsqueeze(1).to(device)
output = model(input_ids, masks)
pred = output.argmax(dim=1)
if real_labels[pred]=='no':# 标签为0的实际名称
totalsen+=1
if sign[0]=='0':
acc0+=1
print(str(totalsen)+'完成,结果为no')
elif real_labels[pred]=='yes':# 标签为1的实际名称
pre1+=1
totalsen+=1
if sign[0]=='1':
acc1+=1
print(str(totalsen)+'完成,结果为yes')
print(f'1的总个数:{total1},被预测为1的个数:{pre1},预测1正确的个数:{acc1}')
print(f'precision:{acc1/pre1:6f} \nrecall:{acc1/total1:6f} \nacc:{(acc1+acc0)/totalsen:6f} \nf1:{2*(acc1/pre1)*(acc1/total1)/(acc1/pre1+acc1/total1):6f} \nf0.8:{1.64*(acc1/pre1)*(acc1/total1)/(0.64*acc1/pre1+acc1/total1):6f}')#分别计算精准率、召回率、准确率、F1、F0.8
三分类(1-2-3)指标计算代码如下,更多分类可以寻找规律自行修改。
需要注意,多分类中,准确率(accuracy)计算的是全验证集,而其他四项指标计算的是某一类的预测性能。
import os
from transformers import BertTokenizer
import torch
from bert_get_data import BertClassifier
import pandas as pd
bert_name = './bert-base-chinese'
tokenizer = BertTokenizer.from_pretrained(bert_name)
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
save_path = './bert_checkpoint'
model = BertClassifier()
model.load_state_dict(torch.load(os.path.join(save_path, 'model2.pt')))#此处修改模型文件名
model = model.to(device)
model.eval()
real_labels = []
with open('./(自己的文件夹名)/class.txt', 'r',encoding='utf-8') as f:
for row in f.readlines():
real_labels.append(row.strip())
f.close()
print(real_labels)
total1=0 #实际1的个数
total2=0 #实际2的个数
total3=0 #实际3的个数
pre1=0 #被预测为1的个数
pre2=0 #被预测为1的个数
pre3=0 #被预测为1的个数
acc1=0 #预测1对的个数
acc2=0 #预测1对的个数
acc3=0 #预测1对的个数
totalsen=0 #总语句数量
with open('./(自己的文件夹名)/test.txt', 'r', encoding='utf-8') as f:
lines=f.readlines()
for line in lines:
sentence,sign=line.split('\t')
if sign[0]=='1':
total1+=1
elif sign[0]=='2':
total2+=1
elif sign[0]=='3':
total3+=1
bert_input = tokenizer(sentence, padding='max_length',
max_length = 35,
truncation=True,
return_tensors="pt")
input_ids = bert_input['input_ids'].to(device)
masks = bert_input['attention_mask'].unsqueeze(1).to(device)
output = model(input_ids, masks)
pred = output.argmax(dim=1)
if real_labels[pred]=='xx1':# 标签为1的实际名称
pre1+=1
totalsen+=1
if sign[0]=='1':
acc1+=1
print(str(totalsen)+'完成')
elif real_labels[pred]=='xx2': # 标签为2的实际名称
pre2+=1
totalsen+=1
if sign[0]=='2':
acc2+=1
print(str(totalsen)+'完成')
elif real_labels[pred]=='xx3': #标签为3的实际名称
pre3+=1
totalsen+=1
if sign[0]=='3':
acc3+=1
print(str(totalsen)+'完成')
print(f'1的总个数:{total1},被预测为1的个数:{pre1},预测1正确的个数:{acc1}')
print(f'2的总个数:{total2},被预测为2的个数:{pre2},预测2正确的个数:{acc2}')
print(f'3的总个数:{total3},被预测为3的个数:{pre3},预测3正确的个数:{acc3}')
print(f'acc:{(acc1+acc2+acc3)/totalsen:6f}\n')# 计算整体的准确率
print(f'第一类\nprecision:{acc1/pre1:6f} \nrecall:{acc1/total1:6f} \nf1:{2*(acc1/pre1)*(acc1/total1)/(acc1/pre1+acc1/total1):6f} \nf0.8:{1.64*(acc1/pre1)*(acc1/total1)/(0.64*acc1/pre1+acc1/total1):6f}')#分别计算精准率、召回率、F1、F0.8
print(f'第二类\nprecision:{acc2/pre2:6f} \nrecall:{acc2/total2:6f} \nf1:{2*(acc2/pre2)*(acc2/total2)/(acc2/pre2+acc2/total2):6f} \nf0.8:{1.64*(acc2/pre2)*(acc2/total2)/(0.64*acc2/pre2+acc2/total2):6f}')#分别计算精准率、召回率、F1、F0.8
print(f'第三类\nprecision:{acc3/pre3:6f} \nrecall:{acc3/total3:6f} \nf1:{2*(acc3/pre3)*(acc3/total3)/(acc3/pre3+acc3/total3):6f} \nf0.8:{1.64*(acc3/pre3)*(acc3/total3)/(0.64*acc3/pre3+acc3/total3):6f}')#分别计算精准率、召回率、F1、F0.8
8.4 模型评价与选优
8.4.1 验证集上的表现
1.train loss 下降,val loss下降: 表明网络还在学习
2. train loss下降,val loss稳定:网络过拟合
3.train loss稳定,val loss下降:数据集有问题
4.train loss稳定,val loss稳定:可能已经收敛,或者学习遇到瓶颈,可以调小学习率试试
5.train loss上升,val loss上升:网络结构设计有问题,或者训练参数设置不当等,及时停止学习,调整代码
一般来说,如果到最后一次 ,模型的参数值仍比较稳定,那就选择最后一次的模型,如果中间出现了loss值的较大波动,则选择波动前的最后一次
参考:https://blog.csdn.net/qq_36230981/article/details/129216625
8.4.2 测试集上的表现
主要观察精准率和f1-score的变化趋势 ,结合考虑其他指标进行最优选择。
9 批量文本预测
此处只需要改写bert_tuili.py文件,将其由单个预测变成批量预测,并将结果录入excel表格即可。大概速度在1分钟4-5份年报,如果年报较多可能速度会比较慢。
如果不是一次性运行完,可以将结果复制到新表中后关闭,随后调整start和end值进行下一阶段的运行
放一个我修改后的版本。
import os
from transformers import BertTokenizer
import torch
from bert_get_data import BertClassifier
import pandas as pd
#创建包容性创新句子空表
df=pd.DataFrame(columns=['code','year','time','sentence','sign'],index=range(1,4001))
fileList=os.listdir('finaltxt')
fileList.sort()
bert_name = './bert-base-chinese'
tokenizer = BertTokenizer.from_pretrained(bert_name)
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
save_path = './bert_checkpoint'
model = BertClassifier()
model.load_state_dict(torch.load(os.path.join(save_path, 'model13.pt')))
model = model.to(device)
model.eval()
real_labels = []
with open('./InclusiveInnovation/class.txt', 'r') as f:
for row in f.readlines():
real_labels.append(row.strip())
f.close()
index=1
start=0
end=2000
for i in range(start,end+1):
with open('finaltxt\\'+fileList[i],'r',encoding='utf-8') as f:
text=f.read()
text=text.replace(';','。')
textList=text.split('。')
for t in textList:
bert_input = tokenizer(t, padding='max_length',
max_length = 35,
truncation=True,
return_tensors="pt")
input_ids = bert_input['input_ids'].to(device)
masks = bert_input['attention_mask'].unsqueeze(1).to(device)
output = model(input_ids, masks)
pred = output.argmax(dim=1)
if real_labels[pred]=='InclusiveInno':
words=fileList[i].split('_')
df['code'][index]=words[0]
df['time'][index]=words[2]
df['year'][index]=words[3][:4]
df['sign'][index]=1
df['sentence'][index]=t
print(str(index)+'完成')
index+=1
print(fileList[i]+'完成,'+str(i))
if i%100==0:
df.to_excel('sentence.xlsx')
df.to_excel(f'sentence_{start}to{end}.xlsx')
10 更换预训练模型,寻找最优模型
金老师的论文里是对比ERNIE和BERT模型,以及传统的机器学习方法的准确率,从而确定使用ERNIE模型,所以在此提供基于ERNIE模型的复现思路。
其实使用不同模型,本质上只是预训练模型的不同,所以只需要更换预训练模型文件即可。
在下方链接的描述中,有作者提供的ERNIE的预训练模型文件的网盘地址,可以自行下载。
https://gitcode.com/649453932/Bert-Chinese-Text-Classification-Pytorch?utm_source=csdn_github_accelerator&isLogin=1
该地址其实是我第一次尝试的代码,跑通了但是缺少了最后预测的这一步
下载后,在代码的同级目录下,新建一个文件夹,命名为ERNIE,将下载好的预训练模型文件(pytorch_model.bin)放到该文件夹,,并从bert-base-chinese文件夹中复制一份其他文件至该文件夹即可。并新建一个文件夹,命名为ERNIE_checkpoint
随后将bert_get_data.py、bert_tuili.py中的bert_name改为’./ERNIE’,bert_train.py、bert_test.py、bert_tuili.py中的save_path全改为’./ERNIE_checkpoint’,重复2.2中的步骤即可。