pytorch学习------实现文本情感分类

目录

    • 目标
    • 一、案例介绍
    • 二、思路分析
    • 三、准备数据集
      • 3.1 基础Dataset的准备
      • 3.2 文本序列化
    • 四、构建模型
    • 五、模型的训练和评估
    • 六、效果

目标

  1. 知道文本处理的基本方法
  2. 能够使用数据实现情感分类的

一、案例介绍

本案例主要是学习word embedding这种常用的文本向量化的方法
现在我们有一个经典的数据集IMDB数据集,地址:http://ai.stanford.edu/~amaas/data/sentiment/,这是一份包含了5万条流行电影的评论数据,其中训练集25000条,测试集25000条。数据格式如下:

下图左边为名称,其中名称包含两部分,分别是序号和情感评分,(1-4为neg,5-10为pos),右边为评论内容

pytorch学习------实现文本情感分类_第1张图片

根据上述的样本,需要使用pytorch完成模型,实现对评论情感进行预测

二、思路分析

首先可以把上述问题定义为分类问题,情感评分分为1-10,10个类别(也可以理解为回归问题,这里当做分类问题考虑)。那么根据之前的经验,我们的大致流程如下:

  1. 准备数据集
  2. 构建模型
  3. 模型训练
  4. 模型评估

知道思路之后,那么我们一步步来完成上述步骤

三、准备数据集

准备数据集和之前的方法一样,实例化dataset,准备dataloader,最终我们的数据可以处理成如下格式:

pytorch学习------实现文本情感分类_第2张图片

其中有两点需要注意:

  1. 如何完成基础打Dataset的构建和Dataloader的准备
  2. 每个batch中文本的长度不一致的问题如何解决
  3. 每个batch中的文本如何转化为数字序列

3.1 基础Dataset的准备

#!/usr/bin/env python 
# -*- coding:utf-8 -*-
import os.path
import re

import torch

from lib import ws,max_len

from torch.utils.data import DataLoader,Dataset

# 将内容进行分词
def tokenlize(content):
    #将其他无用符号替换为空字符串
    content = re.sub("<.*?>"," ",content)
    fileters = ['\.',':','\t','\n','\x97','#','$','%','&']
    conent = re.sub("|".join(fileters)," ",content)
    tokens = [i.strip().lower() for i in conent.split()]
    return tokens
#完成数据集的准备
class ImdbDataset(Dataset):
    def __init__(self,train=True):
        self.train_data_path = r"D:\djangoProject\practice\文本情感分类\aclImdb_v1\aclImdb\train"
        self.test_data_path = r"D:\djangoProject\practice\文本情感分类\aclImdb_v1\aclImdb\test"
        data_path = self.train_data_path if train else self.test_data_path
        #把所有的文件名放入列表
        temp_data_path = [os.path.join(data_path,"pos"),os.path.join(data_path,"neg")]
        self.total_file_path = [] #所有的评论的文件path
        for path in temp_data_path:
            file_name_list = os.listdir(path)
            file_path_list = [os.path.join(path,i) for i in file_name_list if i.endswith(".txt")]
            self.total_file_path.extend(file_path_list)

    def __getitem__(self, index):
        file_path = self.total_file_path[index]
        #获取标签
        label_str = file_path.split("\\")[-2]
        label = 0 if label_str=="neg" else 1
        #获取内容
        content = open(file_path,encoding="UTF-8").read()
        #分词处理
        tokens = tokenlize(content)
        return tokens,label
    def __len__(self):
        return len(self.total_file_path)

#自定义collate_fn,解决bug
def collate_fn(batch):
    content,label = list(zip(*batch))
    # content = [ws.transform(i,max_len=max_len) for i in content]
    # content = torch.LongTensor(content)
    # label = torch.LongTensor(label)
    return content,label

#获取数据加载器
def get_dataloader(train=True,batch_size = 128):
    imdb_dataset = ImdbDataset(train)
    data_loader = DataLoader(imdb_dataset,batch_size=batch_size,shuffle=True,collate_fn=collate_fn)
    return data_loader


if __name__ == '__main__':
  for idx,(input,target) in enumerate(get_dataloader()):
      print(idx,input,target)
      break


输出结果如下:
pytorch学习------实现文本情感分类_第3张图片

注意:

#自定义collate_fn,解决bug
def collate_fn(batch):
    content,label = list(zip(*batch))
    # content = [ws.transform(i,max_len=max_len) for i in content]
    # content = torch.LongTensor(content)
    # label = torch.LongTensor(label)
    return content,label

