目录
本例子所用的数据为三分类的英文数据,利用torchtext处理数据,构建迭代器并搭建textcnn,将数据用textcnn进行训练,得到训练结果。本例中没有使用验证集对模型进行评估。
一、开发环境和数据集
1、开发环境
Ubuntu 16.04.6
python:3.7
pytorch:1.8.1
torchtext: 0.9.1
2、数据集
数据集:train_data_sentiment
提取码:gw77
二、使用torchtext处理数据集
1、导入必要的库
#导入常用库
import torch
import pandas as pd
import matplotlib.pyplot as plt
from gensim.models import KeyedVectors
import torch.nn as nn
import torch.optim as optim
import torch.utils.data as Data
import torch.nn.functional as F
import torchtext
#比较新版本的需要使用torchtext.legacy.data,旧版本的torchtext使用torchtex.data
from torchtext.legacy.data import TabularDataset
import warnings
warnings.filterwarnings("ignore")
device = torch.device("cuda:1" if torch.cuda.is_available() else "cpu") #我在写博客的时候我们实验室服务器3卡没人用,所以我用的3卡
2、导入并查看数据集
#导入数据集,这一步只是给大家看一下数据集,后面在构建Dataset的时候也可以直接处理数据集
train_data = pd.read_csv('train_data_sentiment.csv')
train_data
3、使用torchtext处理数据集
torchtext对数据的处理主要包括:定义Field、Dataset和迭代器这三部分,可以很方便的对文本数据进行处理,如:分词、截断补长、构建词表等。对torchtext不熟悉的可以学习一下官房文档或者讲解博客。
3.1、定义Field
TEXT = torchtext.legacy.data.Field(sequential=True,lower=True,fix_length=30)#默认分词器是split(),空格分割
LABEL = torchtext.legacy.data.Field(sequential=False,use_vocab=False)
- sequential:是否把数据表示成序列,如果是False, 则不能使用分词。默认值:True。
- lower:是否把数据转化为小写。默认值:False。
- fix_length:在构建迭代器时,将每条文本数据的长度修改为该值,进行截断补长,用pad_token补全。默认值:None。
- use_vocab:是否使用词典对象. 如果是False,数据的类型必须已经是数值类型。默认值:True。
3.2、定义Dataset
TabularDataset可以很方便地读取CSV、JSON或者TSV格式的数据文件。
train_x = TabularDataset(path = 'train_data_sentiment.csv',
format = 'csv',skip_header=True,
fields = [('utterance',TEXT),('label',LABEL)])
- skip_header = True,将列名不作为数据处理。
- fields的顺序要与原数据列的顺序相同。
可以看到文本数据已经被分词
3.3、构建词表、加载预训练词向量
因为计算机不认识文本,所以我们需要将文本数据转换成数值或者向量,这样我们才能输入到textcnn或者深度神经网络中去训练。首先将文本数据构建成词-索引的形式,接着,加载了预训练的词向量之后,每个词会对应一个词向量,即词-向量的形式,最后,在后面的模型训练中,我们就可以使用词嵌入矩阵了,即Embedding层。这样我们就将每个词都转换为向量了,可以输入到模型中进行训练了。
这里解释一下词嵌入矩阵,因为我在学习的时候理解词嵌入矩阵就花费了很长时间。我们构建了词表之后,就可以用索引去表示一句话了,根据下面构建的词表举个例子,例如:it is you
,这句话就可以用10 9 3
表示,当然我们可以直接使用索引输入到网络中进行训练,但是索引所表示的特征太少了,我们为了得到更好的特征去训练网络一般会使用word2vec向量或者glove向量。本文使用了glove向量,这样每个词的特征就得到了更好的表示,更有利于我们训练网络。it is you
中每个词就会被300维向量表示,这样就会将其更多的特征输入到网络中,我们的网络模型就会被训练的更好。总结一下词嵌入矩阵即:①构建词表,即词-索引;②加载预训练词向量,即词-向量;③得到词嵌入矩阵,即索引-向量。
#构建词表
TEXT.build_vocab(train_x) #构建了10440个词,从0-10439
for w,i in TEXT.vocab.stoi.items():
print(w,i)
#加载glove词向量,第一次使用会自动下载,也可以自己下载好该词向量,我这里用的是400000词,每个词由300维向量表示
TEXT.vocab.load_vectors('glove.6B.300d',unk_init=torch.Tensor.normal_) #将数据中有但glove词向量中不存在的词进行随机初始化分配300维向量
我们可以查看一下构建的词嵌入矩阵中的向量,这里展示的是我们所构建的词表中索引为3的词向量,也就是you这个词,当然这个词是被用300维向量表示的,这里只截图了部分展示。
我们再看一下glove向量中you这个词所对应的向量,这里也只截图了部分展示,这就说明了我们可以通过索引获取对应词的向量,也就是词嵌入矩阵的意义。
#查看词向量维度
print(TEXT.vocab.vectors.shape) #torch.Size([10440, 300])
可以看出来,我们的数据总共被分成10440个词,每个词由300维向量表示,接下来就可以构建迭代器了。
3.4、构建迭代器
迭代器有Iterator和BucketIterator
- Iterator:跟原始数据顺序相同,构建批数据。
- BucketIterator:将长度类似的数据构建成一批数据,这样就会减少截断补长操作时的填充。
一般在进行训练网络时,每一次我们都会输入一个batch的数据,我设置了batch_size=64,那么就有9989//64+1=157个batch,因为我们总共有9989条数据,每个batch有64条数据,而9989/64=156余5,那么余下的5条数据就会组成一个batch。
batch_size = 64
train_iter = torchtext.legacy.data.Iterator(dataset = train_x,batch_size=64,shuffle=True,sort_within_batch=False,repeat=False,device=device)
len(train_iter) #157
- shuffle:是否打乱数据
- sort_within_batch:是否对每个批数据内进行排序
- repeat:是否在不同的epoch中重复迭代批数据
查看构建的迭代器以及内部数据表示:
#查看构建的迭代器
list(train_iter)
#查看批数据的大小
for batch in train_iter:
print(batch.utterance.shape)
可以看到每批数据为64条(除了最后一批数据),即batch_size=64,每条数据由30个词组成,我们也能看到最后剩下的5条数据组成了一个batch。
#查看第一条数据
batch.utterance[:,0]#我们取的是第1列,因为第1列表示第一条数据,即第64列表示第64条数据。每条数据由30个词组成,下面非1部分表示第一条数据中的词在词表中的索引,剩下的1表示补长的部分。
#查看第一条数据中的词所对应的索引值
list_a=[]
for i in batch.utterance[:,0]:
if i.item()!=1:
list_a.append(i.item())
print(list_a)
for i in list_a:
print(TEXT.vocab.itos[i],end=' ')
#查看迭代器中的数据及其对应的文本
l =[]
for batch in list(train_iter)[:1]:
for i in batch.utterance:
l.append(i[0].item())
print(l)
print(' '.join([TEXT.vocab.itos[i] for i in l]))
至此,数据就处理好了,我们接下来了解textcnn。
三、textcnn知识与pytorch版框架搭建
1、textcnn知识
textcnn和我们所熟悉处理图像的cnn是异曲同工的,只不过cnn中卷积核大小一般都是k * k
,而NLP中的卷积核大小一般为k * embedding_size
,其中embedding_size为每个词用embdding_size维的词向量表示。例如下图中的一句话由三个词组成,每个词由3维词向量表示,我们选取两个卷积核大小为2*3,可以得到以下结果,池化后的结果其实是会拼接起来,这里为了展示,就没有进行拼接。
从上图可以看出,一个卷积核进行卷积之后得到的结果为[len(sentence)-k+1,1]
,其中len(sentence)表示一句话中由多少个词组成,池化后就会得到[2,1]
的结果,这个结果是进行拼接了的。
基本的理论知识了解之后,我们就可以搭建textcnn的网络框架了,想要详细了解textcnn可以自己找资料继续学习。
2、利用pytorch搭建textcnn
我搭建了一个两层的textcnn网络,textcnn的框架主要是:卷积、激活、池化
。
网络框架中的参数说明:
- vocab_size: 构建的词表中的词数
- embedding_size: 每个词的词向量维度
- num_channels: 输出通道数,也就是卷积核的数量
- kernel_sizes :卷积核尺寸
kernel_sizes, nums_channels = [3, 4], [150, 150]
embedding_size = 300
num_class = 3
vocab_size = 10440
这里我搭建了两层textcnn网络,第一层卷积核大小为:3 * 300,共有150形状相同的卷积核
,第二层卷积核大小为:4*300,同样的也有150个这样的卷积核
,textcnn框架代码如下:
class TextCNN(nn.Module):
def __init__(self,kernel_sizes,num_channels):
super(TextCNN,self).__init__()
self.embedding = nn.Embedding(vocab_size,embedding_size) #embedding层
self.dropout = nn.Dropout(0.5)
self.convs = nn.ModuleList()
for c,k in zip(num_channels,kernel_sizes):
self.convs.append(nn.Sequential(nn.Conv1d(in_channels=embedding_size,
out_channels = c, #这里输出通道数为[150,150],即有150个卷积核大小为3*embedding_size和4*embedding_size的卷积核
kernel_size = k), # 卷积核的大小,这里指3和4
nn.ReLU(), #激活函数
nn.MaxPool1d(30-k+1))) #池化,在卷积后的30-k+1个结果中选取一个最大值,30表示的事每条数据由30个词组成,
self.decoder = nn.Linear(sum(num_channels),3) #全连接层,输入为300维向量,输出为3维,即分类数
def forward(self,inputs):
embed = self.embedding(inputs) #[30,64,300]
embed = embed.permute(1,2,0) #[64,300,30],这一步是交换维度,为了符合后面的卷积操作的输入
#在下一步的encoding中经过两层textcnn之后,每一层都会得到一个[64,150,1]的结果,squeeze之后为[64,150],然后将两个结果拼接得到[64,300]
encoding = torch.cat([conv(embed).squeeze(-1) for conv in self.convs],dim=1) #[64,300]
outputs =self.decoder(self.dropout(encoding)) #将[64,300]输入到全连接层,最终得到[64,3]的结果
return outputs
我们来看一下搭建好的网络框架
net = TextCNN(kernel_sizes, nums_channels).to(device)
net
从图中可以看出,搭建的网络共有两层,这两层的不同之处就是卷积核大小和池化层不相同,其他都相同。
- 输入通道数in_channels=300;
- 输出通道数out_channels=150;
- 卷积核大小:第一层为3 * 300,第二层为4 * 300
- 卷积的步长stride = 1
- 池化层的步长stride = 30-kernel_size+1,因为设定的每条数据都由30个词组成,那么池化的目的就是从卷积结束后的结果中挑出最大的那个值作为结果,这就是最大池化的目的,而卷积后的结果就是
[30-kernel_size+1,1]
,这一步大家可以自己推一下,很简单的。
四、模型训练及结果
1、定义训练函数、优化器、损失函数等参数
一般我是定义好训练函数,再调用进行模型训练,大家可以随意操作。
net.embedding.weight.data.copy_(TEXT.vocab.vectors) #给模型的Embedding层传入我们的词嵌入矩阵
optimizer = optim.Adam(net.parameters(),lr=1e-4) #定义优化器,lr是学习率可以自己调
criterion = nn.CrossEntropyLoss().to(device) #定义损失函数
train_x_len = len(train_x) #这一步是我为了计算后面的Acc而获取的数据数量,也就是9989
#定义训练函数
def train(net,iterator,optimizer,criterion,train_x_len):
epoch_loss = 0 #初始化loss值
epoch_acc = 0 #初始化acc值
for batch in iterator:
optimizer.zero_grad() #梯度清零
preds = net(batch.utterance) #前向传播,求出预测值
loss = criterion(preds,batch.label) #计算loss
epoch_loss +=loss.item() #累加loss,作为下面求平均loss的分子
loss.backward() #反向传播
optimizer.step() #更新网络中的权重参数
epoch_acc+=((preds.argmax(axis=1))==batch.label).sum().item() #累加acc,作为下面求平均acc的分子
return epoch_loss/(train_x_len),epoch_acc/train_x_len #返回的是loss值和acc值
2、进行训练
总共训练100轮,每10轮进行打印输出。
n_epoch = 100
acc_plot=[] #用于后面画图
loss_plot=[] #用于后面画图
for epoch in range(n_epoch):
train_loss,train_acc = train(net,train_iter,optimizer,criterion,train_x_len)
acc_plot.append(train_acc)
loss_plot.append(train_loss)
if (epoch+1)%10==0:
print('epoch: %d \t loss: %.4f \t train_acc: %.4f'%(epoch+1,train_loss,train_acc))
结果如下:
3、可视化结果
#使用画图函数matplotlib
plt.figure(figsize =(10,5),dpi=80)
plt.plot(acc_plot,label='train_acc')
plt.plot(loss_plot,color='coral',label='train_loss')
plt.legend(loc = 0)
plt.grid(True,linestyle = '--',alpha=1)
plt.xlabel('epoch',fontsize = 15)
plt.show()
五、总结
本文主要是利用torchtext处理了训练数据集,将其构建为迭代器用于模型输入,并没有使用验证集评估模型,大家可以尝试从训练集中分出验证集对模型进行评估。完整代码如下:
#代码我测试过了,没有任何问题,一些用于打印输出的代码我注释掉了,
#导入常用库
import torch
import pandas as pd
import matplotlib.pyplot as plt
from gensim.models import KeyedVectors
import torch.nn as nn
import torch.optim as optim
import torch.utils.data as Data
import torch.nn.functional as F
import torchtext
#比较新版本的需要使用torchtext.legacy.data,旧版本的torchtext使用torchtex.data
from torchtext.legacy.data import TabularDataset
import warnings
warnings.filterwarnings("ignore")
device = torch.device("cuda:1" if torch.cuda.is_available() else "cpu") #我在写博客的时候我们实验室服务器3卡没人用,所以我用的3卡
train_data = pd.read_csv('train_data_sentiment.csv')
# train_data
#使用torchtext处理数据
#定义filed
TEXT = torchtext.legacy.data.Field(sequential=True,lower=True,fix_length=30)
LABEL = torchtext.legacy.data.Field(sequential=False,use_vocab=False)
train_x = TabularDataset(path = 'train_data_sentiment.csv',
format = 'csv',skip_header=True,
fields = [('utterance',TEXT),('label',LABEL)])
# print(train_x[0].utterance)
# print(train_x[0].label)
TEXT.build_vocab(train_x)
# for w,i in TEXT.vocab.stoi.items():
# print(w,i)
TEXT.vocab.load_vectors('glove.6B.300d',unk_init=torch.Tensor.normal_)
glove_model = KeyedVectors.load_word2vec_format('glove.6B.300d.word2vec.txt', binary=False)
# glove_model['you']
# print(TEXT.vocab.vectors.shape) #torch.Size([10440, 300])
batch_size = 64
train_iter = torchtext.legacy.data.Iterator(dataset = train_x,batch_size=64,shuffle=True,sort_within_batch=False,repeat=False,device=device)
# len(train_iter)
# list(train_iter)
# for batch in train_iter:
# print(batch.utterance.shape)
# batch.utterance[:,0]
# list_a=[]
# for i in batch.utterance[:,0]:
# if i.item()!=1:
# list_a.append(i.item())
# print(list_a)
# for i in list_a:
# print(TEXT.vocab.itos[i],end=' ')
# l =[]
# for batch in list(train_iter)[:1]:
# for i in batch.utterance:
# l.append(i[0].item())
# print(l)
# print(' '.join([TEXT.vocab.itos[i] for i in l]))
kernel_sizes, nums_channels = [3, 4], [150, 150]
embedding_size = 300
num_class = 3
vocab_size = 10440
#搭建textcnn
class TextCNN(nn.Module):
def __init__(self,kernel_sizes,num_channels):
super(TextCNN,self).__init__()
self.embedding = nn.Embedding(vocab_size,embedding_size)
self.dropout = nn.Dropout(0.5)
self.convs = nn.ModuleList()
for c,k in zip(num_channels,kernel_sizes):
self.convs.append(nn.Sequential(nn.Conv1d(in_channels=embedding_size,
out_channels = c,
kernel_size = k),
nn.ReLU(),
nn.MaxPool1d(30-k+1)))
self.decoder = nn.Linear(sum(num_channels),3)
def forward(self,inputs):
embed = self.embedding(inputs)
embed = embed.permute(1,2,0)
encoding = torch.cat([conv(embed).squeeze(-1) for conv in self.convs],dim=1)
outputs =self.decoder(self.dropout(encoding))
return outputs
net = TextCNN(kernel_sizes, nums_channels).to(device)
# net
net.embedding.weight.data.copy_(TEXT.vocab.vectors)
optimizer = optim.Adam(net.parameters(),lr=1e-4)
criterion = nn.CrossEntropyLoss().to(device)
train_x_len = len(train_x)
#定义训练函数
def train(net,iterator,optimizer,criterion,train_x_len):
epoch_loss = 0
epoch_acc = 0
for batch in iterator:
optimizer.zero_grad()
preds = net(batch.utterance)
loss = criterion(preds,batch.label)
epoch_loss +=loss.item()
loss.backward()
optimizer.step()
epoch_acc+=((preds.argmax(axis=1))==batch.label).sum().item()
return epoch_loss/(train_x_len),epoch_acc/train_x_len
n_epoch = 100
acc_plot=[]
loss_plot=[]
for epoch in range(n_epoch):
train_loss,train_acc = train(net,train_iter,optimizer,criterion,train_x_len)
acc_plot.append(train_acc)
loss_plot.append(train_loss)
if (epoch+1)%10==0:
print('epoch: %d \t loss: %.4f \t train_acc: %.4f'%(epoch+1,train_loss,train_acc))
plt.figure(figsize =(10,5),dpi=80)
plt.plot(acc_plot,label='train_acc')
plt.plot(loss_plot,color='coral',label='train_loss')
plt.legend(loc = 0)
plt.grid(True,linestyle = '--',alpha=1)
plt.xlabel('epoch',fontsize = 15)
plt.show()