RNN
1.分词
tokenization
:分词,每个词语是一个token
分词方法:
- 把句子转化为词语
比如我爱深度学习=》{我, 爱,深度学习}- 把句子转化为单个字
比如我爱深度学习=》{我,爱,深,度,学,习}- 把连续多个字作为一个词
2.N-garm表示方法
分词的第三种方法,N-garm
,一组一组的词语,其中N表示能够被一起使用的词语数量。
在传统的机器学习中,用N-gram往往会取得很好的效果,但是在深度学习比如RNN中往往自带N-gram的效果。
3.向量化
因为文本不能直接被模型计算,所以需要将文本转化为向量
文本转化为向量有两种表示方法:
- 转化为
one-hot
编码- 转化为
word embedding
3.1 one-hot编码
在one-hot
编码中,每一个token使用一个长度为n的向量表示,n为词典中单词的数量。
假设我们有一个词典里面十个词,其中第五个词为“爱”,那么“爱”这个词表示为one-hot向量时为0000100000
,也就是除第五个位置为1,其他位置都为0.
但是one-hot使用稀疏向量表示文本,占的空间较多
3.2 word embedding(词嵌入)
word embedding是深度学习中一种常用的方法。和one-hot编码不同,word-embedding使用了浮点型的稠密矩阵来表示token。根据词典的大小,向量通常使用不同的维度,例如100,256,300等,其中向量的每一个值是一个超参数,其初始值是随机生成的,之后在训练过程中通过学习获得。
如果文本有30000个词,使用one-hot编码,那么会有30000*30000的矩阵,但是使用word embedding只需要30000*维度,比如30000*300.
token | num | vector |
---|---|---|
词1 | 0 | [w11,w12,w13…w1n] 其中n表示维度 |
词2 | 1 | [w21,w22,w23…w2n] |
… | … | … |
词m | m | [wm1,wm2,wm3…wmn] 其中m表示词典的大小 |
我们会把所有的文本用向量来表示,也就是把句子用向量表示,但是,由于初始向量是随机生成的,我们要先把文本输入,但文本不能输入模型,所以要先把文本转化为数字,再把数字输入转化为向量。即token--->num--->vector
embedding的理解:
比如选择五个句子,batch_size=5,每个句子有N个词,那矩阵为[batch_size, N](下图左),我们选择的向量维度为4,词典大小为M表示为向量的矩阵为[M,4](下图上),然后将一个batch_size放在一起就是一个[batch_size, N, 4]的矩阵(下图右)
3.3 word embedding API
torch.nn.Embedding(num_embeddings, embdeeing_dim)
参数:
num_embeddings
:词典的大小embdeeing_dim
:embedding的维度
3.4形状变化
例:每个batch
中的每个句子有十个词语,经过形状为[20, 4]
的word embedding
之后,原来的句子会变成什么形状?
每个词语用形状为4的向量表示,所以最终会变成[batch_size, 10, 4]
,增加了一个维度,这个维度是embedding的dim。
文本情感分类
1.案例介绍
为了对前面的word_embedding这种常用的文本向量化进行巩固,这里完成一个文本情感分类的案例
现在有一个经典的数据集IMDB数据集,地址:点我
这是一条包含了五万条流行电影的评论数据,其中训练集25000条,测试集25000条,数据格式如下:
下图分别为名称和评论内容,名称包含两部分,分别是序号和情感评分,(1-4为neg,5-10为pos)
2.思路分析
首先,可以将问题定义分类问题,情感评分为1-10,10个类别(也可以当做回归问题,这里当做分类问题考虑)
大致流程如下:
- 准备数据集
- 构建模型
- 训练模型
- 模型评估
3.准备数据集
3.1实例化Dataset和准备DataLoader
有几个问题需要考虑一下:
- 每个batch中文本的长度如何解决
- 每个batch中的文本如何转化为数字
tips:
DataLoader
里面有一个默认的参数collate_fn
,它的默认值是torch
自定义的, collate_fn的作用是对每一个batch
进行处理,而默认的default_collate
处理会出错(会将每一个batch的第一个拿出来作为一个元组,第二个拿出来作为一个元组,以此类推,但两个batch长度不一致还会报错)。所以有一个办法就是自定义一个collate_fn
def collate_fn(batch):
"""
:param batch: ([token, label], [token, label]...)
:return:
"""
content, label = list(zip(*batch))
return content, label
Dataset.py
文件实现分词操作(tokenlize
方法),定义一个ImdbDataset
的数据集类,该类可以获取数据集的分词后的内容和label
def tokenlize(content):
# 去掉<br/>这种
content = re.sub("<.*?>", " ", content)
# 还有一些符号不需要
filters = ['\:','\.', '\t', '\n', '\x97', '\x96', '#', '$', '%', '&']
content = re.sub("|".join(filters), " ", content)
tokens = [i.strip().lower() for i in content.split()] # Python strip() 方法用于移除字符串头尾指定的字符(默认为空格或换行符)或字符序列
return tokens
class ImdbDataset(Dataset):
def __init__(self, train=True):
self.train_data_path = r"D:\python\NLP\data\aclImdb\train"
self.test_data_path = r"D:\python\NLP\data\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:
# 得到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")]
# 因为要pos 和 neg的所有文件,所以存到一个列表
self.total_file_path.extend(file_path_list)
def __getitem__(self, index):
file_path = self.total_file_path[index]
# 获取label pos=1 neg=0
label_str = file_path.split("\\")[-2]
label = 0 if label_str == "neg" else 1
#获取内容
content = open(file_path).read()
# 分词
tokens = tokenlize(content)
return tokens, label
def __len__(self):
return len(self.total_file_path)
但是get_dataloader直接使用的是本文,要实现向量化,要先将文本转化为数字。
3.2文本序列化
前面说word embedding的时候,说过要先将文本转化为数字,再把数字转化为向量,在这个过程该怎么实现?
这里可以考虑把文本中的每个词和其对应的数字使用字典保存,同时实现方法把句子通过字典映射为包含数字的列表。
问题分析:
- 如何使用字典把句子和词语对应
- 不同的词语出现次数不尽相同,是否需要对高频或低频词语进行过滤,以及总的词语数量是否需要进行限制。
- 得到词语字典后,如何转化为数字序列,如何把数字序列转化为句子。
- 不同的句子长度不同,每个batch的句子如何构造相同的长度(可以对短句子进行填充,填充特殊字符)
- 对于新出现的词语在词典中没有怎么办(可以使用特殊字符代理)
思路分析:
- 对所有句子进行分词
- 词语存入字典,对所有句子进行过滤,并统计次数
- 实现文本转数字序列的方法
- 实现数字序列转文本的方法
用一个Word2Sequence
类实现以上要求
该类将传入的单个句子保存到dict
中,句子应该是已经用tokenlize
分词后形成的列表,然后将数据集所有句子输入后得到的一个词典(fit
方法),再通过词典将每一个词对应一个数字(编号)(build_vocab
方法),这样,当给一个文本之后,将文本分词,查字典,就能将每一个句子转换数字序列了(transform
方法)。同时,给定一个数字序列,也能将序列转化为文本(inverse_transform
方法)。
word_sequence.py
# -*- coding = utf-8 -*-
# @Time : 2022/10/3 9:25
# @Author : 头发没了还会再长
# @File : word_sequence.py
# @Software : PyCharm
"""
构建词典,实现方法把句子转化为数字序列和其翻转
"""
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:[word1, word2, word3...]
"""
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: 一共保留多少个词语
"""
# 删除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_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)
# 翻转词典,方便通过数字得到词
self.reverse_dict = dict(zip(self.dict.values(), self.dict.keys()))
def transform(self, sentence, max_len=None):
"""
把句子转为序列 并且把句子转化为长度相同的序列
:param sentence: [word1, word2,..]
:param max_len: 对句子进行填充或裁剪
"""
s_len = len(sentence)
if max_len is not None:
if max_len > s_len:
sentence = sentence + [self.PAD_TAG] * (max_len - s_len) # 填充
if max_len < s_len:
sentence = sentence[:max_len] # 裁剪
return [self.dict.get(word, self.UNK) for word in sentence]
def inverse_transform(self, indices):
"""
把序列转化为句子
:param indices: [1, 2, 3, 4...]
"""
return [self.reverse_dict.get(idx) for idx in indices]
def __len__(self):
return len(self.dict)
有了以上两个文件之后,我们就能根据训练集得到对应的词典,并在dataset
里将文本转为序列后再返回。
先生成词典并保存在ws.pkl
文件,直接使用ws.pkl文件生成ws
,ws就可以直接拿来使用,是我们根据训练集得到的词典
# lib,py文件
import pickle
ws = pickle.load(open("./model/ws.pkl", "rb"))
# -*- coding = utf-8 -*-
# @Time : 2022/10/3 13:44
# @Author : 头发没了还会再长
# @File : main.py
# @Software : PyCharm
import os
import pickle
from Dataset import tokenlize
from tqdm import tqdm
from word_sequence import Word2Sequence
if __name__ == '__main__':
ws = Word2Sequence()
path = r"D:\python\NLP\data\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)
pickle.dump(ws, open("./model/ws.pkl", "wb"))
print(len(ws))
然后修改collate_fn
方法即可
def collate_fn(batch):
"""
:param batch: ([token, label], [token, label]...)
:return:
"""
content, label = list(zip(*batch))
# 使用分词 将content转化为序列
content = [ws.transform(i, max_len=20) for i in content]
return content, label
4.定义模型和训练
# -*- coding = utf-8 -*-
# @Time : 2022/10/3 14:27
# @Author : 头发没了还会再长
# @File : model.py
# @Software : PyCharm
import torch.nn as nn
from lib import ws, max_len
import torch.nn.functional as F
from torch.optim import Adam
from Dataset import get_dataLoader
class MyModel(nn.Module):
def __init__(self):
super(MyModel, self).__init__()
self.embedding = nn.Embedding(len(ws), 100)
self.fc = nn.Linear(max_len*100, 2) # 二分类
def forward(self, input):
"""
:param input: [natch_size, max_len]
:return:
"""
x = self.embedding(input) # 进行embedding操作,形状:[batch_zise, max_len, 100]
# 将三维的x转化为二维 这样才能作为输入
x = x.view([-1, max_len*100])
out = self.fc(x)
return F.log_softmax(out, dim=-1)
model = MyModel()
optimizer = Adam(model.parameters())
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())
if __name__ == '__main__':
for i in range(1):
train(i)
因为主要为了做embedding,所以模型比较简单,只训练一轮,没什么效果,不做测试了