本案例主要是学习word embedding
这种常用的文本向量化的方法
现在我们有一个经典的数据集IMDB
数据集,地址:http://ai.stanford.edu/~amaas/data/sentiment/
,这是一份包含了5万条流行电影的评论数据,其中训练集25000条,测试集25000条。数据格式如下:
下图左边为名称,其中名称包含两部分,分别是序号和情感评分,(1-4为neg,5-10为pos),右边为评论内容
根据上述的样本,需要使用pytorch完成模型,实现对评论情感进行预测
首先可以把上述问题定义为分类问题,情感评分分为1-10,10个类别(也可以理解为回归问题,这里当做分类问题考虑)。那么根据之前的经验,我们的大致流程如下:
知道思路之后,那么我们一步步来完成上述步骤
准备数据集和之前的方法一样,实例化dataset
,准备dataloader
,最终我们的数据可以处理成如下格式:
其中有两点需要注意:
Dataset
的构建和Dataloader
的准备batch
中文本的长度不一致的问题如何解决batch
中的文本如何转化为数字序列#!/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
注意:
#自定义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
该函数是为了解决数据以元组的方式保存的问题,如果不自定义该函数,会出现以下情况:
或者数据格式错误
所以别忘记自定义collate_fn
函数
再介绍word embedding
的时候,我们说过,不会直接把文本转化为向量,而是先转化为数字,再把数字转化为向量,那么这个过程该如何实现呢?
这里我们可以考虑把文本中的每个词语和其对应的数字,使用字典保存,同时实现方法把句子通过字典映射为包含数字的列表。
现文本序列化之前,考虑以下几点:
batch
的句子如何构造成相同的长度(可以对短句子进行填充,填充特殊字符)思路分析:
代码如下:
#!/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)
改代码可以当作一个工具类,以后每次用到,根据业务需求修改直接使用即可!
完成了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))
这里的保存的最大的词语键值对为10000个,为什么输出结果为10002呢?
因为前面初始化 Word2Sequence
时,放入了“UNK
”和“PAD
”
这里我们只练习使用word embedding
,所以模型只有一层,即:
word embedding
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)
训练流程和之前相同
#!/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()
可见效果不是很好,因为我们这里的神经网络是很简单的,所以达到的效果不是很好,不过之后我们可以使用循环神经网络进行改进!