从零开始构建基于textcnn的文本分类模型(上),word2vec向量训练,预训练词向量模型加载,pytorch Dataset、collete_fn、Dataloader转换数据集并行加载

伴随着bert、transformer模型的提出,文本预训练模型应用于各项NLP任务。文本分类任务是最基础的NLP任务,本文回顾最先采用CNN用于文本分类之一的textcnn模型,意在巩固分词、词向量生成、任务词表构建、预训练向量加载、深度学习网络构建、模型训练、验证、预测等NLP模型构建的基本流程。本文中textcnn网络的构建和训练基于pytorch框架。明确以上流程,能够快速打牢基础,自然理解衔接应用后续类bert模型,用于NLP的各项任务(如文本分类、序列标注、文本生成、半指针半标注的分类预测任务等)。

        构建NLP任务的基本思路和流程如下:

(1)构建预训练的词向量模型,也可以直接使用已经训练好的预训练模型。构建当前任务下的词典和词典与预训练向量的映射。

(2)构建NLP任务数据集的转换,转换成深度学习框架所支持的格式,本文采用pytorch框架,故介绍数据集转换成pytorch框架支持的格式,用于训练、验证与预测。

(3)简介textcnn的原理,并采用pytorch构建网络结构。

(4)构建模型的训练、评估与预测流程。

由于笔者在自己学习的过程中,在第一次全流程手或者看人家的分享时不希望一篇博客写的过长,故将此任务分为上、下两篇博客。

1. 预训练词向量、词表构建

        word2vec提出后,文本中词的表征采用稠密的分布式向量表征形式。因此,需要大语料下训练词向量模型,当然也可以加载别人已经训练好的模型。

        一般来说,预训练的词表更考虑通用性,当然可以针对领域构建。然而,实际在具体NLP任务中并不会用到这么多词,会统计NLP任务数据集的特点,加载任务需要关注的词就足够了。比如在本文中,我们采用数据集中词频出现次数最多的30000个词,来代表这个NLP任务数据集中包含场景语义的有效词。注意这是一种简化的做法,实际采用哪些词根据场景和数据集特点决定。

        本文采用词向量作为输入,是一种word-based的方法,就是把句子先分词,然后把词映射成为向量,当然后续bert模型中文场景一般分每个字进行映射,只是分词模型和词向量构建不同,整体模型构建流程还是基本一样的。

1.1 通用词向量模型的训练和调用

        词向量模型的训练一般会搜集大量的文本,这些文本可以是通用文本也可以是与NLP任务相关的领域文本。然后进行分词,接着训练word2vec或者glove模型,这样训练出来的模型会有更好的泛化性。下文中简介了,基于百度提出的Lac模型进行分词,采用gensim中的word2vec构建词向量模型:

基于LAC分词与gensim的词向量训练,pandas批量中文分词_chen10314的博客-CSDN博客

1.2 通过训练集的数据洞察,生成NLP任务下的词表

        将训练集中的句子进行分词,统计词频前30000的词作为当前NLP任务的有效词。

from collections import Counter
from tqdm import tqdm

def get_cutwords_list(line):
    sen = lac.run(line) #采用lac进行分词,返回list
    return sen

def get_vocab(config):
    model_train = pd.read_csv(config['train_file_path'])
    model_train['cut'] = ''
    cut_df(model_train[['sentence']], get_cutwords_list)
    
    token_counter = Counter()
    
    for i in tqdm(model_train['cut'], total=len(model_train['cut']), desc='Counting token'):
        token_counter.update(i)

    vocab = set(token for token, _ in token_counter.most_common(config['vocab_size']))

    return vocab

vocab = get_vocab(config)

1.3 构建任务词表下的预训练向量映射表

        在我们统计高频的3W个词中,找寻预训练词表中有的词,构建分词后的token与预训练词表embedding之间的映射。

        在token2embedding的映射过程中,有几个特别的字符需要处理:

(1)填充位: pad 深度学习训练的输入是固定的,但是句子长度是不固定的,因此一般会预设一个句子的最大长度,如果句子没有满最大长度,没满的位置由pad填充。

(2)未登录词:unk 分词的token结果,不在所构建预训练词表中,一般会专门设置一个token处理

