1.数据准备

数据集来源于weibo100k,由于我比较懒,所以暂时不贴github地址了。之后开始对文本进行处理,大体思路为,依次读取weibo100k数据集的每一行,然后进行分词处理,最终统计整个文本数据集中每个词语出现的数量,然后取前topn个出现次数最高的词作为我们的字典。注:在统计过程中我用到了停用词,通俗来说就是文本数据集中许多词语比如了、吗、啊 等等这种是没有太大的实际语义的,对于后续的训练也没有太大意义,然而这类词往往出现的频率还很高,所以我们为了让最终生成的词典质量较好,会采用停用词来筛选掉一些意义不大的词。这次我采用的是hit提供的停用词。最终处理完以后会生成一个字典文件,然后我们再通过这个字典将数据集转化成one-hot编码,方便开展后续工作

 

import jieba #pip install jieba
#提供一下微博数据集的路径、停用词的路径
data_path = "sources/weibo_senti_100k.csv"
data_stop_path = "sources/hit_stopword"
data_list = open(data_path).readlines()[1:]
stops_word = open(data_stop_path).readlines()
stops_word = [line.strip() for line in stops_word] #此处strip是为了去掉换行符\n
stops_word.append(" ")
stops_word.append("\n")

voc_dict = {}
min_seq = 1
top_n = 1000 #意为取前1000个出现次数较多的词
UNK = "<UNK>"
PAD = "<PAD>"

#开始对微博数据集进行处理
for item in data_list[:]:
    label = item[0]
    content = item[2:].strip()
    seg_list = jieba.cut(content, cut_all=False) #利用cut操作对一整个句子进行分词
    seg_res = []
    for seg_item in seg_list:
        if seg_item in stops_word:
            #如果当前取到的词在停用词典中,说明这个词参考意义不大,我们跳过~
            continue
        seg_res.append(seg_item)
        if seg_item in voc_dict.keys():# 统计当前词出现的次数
            voc_dict[seg_item] = voc_dict[seg_item] + 1
        else:
            voc_dict[seg_item] = 1

    print(content)
    print(seg_res)

#对构造好的 字典按出现次数从大到小进行排序,并取前topn个
voc_list = sorted([_ for _ in voc_dict.items() if _[1] > min_seq],
                  key=lambda x:x[1],
                  reverse=True)[:top_n]
#构造字典
voc_dict = {word_count[0]: idx for idx, word_count in enumerate(voc_list)}
#添加 两个标记, UNK和 PAD
voc_dict.update({UNK:len(voc_dict), PAD:len(voc_dict) + 1})

print(voc_dict)
#进行写操作
ff = open("sources/dict.txt", "w")
for item in voc_dict.keys():
    ff.writelines("{},{}\n".format(item, voc_dict[item]))
ff.close()

 

2.数据加载

其实在讲解这部分之前,我还是想先大概写一些DataLoader、Dataset两者的作用与关系。由于笔者水平有限,理解不够深刻,仅供参考。

个人看来,DataLoader作用就是一个用来装载数据的东西,根据我们设定好的batchsize,可以将数据分成一个个batch来提供给模型进行训练,然而我们如何取到具体的数据呢?这就需要Dataset出场了,用户个人定义dataset的话最基本要实现三个方法:__init__   __getitem__  __len__ (其余还有什么我暂时不知道,但这三个是必须要的). __init__就不用说了,len这个方法用于返回元素个数,getitem则是告诉机器怎么去读数据。再编写完dataset以后,把它传给dataloader,dataloader就知道怎么去读数据喽。

torch.utils.data.DataLoader():

构建可迭代的数据装载器, 我们在训练的时候,每一个for循环,每一次iteration,就是从DataLoader中获取一个batch_size大小的数据的。

DataLoader的参数很多,但我们常用的主要有5个:

dataset: Dataset类, 决定数据从哪读取以及如何读取
bathsize: 批大小
num_works: 是否多进程读取机制
shuffle: 每个epoch是否乱序
drop_last: 当样本数不能被batchsize整除时, 是否舍弃最后一批数据

