NLP自然语言处理与神经网络——01.embedding实现(理论+实践)

RNN

1.分词

tokenization:分词,每个词语是一个token

分词方法

  1. 把句子转化为词语
    比如我爱深度学习=》{我, 爱,深度学习}
  2. 把句子转化为单个字
    比如我爱深度学习=》{我,爱,深,度,学,习}
  3. 把连续多个字作为一个词

2.N-garm表示方法

分词的第三种方法,N-garm,一组一组的词语,其中N表示能够被一起使用的词语数量
NLP自然语言处理与神经网络——01.embedding实现(理论+实践)_第1张图片
在传统的机器学习中,用N-gram往往会取得很好的效果,但是在深度学习比如RNN中往往自带N-gram的效果。

3.向量化

因为文本不能直接被模型计算,所以需要将文本转化为向量

文本转化为向量有两种表示方法:

  1. 转化为one-hot编码
  2. 转化为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]的矩阵(下图右)
NLP自然语言处理与神经网络——01.embedding实现(理论+实践)_第2张图片

3.3 word embedding API

torch.nn.Embedding(num_embeddings, embdeeing_dim)

参数:

  1. num_embeddings:词典的大小
  2. 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)
NLP自然语言处理与神经网络——01.embedding实现(理论+实践)_第3张图片
NLP自然语言处理与神经网络——01.embedding实现(理论+实践)_第4张图片

2.思路分析

首先,可以将问题定义分类问题,情感评分为1-10,10个类别(也可以当做回归问题,这里当做分类问题考虑)
大致流程如下:

  1. 准备数据集
  2. 构建模型
  3. 训练模型
  4. 模型评估

3.准备数据集

3.1实例化Dataset和准备DataLoader

有几个问题需要考虑一下:

  1. 每个batch中文本的长度如何解决
  2. 每个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):
    # 去掉
这种
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,所以模型比较简单,只训练一轮,没什么效果,不做测试了

你可能感兴趣的:(NLP,自然语言处理,神经网络,深度学习)