上图为TextCnn的网络结构,我们将利用上图的模型进行MR数据集的分析:
首先一般的文本分析主要为以下步骤:
- 将句子转成词,利用词建立字典
- 词转成向量(word2vec,Glove,bert,nn.embedding)
- 句子补0操作变成等长
- 建立Textcnn模型,训练和测试
我们所要进行的MR数据集分类也要经历上述步骤,不过有所不同的是MR数据集没有划分好训练集,验证集和测试集,需要我们利用K折交叉验证对数据处理。
其中:
( K折交叉验证用于模型调优,所有的数据都被用来训练,会导致过拟合,K折交叉验证可以缓解过拟合。将数据分为k组,每次从训练集中,抽取出k份中的一份数据作为验证集,剩余数据作为测试集。测试结果采用k组数据的平均值。若训练集较大,则k较小,降低训练成本,若训练集较小,则k较大,增加训练数据。如k=10,则90%的数据被训练;k=20,留一K折交叉验证,是K折交叉验证的一种特例,每次从数据集中抽取一个数据作为测试数据,一般用于数据集很小的情况。)
将数据集分为训练集,验证集,测试集,并利用word2vec将词转换成向量,然后进行训练,测试。
下面我将利用代码分析对过程进行解释。
参数设置
class N_config():
def __init__(self):
self.embedding_pretrained = None # 是否使用预训练的词向量
self.n_vocab = 100 # 词表中单词的个数
self.embed_size = 300 # 词向量的维度
self.cuda = False # 是否使用gpu
self.filter_num = 100 # 每种尺寸卷积核的个数
self.filters = [3,4,5] # 卷积核的尺寸
self.label_num = 2 # 标签个数
self.dropout = 0.5 # dropout的概率
self.batch_size = 50 #最大句子长度
self.epoch = 200 #训练的轮数
self.gpu = 0 #是否使用GPU
self.learning_rate = 0.0005 #学习率
self.seed = 1 #随机种子
self.l2 = 0.004 #l2正则化权重
self.use_pretrained_embed = True #是否使用预训练
self.k = 0 #交叉验证的k值
将必要的参数利用class封装在parameter函数中:
- embedding_pretrained —》》是否使用预训练的词向量
- n_vocab —》》词表中单词的个数
- embed_size —》》词向量的维度
- cuda —》》是否使用gpu
- filter_num—》》每种尺寸卷积核的个数
- filters—》》卷积核的尺寸
- label_num —》》标签个数
- dropout—》》dropout的概率
- batch_size—》》最大句子长度
- epoch—》》训练的轮数
- gpu —》》是否使用GPU
- learning_rate —》》学习率
- seed —》》随机种子
- l2 —》》l2正则化权重
数据处理
from torch.utils import data
import os
import random
import numpy as np
from gensim.models import KeyedVectors
class MR_Dataset(data.Dataset):
def __init__(self,state="train",k=0,embedding_type="word2vec"):
self.path = os.path.abspath('.')
if "data" not in self.path:
self.path+="/data"
# 导入数据及
pos_samples = open(self.path+"/MR/rt-polarity.pos",errors="ignore").readlines()
neg_samples = open(self.path+"/MR/rt-polarity.neg",errors="ignore").readlines()
datas = pos_samples+neg_samples
#datas = [nltk.word_tokenize(data) for data in datas]
datas = [data.split() for data in datas]
max_sample_length = max([len(sample) for sample in datas]) # 求句子最大长度,将所有句子pad成一样的长度
labels = [1]*len(pos_samples)+[0]*len(neg_samples)
word2id = {"<pad>":0} # 生成word2id
for i,data in enumerate(datas):
for j,word in enumerate(data):
if word2id.get(word)==None:
word2id[word] = len(word2id)
datas[i][j] = word2id[word]
datas[i] = datas[i]+[0]*(max_sample_length-len(datas[i]))
self.n_vocab = len(word2id)
self.word2id = word2id
self.get_word2vec()
c = list(zip(datas,labels)) # 打乱训练集
random.seed(1)
random.shuffle(c)
datas[:],labels[:] = zip(*c)
if state=="train": # 生成训练集
self.datas = datas[:int(k * len(datas) / 10)] + datas[int((k + 1) * len(datas) / 10):]
self.labels = labels[:int(k * len(datas) / 10)] + labels[int((k + 1) * len(labels) / 10):]
self.datas = np.array(self.datas[0:int(0.9*len(self.datas))])
self.labels = np.array(self.labels[0:int(0.9*len(self.labels))])
elif state == "valid": # 生成验证集
self.datas = datas[:int(k * len(datas) / 10)] + datas[int((k + 1) * len(datas) / 10):]
self.labels = labels[:int(k * len(datas) / 10)] + labels[int((k + 1) * len(labels) / 10):]
self.datas = np.array(self.datas[int(0.9 * len(self.datas)):])
self.labels = np.array(self.labels[int(0.9 * len(self.labels)):])
elif state == "test": # 生成测试集
self.datas = np.array(datas[int(k * len(datas) / 10):int((k + 1) * len(datas) / 10)])
self.labels = np.array(labels[int(k * len(datas) / 10):int((k + 1) * len(datas) / 10)])
def __getitem__(self, index):
return self.datas[index], self.labels[index]
def __len__(self):
return len(self.datas)
将数据处理的MR_Dataset封装在data函数中:
首先导入相应的库, 进行初始化:
- 首先判断在当前路径是否含有data文件如果没有就添加,然后导入数据将MR数据集中的积极和消极的词语分别导入,并进行组合
- 然后将数据中的每一句子的单词分离,由于数据集分为积极和消极两个部分,可以分别利用0,1代表消极和积极构成标签,利用句子的最大长度将所有的数据填充成相同大小,并把每一次映射到词向量里
- 由于数据集中的数据是按照积极和消极数据排序,现在要将数据打乱,由于要保证每一个数据集和标签对应,所以必须将数据集和标签打包并转换成列表并且随机打乱。
- 然后利用k折交叉验证,这里将数据分为10份。将第从开始到第k份的数据集和k+1到结尾的数据和标签取出,并且将其中90%的数据作为训练集,其余10%作为验证集。将第k份数据作为测试集。
- 后利用类中的函数可以方便获取信息如由输入的索引返回数据和标签,和返回词的长度
接上:
def get_word2vec(self):
'''
生成word2vec词向量
:return: 根据词表生成的词向量
'''
if not os.path.exists(self.path+"/word2vec_embedding_mr.npy"): # 如果已经保存了词向量,就直接读取
print ("Reading word2vec Embedding...")
wvmodel = KeyedVectors.load_word2vec_format("D:/python/GoogleNews-vectors-negative300.bin.gz",binary=True)
tmp = []
for word, index in self.word2id.items():
try:
tmp.append(wvmodel.get_vector(word))
except:
pass
mean = np.mean(np.array(tmp))
std = np.std(np.array(tmp))
print (mean,std)
vocab_size = self.n_vocab
embed_size = 300
embedding_weights = np.random.normal(mean,std,[vocab_size,embed_size]) # 正太分布初始化方法
for word, index in self.word2id.items():
try:
embedding_weights[index, :] = wvmodel.get_vector(word)
except:
pass
np.save(self.path+"/word2vec_embedding_mr.npy", embedding_weights) # 保存生成的词向量
else:
embedding_weights = np.load(self.path+"/word2vec_embedding_mr.npy") # 载入生成的词向量
self.weight = embedding_weights
我们已经构成了词典,接下来就是将每一个词转换成Word2vec的词向量:
- 先判断当前路径中是否已经含有了词向量如果含有直接导入,如果没有将已经训练好的word2vec词向量导入。
- 利用数据集中的数据对应求得词向量,加入列表求取方差和标准差利用其和词向量大小和,嵌入大小随机生成权重
- 将每个词向量在word2vec中的词张量保存
- 如果在路径已经含有就将其直接导入。
模型构建
我们basicmodel和TextCnnmodel封装到basic,model两个python函数:
class BasicModule(nn.Module):
def __init__(self):
super(BasicModule, self).__init__()
self.model_name = str(type(self))
def load(self, path):
self.load_state_dict(torch.load(path))
def save(self, path):
torch.save(self.state_dict(), path)
def forward(self):
pass
if __name__ == '__main__':
print('Running the BasicModule.py...')
model = BasicModule()
BasicModel():
- 继承torch.nn.model模块
- 可以将数据导入和保存
class TextCNN(BasicModule):
def __init__(self, config):
super(TextCNN, self).__init__()
if config.embedding_pretrained is not None:
self.embedding = nn.Embedding.from_pretrained(config.embedding_pretrained, freeze=False)
else:
self.embedding = nn.Embedding(config.n_vocab, config.embed_size)
if config.cuda:
self.convs = [nn.Conv1d(config.embed_size, config.filter_num, filter_size).cuda()
for filter_size in config.filters]
else:
self.convs = [nn.Conv1d(config.embed_size, config.filter_num, filter_size)
for filter_size in config.filters]
self.dropout = nn.Dropout(config.dropout)
self.fc = nn.Linear(config.filter_num * len(config.filters), config.label_num)
def conv_and_pool(self, x, conv):
x = F.relu(conv(x))
x = F.max_pool1d(x, x.size(2)).squeeze(2)
return x
def forward(self, x):
out = self.embedding(x)
out = out.transpose(1, 2).contiguous()
out = torch.cat([self.conv_and_pool(out, conv) for conv in self.convs], 1)
out = self.dropout(out)
out = self.fc(out)
return out
通过BasicModel的导入TextCnnmodel()
- 首先进行初始化:先判断是否进行预训练,和GPU对数据进行嵌入层和卷积处理,设置dropout函数和线性全连接层将卷积的结果分类。
- 由池化层将定义激活函数和最大池化
- 定义forward模块,对输入的权重嵌入,并将其第二维和第三维转置,将数据先卷积再最大池化,将所有得到的数据cat集合在一起,经Dropout后经线性全连接层进行分类
注:其中涉及的所有函数中的参数均参考 N_config中的设定
验证工具
由于我们要进行交叉验证,对数据要进行不断处理,所以当数据的loss一直不变时停止训练;我将EarlyStopping封装到tool函数中:
class EarlyStopping:
"""Early stops the training if validation loss doesn't improve after a given patience."""
def __init__(self, patience=7, verbose=False, delta=0,cv_index = 0)
self.patience = patience
self.verbose = verbose
self.counter = 0
self.best_score = None
self.early_stop = False
self.val_loss_min = np.Inf
self.delta = delta
self.cv_index = cv_index
def __call__(self, val_loss, model):
score = -val_loss
if self.best_score is None:
self.best_score = score
self.save_checkpoint(val_loss, model)
elif score < self.best_score + self.delta:
self.counter += 1
print('EarlyStopping counter: %d out of %d'%(self.counter,self.patience))
if self.counter >= self.patience:
self.early_stop = True
else:
self.best_score = score
self.save_checkpoint(val_loss, model)
self.counter = 0
def save_checkpoint(self, val_loss, model):
'''Saves model when validation loss decrease.'''
if self.verbose:
print('Validation loss decreased (%.5f --> %.5f). Saving model ...'%(self.val_loss_min,val_loss))
torch.save(model.state_dict(), './checkpoints/checkpoint%d.pt'%self.cv_index)
self.val_loss_min = val_loss
- patience (int):改进了上次验证丢失后的等待时间。默认值:7
- verbose (bool):如果为True,则打印每个验证丢失改进的消息。默认值:False
- delta(浮动):监测数量的最小变化,以合格为改进。默认值:0
这里我们将Loss取负数使其越小越好,首先由其best_score默认值为None,由判断可将sore的值赋给其,并将其进行保存,后再经过调用判断sore是否小于self.best_score + self.delta,如果小于使count加1,当count的值大于等于patience (这里我们设置为10),将停止训练设置为Ture;否则将sore再次赋给best_score,保存并将count归零。
主函数
前面我介绍了封装的函数,现在介绍主函数对该项目目的实现:
from parameter import N_config
from data_process import MR_Dataset
from model_buiding import TextCNN
import torch
import torch.optim as optim
import torch.nn as nn
from tool import EarlyStopping
import numpy as np
import torch.autograd as autograd
导入相关库:
将N_config,MR_Datase,TextCnn和EarlyStoppingt从parameter,data_process,model_buiding,tool函数中导入
定义相关函数
config = N_config()
def get_parameter():
print("参数准备完毕!")
print()
将参数初始化
def get_data():
torch.manual_seed(config.seed)
if torch.cuda.is_available():
torch.cuda.set_device(config.gpu)
training_set = MR_Dataset(state="train", k=config.k)
config.n_vocab = training_set.n_vocab
training_iter = torch.utils.data.DataLoader(dataset=training_set,
batch_size=config.batch_size,
shuffle=True,
num_workers=2)
if config.use_pretrained_embed:
config.embedding_pretrained = torch.from_numpy(training_set.weight).float()
else:
pass
valid_set = MR_Dataset(state="valid", k=config.k)
valid_iter = torch.utils.data.DataLoader(dataset=valid_set,
batch_size=config.batch_size,
shuffle=False,
num_workers=2)
test_set = MR_Dataset(state="test", k=config.k)
test_iter = torch.utils.data.DataLoader(dataset=test_set,
batch_size=config.batch_size,
shuffle=False,
num_workers=2)
print("数据准备完毕!")
print()
return training_iter, valid_iter, test_iter,valid_set,test_set
get_data():
- 首先对参数进行随机初始化
- 判断是否使用GPU对训练数据集,验证数据集,测试数据集获取并返回
- 并将其转换成张量形式
def get_model(pa):
model = TextCNN(pa)
if config.cuda and torch.cuda.is_available():
model.cuda()
config.embedding_pretrained.cuda()
print('模型构建完毕!')
print()
return model
def get_loss_and_opitm():
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=config.learning_rate)
print('损失函数和优化器构建完毕!')
print()
return criterion, optimizer
将模型和损失函数以及优化器设置
def get_test_result(data_iter,data_set):
model.eval()
data_loss = 0
true_sample_num = 0
for data, label in data_iter:
if config.cuda and torch.cuda.is_available():
data = data.cuda()
label = label.cuda()
else:
data = torch.autograd.Variable(data).long()
out = model(data)
loss = criterion(out, autograd.Variable(label.long()))
data_loss += loss.data.item()
true_sample_num += np.sum((torch.argmax(out, 1) == label).cpu().numpy()) #(0,0.5)
acc = true_sample_num / data_set.__len__()
return data_loss,acc
将数据集传入,将数据集中的数据和标签遍历出,通过判断是否需要GPU将数据转换成张量,计算数据中正确的个数,最后利用正确的个数除以数据的总长度获得正确率。
进行训练
acc = 0
for i in range(0, 10): # 10-cv
print("现在进行第{}fold的运算".format(i+1))
early_stopping = EarlyStopping(patience=10, verbose=True, cv_index=i)
print("-----获取数据集--------------")
training_iter,valid_iter,test_iter,valid_set,test_set = get_data()
print('------构建模型---------------')
model = get_model(config)
if config.cuda and torch.cuda.is_available():
model.cuda()
config.embedding_pretrained.cuda()
print('------构建损失函数和优化器---------')
criterion ,optimizer = get_loss_and_opitm()
count = 0
loss_sum = 0
通过对10折的交叉验证,将工具EarlyStopping的patience设置为10,进行数据集的构建,和模型构建和优化器的使用
print("-----现在开始训练--------")
for epoch in range(config.epoch):
# 开始训练
model.train()
for data, label in training_iter:
if config.cuda and torch.cuda.is_available():
data = data.cuda()
label = label.cuda()
else:
data = torch.autograd.Variable(data).long()
label = torch.autograd.Variable(label).squeeze()
out = model(data)
l2_loss = config.l2 * torch.sum(torch.pow(list(model.parameters())[1], 2))
loss = criterion(out, autograd.Variable(label.long())) + l2_loss
loss_sum += loss.data.item()
count += 1
if count % 100 == 0:
print("epoch", epoch, end=' ')
print("The loss is: %.5f" % (loss_sum / 100))
loss_sum = 0
count = 0
optimizer.zero_grad()
loss.backward()
optimizer.step()
# save the model in every epoch
# 一轮训练结束,在验证集测试
valid_loss, valid_acc = get_test_result(valid_iter, valid_set)
early_stopping(valid_loss, model)
print("The valid acc is: %.5f" % valid_acc)
if early_stopping.early_stop:
print("Early stopping")
break
# 1 fold训练结果
model.load_state_dict(torch.load('./checkpoints/checkpoint%d.pt' % i))
test_loss, test_acc = get_test_result(test_iter, test_set)
print("The test acc is: %.5f" % test_acc)
acc += test_acc / 10
# 输出10-fold的平均acc
print("The test acc is: %.5f" % acc)
正试训练
- 通过循环进行200轮的训练
- model.train()进入训练模式,和下文中测试中的 get_test_result():中model.eval()进入测试模式有所不同,因为在训练的过程要根据一定的概率Dropout一部分数据,而在测试中不需要,这相当于集成化的学习,如果在测试中使用model.train()会使结果差很多。
- 随后判断是否使用GPU,如果使用就将其转换到GPU中,如果不就将其转换成torch中long的形式,label也将其转换Torch类型
- 然后,将数据传入模型进行L2正则计算总的损失每一百个输出一次,通过手动调零,反向传播求导和优化循环训练
验证 - 将验证集导入get_test_result,early_stopping.early_stop,由其进行判断当达到patience值后停止验证
测试
通过导入先前保存的数据,将测试集传入,获得正确率
输出10折的平均正确率
结果
我的运行截图如下:
运行过程大概是这样,因为我使用的CPU训练很慢,就只截取前一部分。
总结
- 这是我对前一段时间学习的总结笔记
- 主要利用MR数据集(利用K折交叉验证),通过Word2vec进行词向量的转换实现MR数据集的分类
- 这里使用的CPU会使训练的时间很长,可以使用GPU改进