from torch.utils.data import Dataset, DataLoader
import jieba
import numpy as np

def read_dict(voc_dict_path):
    voc_dict = {}
    dict_list = open(voc_dict_path,errors='ignore').readlines()
    for item in dict_list:
        item = item.split(",")
        voc_dict[item[0]] = int(item[1].strip())
    return voc_dict

#最终load_data得到的是 <文本的标签 ,评论内容被分词后的列表>组成的列表
def load_data(data_path,data_stop_path):
    data_list = open(data_path,errors='ignore').readlines()[1:]
    stops_word = open(data_stop_path,errors='ignore').readlines()
    stops_word = [line.strip() for line in stops_word]
    stops_word.append(" ")
    stops_word.append("\n")
    voc_dict = {}
    data = []
    max_len_seq = 0 #用于后续求句子最大长度,方便对词向量进行PAD操作
    np.random.shuffle(data_list)
    for item in data_list[:10000]:
        label = item[0]
        content = item[2:].strip() #即具体微博评论内容,从2开始是因为 下标为1 处是 逗号
        seg_list = jieba.cut(content, cut_all=False)
        seg_res = []
        for seg_item in seg_list:
            if seg_item in stops_word:
                continue
            seg_res.append(seg_item)
            if seg_item in voc_dict.keys():
                voc_dict[seg_item] = voc_dict[seg_item] + 1
            else:
                voc_dict[seg_item] = 1
        if len(seg_res) > max_len_seq:
            max_len_seq = len(seg_res)
        data.append([label, seg_res])
    return data, max_len_seq


class text_ClS(Dataset):
    def __init__(self, voc_dict_path, data_path, data_stop_path, max_len_seq=None):
        self.data_path = data_path
        self.data_stop_path = data_stop_path
        self.voc_dict = read_dict(voc_dict_path)
        self.data, self.max_seq_len = \
            load_data(self.data_path, self.data_stop_path)
        if max_len_seq is not None:
            self.max_seq_len = max_len_seq
        np.random.shuffle(self.data)


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

    def __getitem__(self, item):
        data = self.data[item]
        label = int(data[0])
        word_list = data[1]
        input_idx = []
        #构建词向量
        for word in word_list:
            if word in self.voc_dict.keys():
                input_idx.append(self.voc_dict[word])
            else:
                input_idx.append(self.voc_dict["<UNK>"])
        #长度不够最大长度时,进行pad操作
        if len(input_idx) < self.max_seq_len:
            input_idx += [self.voc_dict["<PAD>"]
                          for _ in range(self.max_seq_len - len(input_idx))]
        #转化成numpy矩阵
        data = np.array(input_idx)
        return label, data
#通过定义好的dataset类,最终生成dataloader
def data_loader(dataset, config):
    return DataLoader(dataset, batch_size=config.batch_size, shuffle=config.is_shuffle)

if __name__ == "__main__":
    data_path = "sources/weibo_senti_100k.csv"
    data_stop_path = "sources/hit_stopword"
    dict_path = "sources/dict.txt"
    train_dataloader = data_loader(data_path, data_stop_path, dict_path)
    for i, batch in enumerate(train_dataloader):
        print(batch[1].size())

3.模型搭建

TextRCNN模型我自己没仔细看过原论文,因为最近没有时间,所以先贴一部分网上其他人的博客。日后推免结束有空的话我再来更正吧。

1,根据上图从左向右看,首先将词进行词向量编码,即第一栏中间的 word embedding 层;得到 e(w)。

2,接着将词向量输入到双向的 RNN(这里的 RNN cell 可以使用 lstm 或者 gru 或者最简单的)。得到 Cl 和 Cr。

可以理解为使用两个 RNN,一个从左往右扫描,一个从右往左扫描。文中说,这么做的好处是,可以抓到词汇更多的上下文信息。

