从零开始构建基于textcnn的文本分类模型(上)

admin 2022年5月24日17:40:47评论19 views字数 7784阅读25分56秒阅读模式
伴随着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批量中文分词[EB/OL]. https://blog.csdn.net/chen10314/article/details/121996866


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

将训练集中的句子进行分词,统计词频前30000的词作为当前NLP任务的有效词。
from collections import Counterfrom 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 KeyedVectorsdef 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 = '<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 randomimport 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 pdfrom collections import defaultdictdef 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 Datasetclass 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[EB/OL]. https://pytorch.org/docs/1.7.1/data.html?highlight=dataset#torch.utils.data.Dataset


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 DataLoaderdef 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[EB/OL]. https://pytorch.org/docs/1.7.1/data.html?highlight=dataloader#torch.utils.data.DataLoader


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


————————————————

版权声明:本文为CSDN博主「沉睡的小卡比兽」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:https://blog.csdn.net/chen10314/article/details/122133221

原文始发于微信公众号(黑客茶话会):从零开始构建基于textcnn的文本分类模型(上)

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年5月24日17:40:47
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   从零开始构建基于textcnn的文本分类模型(上)https://cn-sec.com/archives/1042213.html

发表评论

匿名网友 填写信息