赞
踩
本案例主要是学习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()
可见效果不是很好,因为我们这里的神经网络是很简单的,所以达到的效果不是很好,不过之后我们可以使用循环神经网络进行改进!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。