循环神经网络和自然语言处理二-文本情感分类

一.案例介绍

为了练习一下word embedding,现在有一个经典的数据集IMDB数据集,其中包含了5完条流行电影的评价,训练集25000条,测试集25000条,根据这些数据,通过pytorch完成模型,实现对评论情感进行预测

二.思路

首先可以把上述问题定义为分类问题,情感评分分为1-10分。十个类别,那么怎样分出着十个类别?

1.准备数据

2.构建模型

3.模型训练

4.模型评估

三.准备数据集

在前面说过准备数据集,实例化dataset,准备dataloader,下面代码每行都有解释的

""" dataset.py文件获取数据 """
from torch.utils.data import DataLoader, Dataset
import os
import re

# 资源路径
data_base_path = r"保存数据的路径\data\aclImdb_v1\aclImdb"

# 创建一个文本处理函数
def tokenize(text):
    # 因为这是一个英文的电影评价,如果里面有下面这些字符会影响判断,所以用正则表达式替换掉
    fileters = ['!', '"', '#', '$', '%', '&', '\(', '\)', '\*', '\+', ',', '-', '\.', '/', ':', ';', '<', '=', '>',
                '\?', '@'
        , '\[', '\\', '\]', '^', '_', '`', '\{', '\|', '\}', '~', '\t', '\n', '\x97', '\x96', '”', '“', ]
    # 正则将<>里面的全部替换掉
    text = re.sub('<.*?>', ' ', text, flags=re.S)
    # 将上面列表中的字符用|来连接,表示只要有这个列表中的字符就替换
    text = re.sub("|".join(fileters), " ", text, flags=re.S)
    # 将处理好的文本用空格的形式转换成列表的形式输出
    return [i.strip() for i in text.split()]

# 准备dataset
class ImdbDataset(Dataset):
    # 设置一个mode参数来区分是训练数据还是测试数据
    def __init__(self,mode):
        super(ImdbDataset,self).__init__()
        if mode=='train':
            # 将训练数据的路径加上训练或者测试文本所在的文件夹,目的是区分开训练和测试的文件路径
            text_path=[os.path.join(data_base_path,i) for i in ['train/neg','train/pos']]
        else:
            text_path=[os.path.join(data_base_path,i) for i in ['test/neg','text/pos']]
        # 创建一个列表来保存所有需要训练或者测试的文件路径
        self.total_file_path_list=[]
        for i in text_path:
            # os.listdir的作用是读取这个文件夹中所有的文件名
            self.total_file_path_list.extend([os.path.join(i,j) for j in os.listdir(i)])

    def __getitem__(self, idx):
        # 这个函数的作用是获取数据
        cur_path=self.total_file_path_list[idx]  # 通过传入的索引来得到文件路径
        cur_filename=os.path.basename(cur_path)   # basename的作用是通过这个文件路径得到这个文件名
        label=int(cur_filename.split('_')[-1].split('.')[0])-1  # 获得的文件名是464_9.txt形式的,通过这行代码得到文件的编号
        text=tokenize(open(cur_path,encoding='utf-8').read().strip())  # 通过文件路径得到文件内容
        # 返回文件编号和内容
        return label,text

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

# 创建数据集对象
dataset=ImdbDataset(mode='train')
# 创建数据迭代器,dataset:数据集,batch_size:每次处理的样本数量,shuffle:是否打乱数据,collate_fn:下面有详细讲解
dataloader=DataLoader(dataset=dataset,batch_size=2,shuffle=True,collate_fn=lambda x:x)

# 打印看看效果
for idx,(label,text) in enumerate(dataloader):
    print('idx',idx)
    print('label',label)
    print('text',text)

collate_fn函数是实例化dataloader的时候, 以函数形式传递给loader.

既然是collate_fn是以函数作为参数进行传递, 那么其一定有默认参数. 这个默认参数就是getitem函数返回的数据项的batch形成的列表.

先假设, datase类是如下形式:

class testData(Dataset):
	def __init__(self):
		super().__init__()
		
	def __getitem__(self, index):
		return x, y

可以看到, 假设的dataset返回两个数据项: x和y. 那么, 传入collate_fn的参数定义为data, 则其shape为(batch_size, 2,…).

知道了输入参数的形式, 就可以去定义collate_fn函数了:

def collate_fn(data):
	for unit in data:
		unit_x.append(unit[0])
		unit_y.append(unit[1])
		...
	return {x: torch.tensor(unit_x),  y: torch.tensor(unit_y)}

可以看到我对collate_fn函数的定义,最后返回的是一个字典. 这也是collate_fn函数最大的一个好处: 可以自定义取出一个batch数据的格式. 该函数的输出就是对dataloader进行遍历, 取出一个batch的数据.

