PyTorch实战:BI-LSTM模型的情感分析详解

总述

本文的目标是针对一个句子,给出其情感二分类,正向/负向。

代码存放地址:

https://github.com/stay-leave/BI-LSTM-sentiment-classify

输入数据集格式:

标签为1代表正向,0代表负向。

txt版本(即训练集、测试集不在一个文件内),这里我用的是百度千言数据集:

xls版本(即训练集、测试集在一个文件内):

输出数据示例:

接下来对整个流程作梳理。

数据处理

目标:将原始数据转为tensor并加载到dataloader,以供后续使用。

思路是将文本从txt或xls中提取出来,进行分词,划分句子长度,将句子进行编码,最后将其加载到pytorch的dataloader类。

1.提取文件

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

结果如下:

2.对句子进行分词

上面的数据中同时包含句子和标签,因此需要将其分开进行处理。

这是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

结果如下:

3.建立字典,对句子进行编码

思路是统计词频,将句子转换为数字序列,同时根据自己设置的句子长度对句子进行截取和补全。

这里使用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了。

4.将数据加载到dataloader类

以训练集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

数据处理完成!接下来进行模型构建。

BI-LSTM模型构建

关于该模型的原理这篇大神的博客讲得非常好

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值