一.案例介绍
为了练习一下word embedding,现在有一个经典的数据集IMDB数据集,其中包含了5完条流行电影的评价,训练集25000条,测试集25000条,根据这些数据,通过pytorch完成模型,实现对评论情感进行预测
二.思路
首先可以把上述问题定义为分类问题,情感评分分为1-10分。十个类别,那么怎样分出着十个类别?
1.准备数据
2.构建模型
3.模型训练
4.模型评估
三.准备数据集
在前面说过准备数据集,实例化dataset,准备dataloader,下面代码每行都有解释的
python
""" dataset.py文件获取数据 """
from torch.utils.data import DataLoader, Dataset
import os
import re
# 资源路径
data_base_path = r"保存数据的路径\data\aclImdb_v1\aclImdb"
# 创建一个文本处理函数
def tokenize(text):
# 因为这是一个英文的电影评价,如果里面有下面这些字符会影响判断,所以用正则表达式替换掉
fileters = ['!', '"', '#', '$', '%', '&', '\(', '\)', '\*', '\+', ',', '-', '\.', '/', ':', ';', '<', '=', '>',
'\?', '@'
, '\[', '\\', '\]', '^', '_', '`', '\{', '\|', '\}', '~', '\t', '\n', '\x97', '\x96', '"', '"', ]
# 正则将<>里面的全部替换掉
text = re.sub('<.*?>', ' ', text, flags=re.S)
# 将上面列表中的字符用|来连接,表示只要有这个列表中的字符就替换
text = re.sub("|".join(fileters), " ", text, flags=re.S)
# 将处理好的文本用空格的形式转换成列表的形式输出
return [i.strip() for i in text.split()]
# 准备dataset
class ImdbDataset(Dataset):
# 设置一个mode参数来区分是训练数据还是测试数据
def __init__(self,mode):
super(ImdbDataset,self).__init__()
if mode=='train':
# 将训练数据的路径加上训练或者测试文本所在的文件夹,目的是区分开训练和测试的文件路径
text_path=[os.path.join(data_base_path,i) for i in ['train/neg','train/pos']]
else:
text_path=[os.path.join(data_base_path,i) for i in ['test/neg','text/pos']]
# 创建一个列表来保存所有需要训练或者测试的文件路径
self.total_file_path_list=[]
for i in text_path:
# os.listdir的作用是读取这个文件夹中所有的文件名
self.total_file_path_list.extend([os.path.join(i,j) for j in os.listdir(i)])
def __getitem__(self, idx):
# 这个函数的作用是获取数据
cur_path=self.total_file_path_list[idx] # 通过传入的索引来得到文件路径
cur_filename=os.path.basename(cur_path) # basename的作用是通过这个文件路径得到这个文件名
label=int(cur_filename.split('_')[-1].split('.')[0])-1 # 获得的文件名是464_9.txt形式的,通过这行代码得到文件的编号
text=tokenize(open(cur_path,encoding='utf-8').read().strip()) # 通过文件路径得到文件内容
# 返回文件编号和内容
return label,text
def __len__(self):
return len(self.total_file_path_list)
# 创建数据集对象
dataset=ImdbDataset(mode='train')
# 创建数据迭代器,dataset:数据集,batch_size:每次处理的样本数量,shuffle:是否打乱数据,collate_fn:下面有详细讲解
dataloader=DataLoader(dataset=dataset,batch_size=2,shuffle=True,collate_fn=lambda x:x)
# 打印看看效果
for idx,(label,text) in enumerate(dataloader):
print('idx',idx)
print('label',label)
print('text',text)
collate_fn函数是实例化dataloader的时候, 以函数形式传递给loader.
既然是collate_fn是以函数作为参数进行传递, 那么其一定有默认参数. 这个默认参数就是getitem函数返回的数据项的batch形成的列表.
先假设, datase类是如下形式:
python
class testData(Dataset):
def __init__(self):
super().__init__()
def __getitem__(self, index):
return x, y
可以看到, 假设的dataset返回两个数据项: x和y. 那么, 传入collate_fn的参数定义为data, 则其shape为(batch_size, 2,...).
知道了输入参数的形式, 就可以去定义collate_fn函数了:
python
def collate_fn(data):
for unit in data:
unit_x.append(unit[0])
unit_y.append(unit[1])
...
return {x: torch.tensor(unit_x), y: torch.tensor(unit_y)}
可以看到我对collate_fn函数的定义,最后返回的是一个字典. 这也是collate_fn函数最大的一个好处: 可以自定义取出一个batch数据的格式. 该函数的输出就是对dataloader进行遍历, 取出一个batch的数据.
3.如何给collate_fn函数传参
在collate_fn的使用过程中, 我发现只输入data有时候是非常不方便的, 需要额外的参数来传递其他变量.
这里有两个方法可以解决以上问题:
1.使用lambda函数
python
info = args.info # info是已经定义过的
loader = Dataloader(collate_fn=lambda x: collate_fn(x, info))
2.创建一个可调用函数
python
class collater():
def __init__(self, *params):
self. params = params
def __call__(self, data):
'''在这里重写collate_fn函数'''
collate_fn = collater(*params)
loader = Dataloader(collate_fn=collate_fn)
四.文本序列化
前面我们说到不会把文本直接转化为向量,而是先转化为数字,在把数字转化为向量。
这里我们可以将每个词语和数字采用键值对的形式用字典保存起来,同时把句子通过字段映射为包含数字的列表。实现文本序列化之前还需要考虑
1.如何使用字段把词语和数字进行对应
2.不同的词语出现的次数都不一样,是否需要将高频词语和低频词语进行过滤,以及总的词语数量是否需要进行限制
3.得到词典后,如何将句子转化为数字序列,如何把数字序列转化为句子
4.不同的句子长度不同,每个batch的句子如何构造成相同的长度(可以对句子进行填充,填充特殊字符)
5.对于信出现的词语在词典中没有出现怎么办
思路分析:
1.对所有的句子进行分词
2.词语存入字典,根据次数对词语进行过滤,并统计次数
3.实现文本转数字序列的方法
4.实现数字序列转文本的方法
python
""" word_sequece文件,这里是分词文件 """
import numpy as np
class word2Sequence():
UNK_TAG='UNK' # 未知字符替换
PAD_TAG='PAD' # 填充字符
UNK=0 # 未知字符用0替换
PAD=1 # 用1来填充
def __init__(self):
# 构建一个字典来保存词语所对应的数字
self.dict={
self.UNK_TAG:self.UNK,
self.PAD_TAG:self.PAD
}
# 这个是用来控制还没有创建字典的时候就执行转换的,没创建字典之前执行转换就报错
self.fited=False
def to_index(self,word): # 将词语转换成数字
assert self.fited==True # 断言
# 返回这个词语对应的数字,如果这个数字不存在就用未知代替
return self.dict.get(word,self.UNK)
def to_word(self,index): # 将数字转换成词语
assert self.fited
if index in self.inversed_dict: # 判断这个向量是不是在向量转词语的字典中,下面创建的
return self.inversed_dict[index]
# 如果在则直接返回不再这返回未知
return self.UNK_TAG
def __len__(self):
return len(self.dict)
def fit(self,sentences,min_count=1,max_count=None,max_feature=None): # 接收句子统计词频
"""
min_count:这个句子中最小的词频
max_count:这个句子中最大的词频
max_feature:这个句子中最大的词语数
"""
# 创建一个字典来保存各个词语的词频
count={}
for sentence in sentences: # 遍历句子
for a in sentence: # 遍历字符
if a not in count: # 在count中计数
count[a]=0
count[a]+=1
if min_count is not None: # 如果设置了最小词频
count={k:v for k,v in count.items() if v>=min_count} # 过滤掉词频小于最小词频的词语
if max_count is not None: # 如果设置了最大词频
count={k:v for k,v in count.items() if v<=max_count} # 过滤掉词频大于最大词频的词语
if isinstance(max_feature,int): # 如果max_feature是int类型
count=sorted(list(count.items(),key=lambda x:x[1])) # 对count通过值来排序
if max_feature is not None and len(count)>max_feature: # 当最大词语数量不为None且count中的词语数量大于max_feature时
count=count[-int(max_feature):] # 取值最大允许的词
for w,_ in count: # 遍历出词语和数字
self.dict[w]=len(self.dict) # 将结果加入到dict中
else:
for w in sorted(count.keys()): # 对count中的键排序后为列表
self.dict[w]=len(self.dict) # 将这些词语加入到dict中,这里是动态的,加一个进去len(dict)就增加一
self.fited=True
# 创建一个字典,将dict中的键值对调换,这样就可以用数字找到词语了
self.inversed_dict=dict(zip(self.dict.values(),self.dict.keys()))
def transform(self,sentence,max_len=None): # 将句子转化为数字序列,sentence:句子,max_len最大长度
assert self.fited
if max_len is not None:
# 如果max_len不为None,则构建一个列表为max_len长度的需要填充的列表
r=[self.PAD]*max_len
else: # 否则就构建一个和句子长度相同的需要填充的列表
r=[self.PAD]*len(sentence)
if max_len is not None and len(sentence)>max_len: # 当句子长度大于最大句子长度时
sentence=sentence[:max_len] # 将句子切片为最大句子长度
for index,word in enumerate(sentence): # 遍历句子,index为词语索引,word为词语
r[index]=self.to_index(word) # 在上面创建的列表中填充词语转换的数字
# 返回转化numpy1的填充好的列表
return np.array(r,dtype=np.int64)
def inverse_transform(self,indices): # 将数字序列转化为词语序列,indices为传入的数字序列
sentence=[]
for i in indices:
word=self.to_word(i) # 将数字转化为词语
sentence.append(word) # 保存词语
# 返回词语序列
return sentence
if __name__ == '__main__':
sentences=['a','b','c','d','e']
ws=word2Sequence()
# 先fit创建出字典
ws.fit(sentences)
rs=ws.transform(['a','b','c','d','i','h'])
re=ws.inverse_transform(rs)
print(ws.dict)
print(rs)
print(re)
完成了分词后,接下来就是报错现有样本中的数据字典,方便后续的使用
五.实现对IMDB数据的处理和保存
python
""" 这里是保存数据的文件main.py """
from word_sequece import WordSequence # 从刚刚写的分词中导入分词方法
from dataset import get_dataloader # 从获取数据哪里导入获取数据的方法
import pickle
from tqdm import tqdm # 进度条类
if __name__ == '__main__':
ws = WordSequence()
dl_train = get_dataloader(True) # 得到训练数据
dl_test = get_dataloader(False) # 得到测试数据
for reviews, label in tqdm(dl_train, total=len(dl_train)): # 在dataset的时候返回的是一个标题和数据,reviews是数据,label是标题
for sentence in reviews: # 遍历出句子
ws.fit(sentence) # 对每个句子分词
for reviews, label in tqdm(dl_test, total=len(dl_test)):
for sentence in reviews:
ws.fit(sentence)
print(len(ws)) # 42676
# 保存分好的数据
pickle.dump(ws, open("./models/ws.pkl", "wb"))
六.构建模型
对数据分词,序列化后就可以构建模型算法了,这里使用到上一章讲到的embedding,所以模型只有一层word embedding,数据通过这一层返回结果然后通过softmax计算损失
python
"""构建模型文件model.py"""
import torch.nn as nn
import config
import torch.nn.functional as F
class ImdbModel(nn.Module):
def __init__(self):
super(ImdbModel,self).__init__()
self.embedding = nn.Embedding(num_embeddings=len(config.ws),embedding_dim=200,padding_idx=config.ws.PAD)
# 构造全连接层要使用的算法,这里选用y=wx+b
self.fc = nn.Linear(config.max_len*200,2)
def forward(self, input):
"""
:param input:[batch_size,max_len]
:return:
"""
# 传入数据计算
input_embeded = self.embedding(input) #input embeded :[batch_size,max_len,200]
#变形
input_embeded_viewed = input_embeded.view(input_embeded.size(0),-1)
#全连接层计算后输出
out = self.fc(input_embeded_viewed)
# 返回损失
return F.log_softmax(out,dim=-1)
七.模型训练和评估
-
实例化模型,损失函数,优化器
-
遍历dataset_loader,梯度置为0,进行向前计算
-
计算损失,反向传播优化损失,更新参数
python
"""进行模型的训练文件train.py"""
from model import ImdbModel # 导入模型
from dataset import get_dataloader # 数据
from torch.optim import Adam # 优化算法
from tqdm import tqdm # 查看运行进度条的一个类
import torch.nn.functional as F # 损失函数
model = ImdbModel() # 实例化模型
optimizer = Adam(model.parameters()) # 实例化优化类
def train(epoch):
train_dataloader = get_dataloader(train=True) # 得到训练数据
bar = tqdm(train_dataloader,total=len(train_dataloader))
print(bar)
print('-'*20)
for idx,(input,target) in enumerate(bar): # 遍历训练数据
optimizer.zero_grad() # 梯度置0
output = model(input) # 模型训练
loss = F.nll_loss(output,target) # 计算误差
loss.backward() # 反向传播计算梯度
optimizer.step() # 更新参数
# 设置进度条显示形式
bar.set_description("epcoh:{} idx:{} loss:{:.6f}".format(epoch,idx,loss.item()))
if __name__ == '__main__':
for i in range(10):
train(i)