该函数是为了解决数据以元组的方式保存的问题,如果不自定义该函数,会出现以下情况:
pytorch学习------实现文本情感分类_第4张图片
或者数据格式错误
pytorch学习------实现文本情感分类_第5张图片
所以别忘记自定义collate_fn函数

3.2 文本序列化

再介绍word embedding的时候,我们说过,不会直接把文本转化为向量,而是先转化为数字,再把数字转化为向量,那么这个过程该如何实现呢?

这里我们可以考虑把文本中的每个词语和其对应的数字,使用字典保存,同时实现方法把句子通过字典映射为包含数字的列表

现文本序列化之前,考虑以下几点:

  1. 如何使用字典把词语和数字进行对应
  2. 不同的词语出现的次数不尽相同,是否需要对高频或者低频词语进行过滤,以及总的词语数量是否需要进行限制
  3. 得到词典之后,如何把句子转化为数字序列,如何把数字序列转化为句子
  4. 不同句子长度不相同,每个batch的句子如何构造成相同的长度(可以对短句子进行填充,填充特殊字符)
  5. 对于新出现的词语在词典中没有出现怎么办(可以使用特殊字符代理)

思路分析:

  1. 对所有句子进行分词
  2. 词语存入字典,根据次数对词语进行过滤,并统计次数
  3. 实现文本转数字序列的方法
  4. 实现数字序列转文本方法

代码如下:

#!/usr/bin/env python 
# -*- coding:utf-8 -*-

# 实现将句子转化为数字序列和其反转

class Word2Sequence:
    UNK_TAG = "UNK"
    PAD_TAG = "PAD"
    UNK = 0
    PAD = 1

    def __init__(self):
        self.dict = {
            self.UNK_TAG: self.UNK,
            self.PAD_TAG: self.PAD
        }
        self.count = {}  # 统计词频

    def fit(self, sentence):
        """
        把单个句子保存到dict中
        :param sentence:
        :return:
        """
        for word in sentence:
            self.count[word] = self.count.get(word, 0) + 1  # 统计词频

    def build_vocab(self, min=5, max=None, max_features=None):
        """
        生成词典
        :param min:
        :param max:
        :param max_features:
        :return:
        """
        # 删除count中词频小于min的word
        if min is not None:
            self.count = {word: value for word, value in self.count.items() if value >= min}
        if max is not None:
            self.count = {word: value for word, value in self.count.items() if value < max}
        # 限制保留的词语数
        if max_features is not None:
            # 对字典进行排序,并取前max_features个数据进行保留
            temp = sorted(self.count.items(), key=lambda x: x[-1], reverse=True)[:max_features]
            self.count = dict(temp)
        # 构建字典序列
        for word in self.count:
            self.dict[word] = len(self.dict)

        # 得到一个反转的dict字典,方便后续的把序列转换为句子
        self.inverse_dict = dict(zip(self.dict.values(), self.dict.keys()))

    def transform(self, sentence, max_len=None):
        """
        把句子转换为序列
        :param sentence:
        :param max_len:句子长度,对句子填充或者裁剪
        :return:
        """
        if max_len is not None:
            if max_len > len(sentence):
                sentence = sentence + [self.PAD_TAG] * (max_len - len(sentence))  # 填充
            if max_len < len(sentence):
                sentence = sentence[:max_len]  # 裁剪
        num_list = []
        for word in sentence:
            num_list.append(self.dict.get(word, self.UNK))  # 默认为UNK的值,表示稀疏词语,或者没有出现的词语
        return num_list

    def inverse_transform(self, indices):
        """
        把序列转换为句子
        :param indices:
        :return:
        """
        word_list = []
        for idx in indices:
            word_list.append(self.inverse_dict.get(idx))
        return word_list

    def __len__(self):
        return len(self.dict)

if __name__ == '__main__':
    ws = Word2Sequence()
    ws.fit(["我", "是", "谁"])
    ws.fit(["我", "是", "我"])
    ws.build_vocab(min=1)
    print(ws.dict)
    ret = ws.transform(["我", "爱", "北京", "天安门"],max_len=10)
    ret = ws.inverse_transform(ret)
    print(ret)

改代码可以当作一个工具类,以后每次用到,根据业务需求修改直接使用即可!

测试效果:
pytorch学习------实现文本情感分类_第6张图片

完成了wordsequence之后,接下来就是保存现有样本中的数据字典,方便后续的使用。
这里直接运行代码保存数据即可
代码如下:

#!/usr/bin/env python 
# -*- coding:utf-8 -*-
from word_sequence import Word2Sequence
import pickle
import os
from dataset import  tokenlize   #用于分词
from tqdm import tqdm