3,将(1)和(2)中得出的结果拼接到一起,并输入到 tanh 激活函数,得到 y2。

4,对 y2 进行一个池化,使用一维的max pooling ,得到 y3。这样可以得到最重要的信息

5,最后是输出层,一般也是使用 softmax。

 

import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np

class Model(nn.Module):
    #初始化部分 创建模型需要的各个实例变量
    def __init__(self, config):
        super(Model, self).__init__()
        self.embeding = nn.Embedding(config.n_vocab,
                                     config.embed_size,
                                     padding_idx=config.n_vocab - 1)
        self.lstm = nn.LSTM(config.embed_size,
                            config.hidden_size,
                            config.num_layers,
                            bidirectional=True, batch_first=True,
                            dropout=config.dropout)
        self.maxpool = nn.MaxPool1d(config.pad_size)
        self.fc = nn.Linear(config.hidden_size * 2 + config.embed_size,
                            config.num_classes)
        self.softmax = nn.Softmax(dim=1)

    #前向计算部分
    def forward(self, x):
        #先将输入向量进行embeding操作
        embed = self.embeding(x) # [batchsize, seqlen, embed_size]
        #将得到的词嵌入输入到lstm单元中
        out, _ = self.lstm(embed)
        out = torch.cat((embed, out), 2)
        out = F.relu(out)
        out = out.permute(0, 2, 1)
        out = self.maxpool(out).reshape(out.size()[0], -1)
        print(out.size())
        out = self.fc(out)
        out = self.softmax(out)
        return out

if __name__ == "__main__":
    from configs import Config
    cfg = Config()
    cfg.pad_size = 640
    model_textcls = Model(config=cfg)
    input_tensor = torch.tensor([i for i in range(640)]).reshape([1, 640])
    out_tensor = model_textcls.forward(input_tensor)
    # print(out_tensor.size())
    # print(out_tensor)

 

4.模型训练

这一部分中规中矩,没有啥好说的吧。感觉就是把数据加载好,然后定义好具体的优化器、损失函数等,然后 就迭代每一个epoch进行训练就行了。具体注释我已经标明。

import torch
import torch.nn as nn
from torch import optim
from models import Model
from datasets import data_loader, text_ClS
from configs import Config

torch.backends.cudnn.enabled = False
cfg = Config()
data_path = "sources/weibo_senti_100k.csv"
data_stop_path = "sources/hit_stopword"
dict_path = "sources/dict.txt"

dataset = text_ClS(dict_path, data_path, data_stop_path)
train_dataloader = data_loader(dataset, cfg)

cfg.pad_size = dataset.max_seq_len
print(cfg.pad_size)

model_text_cls = Model(cfg)
model_text_cls.to(cfg.devices) #部署训练设备是cpu or gpu
loss_func = nn.CrossEntropyLoss()#定义交叉熵损失函数

optimizer = optim.Adam(model_text_cls.parameters(), lr=cfg.learn_rate)#定义优化器
scheduler = torch.optim.lr_scheduler.StepLR(optimizer,
                                            step_size=1,
                                            gamma=0.9) #动态调整学习率,加速模型收敛

for epoch in range(cfg.num_epochs):
    for i, batch in enumerate(train_dataloader):
        label, data = batch
        data = torch.tensor(data).to(cfg.devices).long()
        label = torch.tensor(label, dtype=torch.int64).to(cfg.devices)

        optimizer.zero_grad() #将参数梯度初始化为0
        pred = model_text_cls.forward(data) #计算预测值
        loss_val = loss_func(pred, label) #计算loss

        # print(pred)
        # print(label)
        print("epoch is {}, ite is {}, val is {}".format(epoch, i, loss_val))
        loss_val.backward() #反向传播更新梯度值
        optimizer.step() #进行梯度下降

    scheduler.step() #动态调整学习率
    if epoch % 10 == 0:
        torch.save(model_text_cls.state_dict(), "models/{}.pth".format(epoch))