(3)句子开头和结尾:bos、eos 句子的开头结尾,bert之前手工构建模型的时代也不一定需要,为了和后续bert模型中cls、sep形成对应,此处先提及一下。

from gensim.models import KeyedVectors
def get_embedding(vocab):
    token2embedding ={}
    
    word2vec = KeyedVectors.load('w2v/model/allw2v.model')

    for token in vocab:
        if token in word2vec.wv.key_to_index.keys():
            token2embedding[token] = word2vec.wv[token]

    meta_info = word2vec.wv[0].shape[0]
    token2id = {token: idx for idx, token in enumerate(token2embedding.keys(), 4)}
    id2embedding = {token2id[token]: embedding for token, embedding in token2embedding.items()}

    PAD, UNK, BOS, EOS = '', '', '', ''

    token2id[PAD] = 0
    token2id[UNK] = 1
    token2id[BOS] = 2
    token2id[EOS] = 3

    id2embedding[0] = [.0] * int(meta_info)
    id2embedding[1] = [.0] * int(meta_info)
    id2embedding[2] = np.random.random(int(meta_info)).tolist()
    id2embedding[3] = np.random.random(int(meta_info)).tolist()

    emb_mat = [id2embedding[idx] for idx in range(len(id2embedding))]

    return torch.tensor(emb_mat, dtype=torch.float), token2id, len(vocab)+4

        此处将token2embedding,拆分成了token2id、id2embedding两步。句子分词成token后先转换成id,将unk的情况先处理了。然后,id2embedding就相当于一个预训练的词表,转换成tensor,通过nn.Embedding.from_pretrained(emb_mat, freeze=True)加载入模型中。

1.4 训练的基本配置

        我们将一些文件路径、词表路径、随机数等变量写成在配置config里面,便于修改。

config = {
    'train_file_path': 'dataset/train.csv',
    'test_file_path': 'dataset/test.csv',
    'train_val_ratio': 0.1,  # 10%用作验证集
    'vocab_size': 30000,   # 词典 3W
    'batch_size': 64,      # batch 大小 64
    'num_epochs': 2,      # 2次迭代
    'learning_rate': 1e-3, # 学习率
    'logging_step': 300,   # 每跑300个batch记录一次
    'seed': 2021           # 随机种子
}

config['device'] = 'cuda' if torch.cuda.is_available() else 'cpu' # cpu&gpu

import random
import numpy as np

def seed_everything(seed):
    random.seed(seed)
    np.random.seed(seed)
    torch.manual_seed(seed)
    torch.cuda.manual_seed_all(seed)
    return seed

seed_everything(config['seed'])

2、数据集转换

        构建好token2id、id2embedding的操作后,需要将训练集(训练集)、测试集转换成id,并通过pytorch内置的数据集加载方法,批量加载训练数据用于模型训练。

2.1 训练集与测试集的批处理转换

           对数据集进行分词,并映射为id,分词依旧采用百度lac分词器。

def tokenizer(sent, token2id):
    ids = [token2id.get(token, 1) for token in lac.run(sent)]
    return ids

        在训练模式中,需要划分训练集与验证集,并转换成id,验证集用于在训练过程中评估模型的训练情况,看看模型是否拟合数据,是否会产生过拟合的情况等。由于上文已经尝试了pandas批处理方式,此处采用逐行分词,练练基本操作。其中,文件row[1]指数据集标签的label id列,row[-1]指带分词的句子列。

import pandas as pd
from collections import defaultdict
def read_data(config, token2id, mode='train'):
    data_df = pd.read_csv(config[f'{mode}_file_path'], sep=',')
    if mode == 'train':
        X_train, y_train = defaultdict(list), []
        X_val, y_val = defaultdict(list), []
        num_val = int(config['train_val_ratio'] * len(data_df))
    
    else:
        X_test, y_test = defaultdict(list), []

    for i, row in tqdm(data_df.iterrows(), desc=f'Preprocesing {mode} data', total=len(data_df)):
        label=row[1] if mode == 'train' else 0
        sentence = row[-1]
        inputs = tokenizer(sentence, token2id)
        if mode == 'train':
            if i < num_val:
                X_val['input_ids'].append(inputs)
                y_val.append(label)
            else:
                X_train['input_ids'].append(inputs)
                y_train.append(label)
        
        else:
            X_test['input_ids'].append(inputs)
            y_test.append(label)

    if mode == 'train':
        label2id = {label: i for i, label in enumerate(np.unique(y_train))}
        id2label = {i: label for label, i in label2id.items()}

        y_train = torch.tensor([label2id[label] for label in y_train], dtype=torch.long)
        y_val = torch.tensor([label2id[label] for label in y_val], dtype=torch.long)

        return X_train, y_train, X_val, y_val, label2id, id2label
    
    else:
        y_test = torch.tensor(y_test, dtype=torch.long)
        return X_test, y_test