if __name__ == '__main__':
    ws = Word2Sequence()
    path =  r"D:\djangoProject\practice\文本情感分类\aclImdb_v1\aclImdb\train"
    temp_data_path = [os.path.join(path, "pos"), os.path.join(path, "neg")]
    for data_path in temp_data_path:
        file_paths = [os.path.join(data_path,file_name) for file_name in os.listdir(data_path) if file_name.endswith(".txt")]
        for file_path in tqdm(file_paths):
            sentence = tokenlize(open(file_path,encoding='UTF-8').read())
            ws.fit(sentence)     #统计词频
    ws.build_vocab(min=10,max_features=10000)  #生成词典   每一个词对应用一个数字,且按照数字排序
    print(ws.dict)
    pickle.dump(ws,open("./model/ws.pkl","wb"))   #保存词典到某一文件,方便之后的文本序列化直接使用
    print(len(ws))

效果:
pytorch学习------实现文本情感分类_第7张图片

pytorch学习------实现文本情感分类_第8张图片
这里的保存的最大的词语键值对为10000个,为什么输出结果为10002呢?
因为前面初始化 Word2Sequence时,放入了“UNK”和“PAD
pytorch学习------实现文本情感分类_第9张图片

四、构建模型

这里我们只练习使用word embedding,所以模型只有一层,即:

  1. 数据经过word embedding
  2. 数据通过全连接层返回结果,计算log_softmax

代码如下:

class IMDBModel(nn.Module):
    def __init__(self):
        super(IMDBModel, self).__init__()
        self.embedding = nn.Embedding(len(ws),100)   #给字典中的每一个词语转换为embedding
        self.fc = nn.Linear(max_len*100,2)  #这里我们做两个分类,neg消极文本,pos积极文本,所以为2
    def forward(self,input):
        """
        :param input:[batch_size,max_len]
        :return:
        """
        x = self.embedding(input)
        x = x.view([-1,max_len*100])
        out = self.fc(x)
        return F.log_softmax(out,dim=-1)

五、模型的训练和评估

训练流程和之前相同

  1. 实例化模型,损失函数,优化器
  2. 遍历dataset_loader,梯度置为0,进行向前计算
  3. 计算损失,反向传播优化损失,更新参数
#!/usr/bin/env python 
# -*- coding:utf-8 -*-
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.optim import Adam
from dataset import get_dataloader
from lib import ws,max_len
class IMDBModel(nn.Module):
    def __init__(self):
        super(IMDBModel, self).__init__()
        self.embedding = nn.Embedding(len(ws),100)   #给字典中的每一个词语转换为embedding
        self.fc = nn.Linear(max_len*100,2)  #这里我们做两个分类,neg消极文本,pos积极文本,所以为2
    def forward(self,input):
        """
        :param input:[batch_size,max_len]
        :return:
        """
        x = self.embedding(input)
        x = x.view([-1,max_len*100])
        out = self.fc(x)
        return F.log_softmax(out,dim=-1)

#实例化模型
model = IMDBModel()
optimizer = Adam(model.parameters(),0.001)
#训练模型
def train(epoch):
    for idx,(input,target) in enumerate(get_dataloader(True)):
        # 梯度置零
        optimizer.zero_grad()
        output = model(input)
        #计算损失
        loss = F.nll_loss(output,target)
        #梯度下降
        loss.backward()
        #更新参数
        optimizer.step()
        print(loss.item())

#评估模型
def eval():
    print("start eval")
    test_loss = 0
    correct = 0
    test_dataloader = get_dataloader(False,batch_size=1000)
    with torch.no_grad():
        for idx, (input, target) in enumerate(test_dataloader):
            output = model(input)
            #累加损失
            test_loss += F.nll_loss(output,target,reduction="sum")
            #获取预测值
            pred = torch.max(output,dim=1)[1]
            #与目标值比较,并计算正确率
            correct = pred.eq(target.data).sum()
        #计算平均总损失
        test_loss = test_loss / (len(test_dataloader.dataset))
        print('\nTest set: Avg. loss: {:.4f}, Accuracy: {}/{} ({:.2f}%)\n'.format(
            test_loss, correct, len(test_dataloader.dataset),
            100. * correct / len(test_dataloader.dataset)))


if __name__ == '__main__':
    # 训练模型
    for i in range(1):
        train(i)
    # 评估模型
    eval()

六、效果

可见效果不是很好,因为我们这里的神经网络是很简单的,所以达到的效果不是很好,不过之后我们可以使用循环神经网络进行改进!

pytorch学习------实现文本情感分类_第10张图片

你可能感兴趣的:(神经网络,自然语言处理,pytorch,分类,人工智能,神经网络)