赞
踩
本文的目标是针对一个句子,给出其情感二分类,正向/负向。
代码存放地址:
https://github.com/stay-leave/BI-LSTM-sentiment-classify
输入数据集格式:
标签为1代表正向,0代表负向。
txt版本(即训练集、测试集不在一个文件内),这里我用的是百度千言数据集:
xls版本(即训练集、测试集在一个文件内):
输出数据示例:
接下来对整个流程作梳理。
目标:将原始数据转为tensor并加载到dataloader,以供后续使用。
思路是将文本从txt或xls中提取出来,进行分词,划分句子长度,将句子进行编码,最后将其加载到pytorch的dataloader类。
txt文件的提取:
def txt_file(self,inpath):
#输入TXT,返回列表
data = []
fp = open(self.inpath,'r',encoding='utf-8')
for line in fp:
line=line.strip('\n')
line=line.split('\t')
data.append(line)
data=data[1:]#去掉表头
return data
xls文件的提取:
def xls_file(self,inpath):
"""提取一个文件为一个列表"""
data = xlrd.open_workbook(self.inpath, encoding_override='utf-8')
table = data.sheets()[0]#选定表
nrows = table.nrows#获取行号
ncols = table.ncols#获取列号
numbers=[]
for i in range(1, nrows):#第0行为表头
alldata = table.row_values(i)#循环输出excel表中每一行,即所有数据
numbers.append(alldata)
return numbers
结果如下:
上面的数据中同时包含句子和标签,因此需要将其分开进行处理。
这是txt文件的代码,若使用xls文件,需要注释掉splitt函数的label那一行,取消下一行的注释。
def tokenlize(self,sentence): #分词,只要/保留 中文/其他字符,单句 #sentence = re.sub('[^\u4e00-\u9fa5]+','',sentence) URL_REGEX = re.compile(r'(?i)\b((?:https?://|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:\'".,<>?«»“”‘’]))',re.IGNORECASE) sentence= re.sub(URL_REGEX,'', sentence)# 去除网址 sentence =jieba.cut(sentence.strip(),cut_all=False,use_paddle=10)#默认精确模式 out=[] for word in sentence: out.append(word) return out def splitt(self,data): #句子和标签的提取 sentence=[] label=[] for i in data: sentence.append(self.tokenlize(i[1])) label.append(int(i[0]))#使用txt #label.append(int(i[2]))#使用xls sentence=tuple(sentence) label=tuple(label) return sentence,label
结果如下:
思路是统计词频,将句子转换为数字序列,同时根据自己设置的句子长度对句子进行截取和补全。
这里使用PAD:0作为补全和未登录词的表示。
首先是建立字典,(词:词频):
txt与xls的转换同上
def count_s(self): #统计词频,排序,建立词典(词和序号对) sentence,label=self.splitt(self.txt_file(self.inpath))#提取数据,分词,使用txt读取 #sentence,label=self.splitt(self.xls_file(self.inpath))#提取数据,分词,使用xls读取 count_dict = dict()#普通词典,词:词频 sentences=[]#合并列表 for i in sentence: sentences += i for item in sentences: if item in count_dict: count_dict[item] += 1 else: count_dict[item] = 1 #print(count_dict) #count_dict_s = sorted(count_dict.items(),key=lambda x: x[1], reverse=True)#以值来排序 count_dict_s = collections.OrderedDict(sorted(count_dict.items(),key=lambda t:t[1], reverse=True))#降序 #print('排序字典:') #print(count_dict_s) vocab=list(count_dict_s.keys())#转换成列表 vocab_index=[i for i in range(1,len(vocab)+1)]#索引值 vocab_to_index = dict(zip(vocab, vocab_index))#词汇索引 vocab_to_index["PAD"] = 0#补全 #vocab_to_index["UNK"] = 0#补零 return vocab_to_index,sentence,label,sentences
结果如下:
有了字典就可以对一个句子进行编码,即转换为数字序列。
同样的,也可以将一个数字序列转换为句子。
def seq_to_array(self,seq,vocab_to_index): #单个句子转换为数字序列,顺序输出标签,需要先将句子分词 #inputs = [] #for i in seq:#取单个句子 seq_index=[]#单个句子的数字序列 for word in seq:#取句子的词 if word in vocab_to_index:#句子的字在字典中 seq_index.append(vocab_to_index[word]) else: seq_index.append(0)#未登录词的处理,为pad # 保持句子长度一致 if len(seq_index) < self.seq_length:#若句子的数字序列短,补全为0 seq_index = [0] * (self.seq_length-len(seq_index)) + seq_index elif len(seq_index) > self.seq_length:#若句子的数字序列长,截断 seq_index = seq_index[:self.seq_length] else: seq_index=seq_index #inputs.append(seq_index)#所有句子的数字序列 #targets = [i for i in label]#对应标签 return seq_index
对于句子
‘你好!我是初学者!’
转换如下:
def array_to_seq(self,indices):
#数字序列转换为句子,一批
vocab_to_index,sentence,label,sentences=self.count_s()
seqs=[]#全部
for i in indices:
seq=[]#单句
for j in i:
for key, value in vocab_to_index.items():
if value==j:
seq.append(key)
seqs.append(seq)
return seqs
对于上面的数字序列
[[0, 0, 0, 0, 6322, 0, 4, 3, 724, 0]]
转换为句子如下:
对句子的编码完毕,接下来就可以加载到tensor了。
以训练集txt文件的加载为例,先是投入句子的编码列表,再转为数组,然后加载到dataloader中。
def data_for_train_txt(self,sentence,vocab_to_index,label): #加载训练集 features=[self.seq_to_array(seq,vocab_to_index) for seq in sentence]#将所有分词好的句子转换为数字序列 # 随机打乱索引 random_order = list(range(len(features))) np.random.seed(2) # 固定种子 np.random.shuffle(random_order)#洗牌 #训练集to数组 features_train = np.array([features[i] for i in random_order]) label_train = np.array([label[i] for i in random_order])[:, np.newaxis] #print(features_train.shape,label_train.shape)#打印形状 #加载到tensor train_data = TensorDataset(torch.LongTensor(features_train), torch.LongTensor(label_train)) train_sampler = RandomSampler(train_data) train_loader = DataLoader(train_data, sampler=train_sampler, batch_size=self.batch_size, drop_last=True) return train_loader
数据处理完成!接下来进行模型构建。
关于该模型的原理这篇大神的博客讲得非常好
https://blog.csdn.net/weixin_42118657/article/details/120022112
实现代码如下,基本每一步都有注释:
class BI_lstm(nn.Module): def __init__(self, vocab_size,vocab_to_index,n_layers,hidden_dim,embed,output_size,dropout): super(BI_lstm, self).__init__() self.n_layers = n_layers # LSTM的层数 self.hidden_dim = hidden_dim# 隐状态的维度,即LSTM输出的隐状态的维度 self.embedding_dim = embed # 将单词编码成多少维的向量 self.dropout=dropout # dropout self.output_size=output_size # 定义embedding,随机将数字编码成向量。还没学会怎么使用预训练词向量 self.embedding = nn.Embedding(vocab_size, self.embedding_dim,padding_idx=vocab_to_index['PAD']) self.lstm = nn.LSTM(self.embedding_dim, # 输入的维度 hidden_dim, # LSTM输出的hidden_state的维度 n_layers, # LSTM的层数 dropout=self.dropout, batch_first=True, # 第一个维度是否是batch_size bidirectional = True#双向 ) # LSTM结束后的全连接线性层 self.fc = nn.Linear(self.hidden_dim*2, self.output_size ) # 由于情感分析只需要输出0或1,所以输出的维度是1# 将LSTM的输出作为线性层的输入 self.sigmoid = nn.Sigmoid() # 线性层输出后,还需要过一下sigmoid self.tanh = torch.nn.Tanh()#激活函数 #self.softmax=nn.Softmax() # 给最后的全连接层加一个Dropout self.dropout = nn.Dropout(self.dropout) def forward(self, x, hidden): """ x: 本次的输入,其size为(batch_size, 200),200为句子长度 hidden: 上一时刻的Hidden State和Cell State。类型为tuple: (h, c), 其中h和c的size都为(n_layers, batch_size, hidden_dim) """ # 因为一次输入一组数据,所以第一个维度是batch的大小 batch_size = x.size(0) # 由于embedding只接受LongTensor类型,所以将x转换为LongTensor类型 x = x.long() # 对x进行编码,这里会将x的size由(batch_size, 200)转化为(batch_size, 200, embedding_dim) embeds = self.embedding(x) #embeds=self.relu(embeds) # 将编码后的向量和上一时刻的hidden_state传给LSTM,并获取本次的输出和隐状态(hidden_state, cell_state) # lstm_out的size为 (batch_size, 200, 128),200是单词的数量,由于是一个单词一个单词送给LSTM的,所以会产生与单词数量相同的输出 # hidden为tuple(hidden_state, cell_state),它们俩的size都为(2, batch_size, 512), 2是由于lstm有两层。由于是所有单词都是共享隐状态的,所以并不会出现上面的那个200 lstm_out, hidden = self.lstm(embeds, hidden) # 接下来要过全连接层,所以size变为(batch_size * 200, hidden_dim), # 之所以是batch_size * 200=40000,是因为每个单词的输出都要经过全连接层。 # 换句话说,全连接层的batch_size为40000 lstm_out = lstm_out.contiguous().view(-1, self.hidden_dim) # 给全连接层加个Dropout out = self.dropout(lstm_out) # 将dropout后的数据送给全连接层 # 全连接层输出的size为(40000, 1) out=torch.reshape(out,(-1,256))#改变形状 out=self.tanh(out)#隐藏层激活函数 out = self.fc(out) # 过一下sigmoid out = self.sigmoid(out) # 将最终的输出数据维度变为 (batch_size, 200),即每个单词都对应一个输出 out = out.view(batch_size, -1) # 只取最后一个单词的输出 # 所以out的size会变为(200, 1) out = out[:,-1] # 将输出和本次的(h, c)返回 return out,hidden def init_hidden(self, batch_size): """ 初始化隐状态:第一次送给LSTM时,没有隐状态,所以要初始化一个 这里的初始化策略是全部赋0。 这里之所以是tuple,是因为LSTM需要接受两个隐状态hidden state和cell state """ hidden = (torch.zeros(self.n_layers*2, batch_size, self.hidden_dim).to(device), torch.zeros(self.n_layers*2, batch_size, self.hidden_dim).to(device) ) return hidden
结果如下:
将数据投喂给模型,进行训练。
def train(config,model,train_loader): #模型训练 model.train() optimizer = torch.optim.Adam(model.parameters(), lr=config.lr)# criterion = nn.BCELoss()# 分类问题 y_loss=[]#训练过程的所有loss for e in range(config.epochs): # initialize hidden state,初始化隐层状态 h = model.init_hidden(config.batch_size) counter = 0 train_losses=[] # 分批 for inputs, labels in train_loader: counter += 1 inputs, labels = inputs.cuda(), labels.cuda()# GPU h = tuple([each.data for each in h]) #model.zero_grad()#梯度清零 output,h= model(inputs, h) output=output[:, np.newaxis]#加上新的维度 #print(inputs) #print(output) #print(labels.float()) train_loss = criterion(output, labels.float()) train_losses.append(train_loss.item()) optimizer.zero_grad() train_loss.backward()#反向传播 optimizer.step()#更新权重 # loss 训练集信息 if counter % config.print_every == 0:#打印间隔 print("Epoch: {}/{}, ".format(e+1, config.epochs), "Step: {}, ".format(counter), "Loss: {:.6f}, ".format(train_loss.item()), "Val Loss: {:.6f}".format(np.mean(train_losses))) y_loss.append(train_loss.item())#写入 # 训练完画图 x = [i for i in range(len(y_loss))] fig = plt.figure() plt.plot(x, y_loss) plt.show() #保存完整的预训练模型 torch.save(model,config.save_model_path)
训练完对其进行测试评估,使用准确率:
def test(config, model, test_loader): #模型验证,计算损失和准确率 criterion = nn.BCELoss()# 分类问题 h = model.init_hidden(config.batch_size) with torch.no_grad():#不计算梯度,不进行反向传播,节省资源 count = 0 # 预测的和实际的label相同的样本个数 total = 0 # 累计validation样本个数 loss=0#损失 l=0#损失的计数 for input_test, target_test in test_loader: h = tuple([each.data for each in h]) input_test = input_test.type(torch.LongTensor)#long target_test = target_test.type(torch.LongTensor) target_test = target_test.squeeze(1) input_test = input_test.cuda()#GPU target_test = target_test.cuda() output_test,h = model(input_test,h)#output_test为输出结果,(0,1) pred=output_test.cpu().numpy().tolist()#输出值列表 target=target_test.cpu().numpy().tolist()#目标值列表 for i,j in zip(pred,target): if round(i)==j: count=count+1#正确个数 total += target_test.size(0)#测试样本总数 #损失计算 loss = criterion(output_test, target_test.float()) loss+=loss#自增 l=l+1#计数 acc=100 * count/ total#测试集准确率 test_loss=loss/l#测试集平均损失 print("test mean loss: {:.3f}".format(test_loss)) print("test accuracy : {:.3f}".format(acc))
训练好的模型就可以直接用来对句子进行预测了。
预测代码:
def predict(config, model, pred_loader): #调用训练好的模型对新句子进行预测,以分好词,编码的形式(调用dataset #model.eval() pred_all=[]#所有结果 with torch.no_grad(): #模型初始化赋值 h = model.init_hidden(config.batch_size_pred)#根据待预测的句子数确定 for dat,id in pred_loader: h = tuple([each.data for each in h]) #dat=torch.Tensor(dat)#列表转张量 dat=dat.cuda()#GPU #print('dat的数据:') #print(dat) output,h= model(dat, h)#输出 #print('output的数据:') #print(output) #pred=output.detach().numpy()#转换数据时不需要保留梯度信息 pred=output.cpu().numpy().tolist()#输出列表[0.521,0.465,...] pred_all=pred_all+pred #最后匹配的时候需要与输入的TXT文件列表做同时循环 return pred_all
保存预测结果:
def save_file(config, alls): """保存结果到excel """ f = openpyxl.Workbook() sheet1 = f.create_sheet('sheet1') sheet1['A1'] = 'id' sheet1['B1'] = '评论内容' sheet1['C1'] = '情感值' sheet1['D1'] = '情感类别'# [0,0.5]负向,(0.5,1]正向 i = 2 # openpyxl最小值是1,写入的是xlsx for all in alls: # 遍历每一页 # for data in all:#遍历每一行 for j in range(1, len(all) + 1): # 取每一单元格 # sheet1.write(i,j,all[j])#写入单元格 sheet1.cell(row=i, column=j, value=all[j - 1]) i = i + 1 # 往下一行 f.save(config.save_pred_path)
此次是基于pytorch框架简单地实现了bi-lstm模型进行文本分类,采用sigmoid函数的输出作为情感值是很不合理的,应该叫倾向值,或者不看该数据也是可以的,只关心正负向就行。
后续将继续学习使用预训练词向量进行训练。
自己也是个小白,还得继续学习。
参考博客:
https://blog.csdn.net/qq_52785473/article/details/122800625
https://blog.csdn.net/qq_40276310/article/details/109248949
https://blog.csdn.net/qq_40276310/article/details/109248949
http://t.csdn.cn/qjkST
https://blog.51cto.com/u_11466419/5184189
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。