X_train, y_train, X_val, y_val, label2id, id2label = read_data(config, token2id, mode='train')
X_test, y_test = read_data(config, token2id, mode='test')

2.2 构建当前NLP任务下的Dataset类

        将转换好id的数据集,通过创建/继承 Dataset类提供数据集的封装, 再使用 Dataloader 实现数据并行加载,创建/继承 Dataset 必须实现python风格函数__len__()方法 返回整个数据集的长度,以及__getitem__(self, index)函数从而支持数据集索引

from torch.utils.data import Dataset
class TNEWSDataset(Dataset):
    def __init__(self, X, y):
        self.x = X
        self.y = y

    def __getitem__(self, idx):
        return {
            'input_ids': self.x['input_ids'][idx],
            'label': self.y[idx]
        }
    
    def __len__(self):
        return self.y.size(0)

 pytorch Dataset类的文档解释,可见一下链接:

torch.utils.data — PyTorch 1.7.1 documentation

2.3 collete_fn将数据集转换为tensor

        在收集Dataset的example后,需要将所有的句子id统一成相同长度的tensor并进行合并,此处将每句话的长度统一成了句子集合中长度最长的句子长度。pad操作隐含在torch.zeros的初始化操作中了。

def my_collate_fn(examples):
    input_ids_list =[]
    labels = []
    for example in examples:
        input_ids_list.append(example['input_ids'])
        labels.append(example['label'])
    
    # 1.找到 input_ids_list 中最长的句子
    max_length = max(len(input_ids) for input_ids in input_ids_list)

    # 2. 定义一个Tensor
    input_ids_tensor = torch.zeros((len(labels), max_length), dtype=torch.long)

    for i, input_ids in enumerate(input_ids_list):
        # 3.得到当前句子长度
        seq_len = len(input_ids)
        input_ids_tensor[i, :seq_len] = torch.tensor(input_ids, dtype=torch.long)

    return {
        'input_ids': input_ids_tensor,
        'label': torch.tensor(labels, dtype=torch.long)
    }

2.4 使用dataloader读入数据

        数据集映射为id、数据集类的封装、数据集转tensor合并以及DataLoader并行加载这些操作都封装在一起。

from torch.utils.data import DataLoader
def build_dataloader(config, vocab):
    X_train, y_train, X_val, y_val, label2id, id2label = read_data(config, token2id, mode='train')
    X_test, y_test = read_data(config, token2id, mode='test')

    train_dataset = TNEWSDataset(X_train, y_train)
    val_dataset = TNEWSDataset(X_val, y_val)
    test_dataset = TNEWSDataset(X_test, y_test)
    
    train_dataloader = DataLoader(dataset=train_dataset, batch_size=config['batch_size'], num_workers=4, shuffle=True, collate_fn=my_collate_fn)
    val_dataloader = DataLoader(dataset=val_dataset, batch_size=config['batch_size'], num_workers=4, shuffle=False, collate_fn=my_collate_fn)
    test_dataloader = DataLoader(dataset=test_dataset, batch_size=config['batch_size'], num_workers=4, shuffle=False, collate_fn=my_collate_fn)

    return id2label, train_dataloader, val_dataloader, test_dataloader
 id2label, train_dataloader, val_dataloader, test_dataloader = build_dataloader(config, vocab)

 pytorch DataLoader类的文档解释,可见一下链接:

torch.utils.data — PyTorch 1.7.1 documentation

至此模型的输入部分已经全部处理好,textcnn原理,基于pytorch构建textcnn模型,构建训练、验证和测试的baseline可见:从零开始构建基于textcnn的文本分类模型(下)

你可能感兴趣的:(nlp基础应用,分类,人工智能,pytorch,nlp,python)