3.如何给collate_fn函数传参

在collate_fn的使用过程中, 我发现只输入data有时候是非常不方便的, 需要额外的参数来传递其他变量.

这里有两个方法可以解决以上问题:

1.使用lambda函数
info = args.info	# info是已经定义过的
loader = Dataloader(collate_fn=lambda x: collate_fn(x, info))
2.创建一个可调用函数
class collater():
	def __init__(self, *params):
		self. params = params
	
	def __call__(self, data):
		'''在这里重写collate_fn函数'''

collate_fn = collater(*params)
loader = Dataloader(collate_fn=collate_fn)

四.文本序列化

前面我们说到不会把文本直接转化为向量,而是先转化为数字,在把数字转化为向量。

这里我们可以将每个词语和数字采用键值对的形式用字典保存起来,同时把句子通过字段映射为包含数字的列表。实现文本序列化之前还需要考虑

1.如何使用字段把词语和数字进行对应

2.不同的词语出现的次数都不一样,是否需要将高频词语和低频词语进行过滤,以及总的词语数量是否需要进行限制

3.得到词典后,如何将句子转化为数字序列,如何把数字序列转化为句子

4.不同的句子长度不同,每个batch的句子如何构造成相同的长度(可以对句子进行填充,填充特殊字符)

5.对于信出现的词语在词典中没有出现怎么办

思路分析:

1.对所有的句子进行分词

2.词语存入字典,根据次数对词语进行过滤,并统计次数

3.实现文本转数字序列的方法

4.实现数字序列转文本的方法

""" word_sequece文件,这里是分词文件 """
import numpy as np

class word2Sequence():
    UNK_TAG='UNK'  # 未知字符替换
    PAD_TAG='PAD'  # 填充字符

    UNK=0  # 未知字符用0替换
    PAD=1  # 用1来填充

    def __init__(self):
        # 构建一个字典来保存词语所对应的数字
        self.dict={
            self.UNK_TAG:self.UNK,
            self.PAD_TAG:self.PAD
        }
        # 这个是用来控制还没有创建字典的时候就执行转换的,没创建字典之前执行转换就报错
        self.fited=False

    def to_index(self,word):  # 将词语转换成数字
        assert self.fited==True  # 断言
        # 返回这个词语对应的数字,如果这个数字不存在就用未知代替
        return self.dict.get(word,self.UNK)

    def to_word(self,index):  # 将数字转换成词语
        assert self.fited
        if index in self.inversed_dict:   # 判断这个向量是不是在向量转词语的字典中,下面创建的
            return self.inversed_dict[index]
        # 如果在则直接返回不再这返回未知
        return self.UNK_TAG

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

    def fit(self,sentences,min_count=1,max_count=None,max_feature=None):  # 接收句子统计词频
        """
        min_count:这个句子中最小的词频
        max_count:这个句子中最大的词频
        max_feature:这个句子中最大的词语数
        """
        # 创建一个字典来保存各个词语的词频
        count={}
        for sentence in sentences:  # 遍历句子
            for a in sentence:  # 遍历字符
                if a not in count: # 在count中计数
                    count[a]=0

                count[a]+=1
        if min_count is not None:   # 如果设置了最小词频
            count={k:v for k,v in count.items() if v>=min_count}  # 过滤掉词频小于最小词频的词语
        if max_count is not None:  # 如果设置了最大词频
            count={k:v for k,v in count.items() if v<=max_count}  # 过滤掉词频大于最大词频的词语
        if isinstance(max_feature,int):  # 如果max_feature是int类型
            count=sorted(list(count.items(),key=lambda x:x[1]))  # 对count通过值来排序
            if max_feature is not None and len(count)>max_feature:  # 当最大词语数量不为None且count中的词语数量大于max_feature时
                count=count[-int(max_feature):]  # 取值最大允许的词
            for w,_ in count: # 遍历出词语和数字
                self.dict[w]=len(self.dict)  # 将结果加入到dict中
        else:
            for w in sorted(count.keys()):  # 对count中的键排序后为列表
                self.dict[w]=len(self.dict)  # 将这些词语加入到dict中,这里是动态的,加一个进去len(dict)就增加一
        self.fited=True
        # 创建一个字典,将dict中的键值对调换,这样就可以用数字找到词语了
        self.inversed_dict=dict(zip(self.dict.values(),self.dict.keys()))

    def transform(self,sentence,max_len=None):  # 将句子转化为数字序列,sentence:句子,max_len最大长度
        assert self.fited
        if max_len is not None:
            # 如果max_len不为None,则构建一个列表为max_len长度的需要填充的列表
            r=[self.PAD]*max_len
        else:  # 否则就构建一个和句子长度相同的需要填充的列表
            r=[self.PAD]*len(sentence)
        if max_len is not None and len(sentence)>max_len: # 当句子长度大于最大句子长度时
            sentence=sentence[:max_len]  # 将句子切片为最大句子长度
        for index,word in enumerate(sentence):  # 遍历句子,index为词语索引,word为词语
            r[index]=self.to_index(word) # 在上面创建的列表中填充词语转换的数字
        # 返回转化numpy1的填充好的列表
        return np.array(r,dtype=np.int64)

    def inverse_transform(self,indices):  # 将数字序列转化为词语序列,indices为传入的数字序列
        sentence=[]
        for i in indices:
            word=self.to_word(i) # 将数字转化为词语
            sentence.append(word)  # 保存词语
        # 返回词语序列
        return sentence

