1.知道文本处理的基本方法
2.能够使用数据实现情感分类
一、案例介绍
现在我们有一个经典的数据集IMDB数据集,地址http://ai.stanford.edu/~amaas/data/sentiment/
:,这是一份包含了5万条流行电影的评论数据,其中训练集25000条,测试集25000条,数据格式如下:
下边左边为名称,其中名称包含两部分,分别是序号和情感评分(1-4位neg,5-10位pos),右边为评论内容。
二、思路分析
首先可以把上述问题定义为分类问题,情感评分为1-10,10个类别(也可以理解为回归问题,这样当做分类问题考虑)也可以分为2类(neg和pos),流程大致为:
- 准备数据集
- 构建模型
- 模型训练
- 模型评估
1.准备数据集
Dataset: 对数据集的封装,用来将数据包装为Dataset类,提供索引方式的对数据样本进行读取。
DataLoader:对Dataset进行封装,提供批量读取的迭代读取。
准备数据集的方法是实例化dataset,准备dataloader,最终处理成如下格式:
其中有两点需要注意:
- 如何完成基础Dataset的构建和Dataloader的准备
- 每个batch中文本的长度不一致的问题如何解决
- 每个batch中的文本如何转换为数字序列
代码如下(示例):
2.读入数据
代码如下(示例):
"""
完后数据集的准备
"""
from torch.utils.data import Dataset,DataLoader
import os
import re
def tokenlize(content):
"""对句子进行预处理"""
content = re.sub("<.*?>"," ",content)
filter = ['\t','\n','\x97','\x96','#','$','%','&']
content = re.sub("|".join(filter)," ",content)
tokens = [word.strip() for word in content.split()]
return tokens
class ImdbDataset(Dataset):
def __init__(self,train = True):
"""获取数据集位置"""
self.train_path = "data/aclimdb/train/"
self.test_path = "data/aclimdb/test/"
data_path = self.train_path if train else self.test_path
# 把所有的文件名称放在列表中
data_file = [os.path.join(data_path,"pos"),os.path.join(data_path,"neg")]
self.total_file_path = []
for index in data_file:
filename = os.listdir(index)
file_list = [os.path.join(index,i) for i in filename]
self.total_file_path.extend(file_list)
def __getitem__(self, index):
file_path = self.total_file_path[index]
# 获取label
label = file_path.split("\\")[-2]
label = 0 if label=="neg" else 1
# 获取内容
content = open(file_path).read()
tokens = tokenlize(content)
return tokens,label
def __len__(self):
return len(self.total_file_path)
def get_dataloader(train=True):
imdbdataset = ImdbDataset(train)
dataloader = DataLoader(imdbdataset,batch_size=2,shuffle=True)
return dataloader
if __name__ == '__main__':
for idx,(content,target) in enumerate(get_dataloader()):
print(idx)
print(content)
print(target)
break
结果如下:
很明显,其中的text内容和想象的不太相似,现在是将一个batch中的句子进行了zip处理,出现问题的原因在于Dataloader中的参数collate_fn
,collate_fn
的默认值为torch自定义的default_collate
,collate_fn
的作用就是对每个batch进行处理,而默认的default_collate
处理出错。
解决问题的思路:
方法1:考虑先把数据转化为数字序列,观察其结果是否符合要求,之前使用Dataloader并未出现类似错误。
方法2:考虑自定义一个collate_fn,观察结果。
def collate_fn(batch):
"""
:param batch: ([tokens,label],[tokens,label])
:return:
"""
result = list(zip(*batch))
return result
文本序列化(word2sequence)
在介绍word embedding时,需要先把文本转化为数字,再把数字转化为向量。
这里我么可以考虑把文本中的每个词语和其对应的数字保存在字典中,同时实现把句子通过字典映射为包含数字的列表。
实现文本序列化之前,考虑以下几点:
- 如何使用字典把词语和数字进行对应。
- 不同的词语出现的次数不相同,是否需要对高频或者低频词语进行过滤,以及总的词语数量是否需要进行限制。
- 得到词典之后,如何把句子转化为数字序列,如何把数字序列转化为句子。
- 不同句子长度不相同,每个batch的句子如何构造成相同的长度(可以对短句子进行填充,填充特殊字符)
- 对新出现的词语在词典中没有出现怎么办(可以用特殊字符处理)
思路分析:
- 对所有句子进行分词
- 将词语存入词典,根据次数对词语进行过滤,并统计次数
- 实现文本转数字序列的方法
- 实现数字序列转文本方法
"""
实现:构建词典,实现把句子转化为数字序列并将其反转
"""
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 # 字典的get方法获取value,有就返回,没有返回默认值
def build_vocab(self,min=5,max=None,max_feature=None):
"""
生成词典
:param min:最小出现的次数
:param max:最大出现的次数
:param max_feature:一共保留多少个词语
:return:
"""
# 删除count中词频小于min的word
if min is not None:
self.count = {word:value for word,value in self.count.items() if value>min}
# 删除count中词频大于max的word
if max is not None:
self.count = {word: value for word, value in self.count.items() if value < max}
# 限制保留的词语数
if max_feature is not None:
temp = sorted(self.count.items(),key = lambda x:x[-1],reverse=True)[:max_feature] # reverse=True降序
self.count = dict(temp)
for word in self.count:
self.dict[word] = len(self.dict)
# 得到一个翻转的dict字典
self.reversed_dict = dict(zip(self.dict.values(),self.dict.keys()))
def transform(self,sentence,max_len=None):
"""
把句子转化为序列
:param sentence:
:return:
"""
if max_len is not None:
if len(sentence)>max_len:
sentence = sentence[:max_len] # 裁剪
if len(sentence) < max_len:
sentence = sentence + [self.PAD_TAG]*(max_len-len(sentence)) # 填充
return [self.dict.get(word,self.UNK) for word in sentence]
def inverse_transform(self,indices):
"""
把序列转化为句子
:param indices:
:return:
"""
return [self.reversed_dict.get(index) for index in indices]
if __name__ == '__main__':
ws = word2sequence()
ws.fit(["我","是","谁"])
ws.fit(["我", "是", "我"])
ws.build_vocab(min=0)
ret = ws.transform(["我","在","河大"],max_len=10)
print(ret)
ret = ws.inverse_transform(ret)
print(ret)
word2senquence结果的保存
# _*_ coding:utf-8 _*_
from word_sequence import word2sequence
from dataset import tokenlize
import pickle
import os
from tqdm import tqdm # 进度展示
# pickle能够实现任意对象与文本之间的相互转化,也可以实现任意对象与二进制之间的相互转化。
# 也就是说,pickle可以实现 Python 对象的存储及恢复。
if __name__ == '__main__':
ws = word2sequence()
data_path = r"data/aclimdb/train/"
temp_data_file = [os.path.join(data_path, "pos"), os.path.join(data_path, "neg")]
for data_file in temp_data_file:
file_name_list = os.listdir(data_file)
for file_name in tqdm(file_name_list):
if not file_name.endswith("txt"):
continue
file_path = os.path.join(data_file,file_name)
sentence = tokenlize(open(file_path,encoding="utf-8").read())
ws.fit(sentence)
ws.build_vocab(min=10,max_feature=10000)
pickle.dump(ws,open("./model/ws.pkl","wb"))
print(len(ws))
在使用的时候直接使用pickle.load()
就可以
ws = pickle.load(open("./model/ws.pkl","rb"))
模型搭建
这里我们只练习使用word embedding,所以模型只有一层,即:
- 数据经过word embedding
- 数据通过全连接层返回结果,计算log_softmax
后面的文章中有使用LSTM网络进行训练的。
from lib import max_len,ws
import torch.nn as nn
from dataset import get_dataloader
from torch.optim import Adam
import torch.nn.functional as F
class MyModel(nn.Module):
def __init__(self):
super().__init__()
super(MyModel,self).__init__()
self.embedding = nn.Embedding(len(ws),100) # 参数为[词的数量,词的维度]
self.fc = nn.Linear(max_len*100,2) # 参数为[input_feature,output_feature]
def forward(self, input):
"""
:param input:[batch_size,max_len]
:return:
"""
x = self.embedding(input) # 进行embedding操作,形状为:[batch_size,max_len]
x = x.view([-1,max_len*100])
out = self.fc(x)
return F.softmax(out,dim=-1)
model = MyModel()
optimizer = Adam(model.parameters(),0.001)
def train(epoch):
for idx,(input,target) in enumerate(get_dataloader(train=True)):
optimizer.zero_grad()
output = model(input)
loss = F.nll_loss(output,target)
loss.backward()
optimizer.step()
print(loss.item())