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))