if __name__ == '__main__':
    sentences=['a','b','c','d','e']  
    ws=word2Sequence()
    # 先fit创建出字典
    ws.fit(sentences)
    rs=ws.transform(['a','b','c','d','i','h'])
    re=ws.inverse_transform(rs)
    print(ws.dict)
    print(rs)
    print(re)

完成了分词后,接下来就是报错现有样本中的数据字典,方便后续的使用

五.实现对IMDB数据的处理和保存

""" 这里是保存数据的文件main.py """

from word_sequece import WordSequence  # 从刚刚写的分词中导入分词方法
from dataset import get_dataloader    # 从获取数据哪里导入获取数据的方法
import pickle
from tqdm import tqdm   # 进度条类

if __name__ == '__main__':
    ws = WordSequence()
    dl_train = get_dataloader(True)  # 得到训练数据
    dl_test = get_dataloader(False)   # 得到测试数据
    for reviews, label in tqdm(dl_train, total=len(dl_train)):   # 在dataset的时候返回的是一个标题和数据,reviews是数据,label是标题
        for sentence in reviews:  # 遍历出句子
            ws.fit(sentence)   # 对每个句子分词
    for reviews, label in tqdm(dl_test, total=len(dl_test)):
        for sentence in reviews:
            ws.fit(sentence)
    print(len(ws))  # 42676
    # 保存分好的数据
    pickle.dump(ws, open("./models/ws.pkl", "wb"))

六.构建模型

对数据分词,序列化后就可以构建模型算法了,这里使用到上一章讲到的embedding,所以模型只有一层word embedding,数据通过这一层返回结果然后通过softmax计算损失

"""构建模型文件model.py"""
import torch.nn as nn
import config
import torch.nn.functional as F

class ImdbModel(nn.Module):
    def __init__(self):
        super(ImdbModel,self).__init__()
        self.embedding = nn.Embedding(num_embeddings=len(config.ws),embedding_dim=200,padding_idx=config.ws.PAD)
        # 构造全连接层要使用的算法,这里选用y=wx+b
        self.fc = nn.Linear(config.max_len*200,2)

    def forward(self, input):
        """
        :param input:[batch_size,max_len]
        :return:
        """
        # 传入数据计算
        input_embeded = self.embedding(input) #input embeded :[batch_size,max_len,200]

        #变形
        input_embeded_viewed = input_embeded.view(input_embeded.size(0),-1)

        #全连接层计算后输出
        out = self.fc(input_embeded_viewed)
        # 返回损失
        return F.log_softmax(out,dim=-1)

七.模型训练和评估

  1. 实例化模型,损失函数,优化器

  2. 遍历dataset_loader,梯度置为0,进行向前计算

  3. 计算损失,反向传播优化损失,更新参数

"""进行模型的训练文件train.py"""
from model import  ImdbModel   # 导入模型
from dataset import get_dataloader   # 数据
from torch.optim import Adam  # 优化算法
from tqdm import tqdm  # 查看运行进度条的一个类
import torch.nn.functional as F  # 损失函数

model = ImdbModel()  # 实例化模型
optimizer = Adam(model.parameters())  # 实例化优化类


def train(epoch):
    train_dataloader = get_dataloader(train=True)  # 得到训练数据
    bar = tqdm(train_dataloader,total=len(train_dataloader))
    print(bar)
    print('-'*20)
    for idx,(input,target) in enumerate(bar):  # 遍历训练数据
        optimizer.zero_grad()  # 梯度置0
        output = model(input)  # 模型训练
        loss = F.nll_loss(output,target)  # 计算误差
        loss.backward() # 反向传播计算梯度
        optimizer.step()  # 更新参数
        # 设置进度条显示形式
        bar.set_description("epcoh:{}  idx:{}   loss:{:.6f}".format(epoch,idx,loss.item()))


if __name__ == '__main__':
    for i in range(10):
        train(i)

 到这里就结束了!!!点个赞把!!!

  • 45
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值