1. 原理
2014年的一篇文章开创了cnn使用文本分类的先河 用于句子分类的卷积神经网络 原理简单明了其实就是单层CNN加全连接层: 但与图像中的cnn相比,卷积核的宽度固定为一个字向量的维数,而长度一般为2、3、4、5 上面第一张图中每个单词对应的线是单词向量,可以用word2vec或者glove进行预训练在这个例子中,使用了随机初始化的向量2. 数据预处理
手头有三个文件,分别是train.txt、valid.txt和test.txt每行都是一个字符串字典,格式为{'type':' xx ',' text ':'xxxxx'}。
2.1 转换为csv格式
首先,将每个文件转换为csv文件,该文件分为两列:文本和标签。有四种标签可以转换成数字表示。代码如下:
#获取文件内容
def getData(文件):
f=打开(文件,“r”)
raw_data=f.readlines()
返回原始数据
#转换文件格式
def d2csv(raw_data,label_map,name):
文本=[]
标签=[]
i=0
对于raw_data中的行:
D=eval(line) #将字符串的每一行转换成字典
如果len (d ['type'])=1或len (d ['text'])=1: #过滤掉无效数据
继续
Y=label_map[d['type']] #根据label_map将标注转换为数字表示。
x=d['text']
文本.追加(x)
标签.追加(y)
i=1
如果i00==0:
打印(一)
df=pd。DataFrame({'text':texts,' label':labels})
Df.to _ csv ('data/'name ')。csv ',index=false,sep=' \ t') #保存文件
Label_map={'Execution': 0,' Criminal': 1,' Civil': 2,' Administration': 3}
train _ data=get data(' data/train . txt ')# 22000行
d2csv(列车数据,标签地图,“列车”)
valid _ data=get data(' data/valid . txt ')# 2000行
d2csv(有效数据,标签映射,'有效')
test _ data=get data(' data/test . txt ')# 2000行
d2csv(测试数据,标签映射,“测试”)
2.2 观察数据分布
对于这个任务,需要观察分词后每段文字的长度。因为每句话的长度不一样,所以需要给模型设定一个固定的长度。数据中不够长的部分被填充,多余的部分被丢弃。
训练时只有训练数据,所以观察训练数据的文本长度分布。可以用解霸分词之类的工具。
train_text=[]
对于train_data中的行:
d=评估(线)
t=jieba.cut(d['text'])
火车_文本.附加(t)
sentence _ length=[len(x)for x in train _ text]# train _ text是train.csv中每一行分词后的数据。
%matplotlib笔记本
将matplotlib.pyplot作为plt导入
plt.hist(句子长度,1000,正常=1,累积=真)
plt.xlim(0,1000)
plt.show()
得到长度分布图:
可以看出,长度小于1000的文本占所有训练数据的80%左右,所以训练时每篇文本的长度都是固定的1000字。
2.3 由文本得到训练用的mini-batch数据
目前我们手里的数据是csv形式的两列数据,一列字符串文本,一列数字标签。标签部分不再需要处理,但是文本部分离可训练数据很远。
假设每个词对应的词向量维数为D i m Dim Dim,每个样本的分段长度已知为W=1000 W=1000 W=1000,每个小批量的大小为N N N N那么我们要得到维数为n w d I m n * w * dimnwdim的浮点数据作为模型的小批量输入。
于是还需要以下几个步骤:
去掉分词的停用词,建立一个词表(词表是词到index的映射,index从0到m,m是已知词的个数,比如{ '可爱':0,'漂亮':1,…})。将分词和去除停用词后的数据转换为下标数据,维数应为n a l w n _ { all } * w nall w,n a l n _ { all }。其中长度小于w的样本用特定字符补充,长度超过w的样本被截断。将数据分成N W N * W NW大小的小批量作为模型的输入。根据mini-batch数据,通过映射到词向量,得到最终输入的n w d I m n * w * dimnwdim的大小。(这一步在模型中)
看起来很复杂,我都哭了。手动处理真的很麻烦。不过后来发现有一个和pytorch相关的包,torchtext,可以很方便的做这些步骤,所以我就介绍一下直接用这个包的做法。
在贴代码之前先贴两个torchtext的教程。如果你仍然不理解torchtext入门教程,请阅读torchtext文档。还是不明白,请直接看源代码。对照教程看下面的代码。
首先,它是一个分词函数,写成一个只有一个参数的函数:
定义标记符(x):
res=[w for w in jieba.cut(x)]
返回资源
然后停止使用这个词汇,在网上找一个停用词资源(你也可以跳过这一步):
stop_words=[]
打印(“构建停用词集”)
用open('data/stopwords.dat ')作为f:
对于f.readlines()中的l:
stop_words.append(l.strip())
然后设置两个字段,文本和标签。并定义参数的含义。参见上述文档或教程。
文本=数据。字段(sequential=True,tokenize=tokenizer,fix_length=1000,stop_words=stop_words)
标签=数据。Field(sequential=False,use_vocab=False)
读文件,分词,去掉停用词等等。直接挥手带走:
训练,有效,测试=数据。tabular dataset . splits(path=' data ',train='train.csv ',
验证='valid.csv ',测试='test.csv ',
格式='csv ',
skip_header=True,CSV _ reader _ params={ ' delimiter ':' \ t ' },
fields=[('text ',text),(' label,LABEL)])
建立词汇:
TEXT.build_vocab(火车)
以迭代器形式生成小批量数据:
train_iter,val_iter,test_iter=data。Iterator.splits((train,valid,test),
batch _ size=(args.batch_size,args.batch_size,args . batch _ size),
device=args.device
sort_key=lambda x:len(x.text),
sort_within_batch=False,
重复=假)
仅此而已!简单得可怕!虽然花了很长时间才理解这些功能。最后,这些xxx_iter将生成我们需要的维数为n w n * wnw的数据。
3. 模型
其实模型比较简单,只有一个嵌入图,一层cnn,一个激活函数,一个全连接。
但是你要注意不同大小的卷积核的写法。
您可以选择使用多个神经网络。Conv2d然后手动拼写。这里,nn。使用了ModuleList模块。实际上,它仍然使用多个Conv2d,然后将它们放在一起。
进口火炬
将torch.nn作为nn导入
导入torch.nn.functional as F
类textCNN(nn。模块):
def __init__(self,args):
超级(textCNN,self)。__init__()
self.args=args
Vocab=args.embed_num ##已知字数
Dim=args.embed_dim ##每个单词的向量长度
Cla=args.class_num ##类别数
Ci=1 ##输入的频道数
Knum=args.kernel_num ##每个卷积核的数量
Ks=args.kernel _ sizes # #卷积核列表,格式为[2,3,4]
自我。embed=nn。嵌入(词汇,dim) # #词向量,这里直接随机。
self . convs=nn . modulelist([nn . conv 2d(ci,knum,(k,dim)) for k in ks]) # #卷积层
self.dropout=nn。辍学(args.dropout)
self.fc=nn .线性(len(Ks)*Knum,Cla) ##全连接层
向前定义(自身,x):
x=self.embed(x) #(N,W,D)
x=x.unsqueeze(1) #(N,Ci,W,D)
x=[F.relu(conv(x)).为self.convs] # len(Ks)*(N,Knum,W)中的卷积神经挤压(3)
x=[F.max_pool1d(line,line.size(2)).为x] # len(Ks)*(N,Knum)中的线挤压(2)
x=torch.cat(x,1) #(N,Knum*len(Ks))
x=自我放弃(十)
logit=self.fc(x)
返回分对数
4. 训练脚本
导入操作系统
导入系统
进口火炬
进口火炬。亲笔签名
导入火炬. nn .功能为F
定义训练(训练项,开发项,模型,参数):
if args.cuda:
模型。cuda(参数。设备)
优化器=火炬。optim。亚当(模特。parameters(),lr=args.lr)
步骤=0
best_acc=0
最后一步=0
model.train()
打印('培训.')
对于范围内的纪元(1,args.epochs 1):
对于火车_iter中的批处理:
feature,target=batch.text,batch.label #(W,N) (N)
feature.data.t_()
if args.cuda:
feature,target=feature.cuda(),target.cuda()
optimizer.zero_grad()
logit=模型(特征)
loss=F.cross _熵(逻辑,目标)
loss.backward()
optimizer.step()
步骤=1
如果步骤% args.log_interval==0:
result=torch.max(logit,1)[1].视图(target.size())
更正=(结果。数据==目标。数据).总和()
准确度=校正值* 100.0/批次。批次大小
sys。stdout。写(' \ r batch[{ }]-loss:{:6f } ACC:{:4f } $({ }/{ })' .格式(步骤,
loss.data.item(),
准确性,
纠正,
batch.batch_size))
如果步骤% args.dev_interval==0:
dev_acc=eval(dev_iter,model,args)
如果开发_acc最佳_acc:
最佳_acc=开发_acc
最后一步=步数
if args.save_best:
保存(model,args.save_dir,'最佳,步骤)
否则:
如果步骤-最后一步=参数。早_停:
打印('提前{}步停止。format(args.early_stop))
否则如果步骤% args.save_interval==0:
保存(model,args.save_dir,"快照",步骤)
训练脚本中还有设置【计算机】优化程序以及失败的部分。其余部分比较琐碎的。
模型的保存:
定义保存(型号、保存目录、保存前缀、步骤):
如果不是os.path.isdir(save_dir):
os.makedirs(保存目录)
save_prefix=os.path.join(保存目录,保存前缀)
save_path='{}_steps_{} .角.格式(保存前缀,步骤)
torch.save(model.state_dict(),save_path)
评价评价函数,用来评估验证集与测试集合上的准确率acc。
def eval(data_iter,model,args):
model.eval()
校正,avg_loss=0,0
对于数据_iter中的批处理:
feature,target=batch.text批处理。标签
feature.data.t_()
if args.cuda:
feature,target=feature.cuda(),target.cuda()
logit=模型(特征)
loss=F.cross _熵(逻辑,目标)
avg_loss=loss.data[0]
result=torch.max(logit,1)[1]
修正=(结果。查看(目标。size().data==target.data).总和()
size=len(data_iter.dataset)
avg_loss /=size
准确度=100.0 *校正/尺寸
打印(' \ n评估-损失:{:6f} acc: {:4f}%({}/{}) \n ' .格式(avg_loss,accuracy,corrects,size))
返回精度
5. main函数
这暂时就不贴了。可以参考下一部分给出的github。
在最终的测试集中,准确率达到百分之九十七(毕竟只是四类)。
但是有一个问题,就是随着精度的提高,损耗也在快速增加。
经过一番研究,我大致得出结论,这是没有问题的。比如本例中是四分类,加上全连接层输出的结果是[-10000,0,0,10000],而正确的分类是0。
那么这就是一个错误的结果。计算这单个样本的损失。先计算softmax,大约等于[E20000,E10000,E10000,1E {-20000},E {-10000},E {-10000},1E20000,E10000,E10000,E10000,1]。的真实标签是[1,0,0,0],所以交叉熵是20000。
于是我们发现,这个错题的损失会这么大。最后损失较大也是正常的。
然而,为什么当准确率接近100%时,损耗会迅速增加,这需要进一步研究。很可能是因为随着准确率的提高,结果越来越接近训练集的分布,这样与验证集或测试集分布差异极大的情况就会越来越多。
6.引用
部分代码参考了很多这位老哥的github,在此致谢。和他不一样的主要是数据处理部分。
以上个人经历,希望能给大家一个参考,也希望大家多多支持我们。
郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。