irpas技术客

NLP自然语言处理与神经网络——01.embedding实现(理论+实践)_头发没了还会再长

大大的周 7229

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.

tokennumvector词10[w11,w12,w13…w1n] 其中n表示维度词21[w21,w22,w23…w2n]………词mm[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,所以模型比较简单,只训练一轮,没什么效果,不做测试了


1.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.本站的原创文章,会注明原创字样,如未注明都非原创,如有侵权请联系删除!;3.作者投稿可能会经我们编辑修改或补充;4.本站不提供任何储存功能只提供收集或者投稿人的网盘链接。