基于feature based的BERT中文标题分类实战
在上篇分享中
https://zhuanlan.zhihu.com/p/72448986zhuanlan.zhihu.com我们实现了基于pytorch pretrained-bert
提供的pretrained-bert进行fine tuning的中文标题分类,事实上在pytorch pretrained-bert
中对于下游NLP任务的应用提供了比较丰富的封装和实现,如针对文本分类的BertForSequenceClassification
,针对字符分类的BertForTokenClassification
,以及判断句子前后关系的BertForNextSentencePrediction
。
事实上,上面提到的这些类都是在原先的BertModel
基础上对于各种应用的适配,如在pool
层上加一个输出大小为1的dense
层做二分类便可以用于BertForSequenceClassification
和BertForNextSentencePrediction
,而在pool
层上加一个输出大小为词典数的dense
层便可用于BertForTokenClassification
,因此具体代码上大同小异,都非常方便。如果想要观察各个模型具体的网络结构上的差异,可以通过https://zhuanlan.zhihu.com/p/71207696 中提到的可视化工具进行网络的可视化,下面是上文中的标题分类模型的网络结构图。
从上图可以看出,因为总共有28个类别,因此会有769 × 28
的全连接层,总之通过可视化可以帮助我们更加直观的了解各个网络之间的差异。
在上篇分享中我们侧重的是fine tuning based,本文主要侧重的是feature based,即将bert作为文本语义特征的提取/生成工具,通过为样本生成低维稠密特征而快速适用于多种机器学习、深度学习模型,该种方式或许无法完全发挥bert的表征学习能力,但是为后续模型的选择和设计提供了很大的便捷性及自由度。本文中使用的数据上一篇文章中进行了仔细的介绍,因此有需要的话可以参阅本专栏前一篇文章。
载入库
import csv
import os
import sys
import pickle
import pandas as pd
import numpy as np
from concurrent.futures import ThreadPoolExecutor
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.nn import Conv1d,BatchNorm1d,MaxPool1d,ReLU,Dropout
from torch.optim import Adam
import pickle
from sklearn.preprocessing import LabelEncoder
from torch.optim import optimizer
from torch.utils.data import DataLoader, RandomSampler, SequentialSampler, TensorDataset
from torch.nn import CrossEntropyLoss,BCEWithLogitsLoss
from tqdm import tqdm_notebook, trange
from pytorch_pretrained_bert import BertTokenizer, BertModel, BertForMaskedLM, BertForSequenceClassification
from pytorch_pretrained_bert.optimization import BertAdam, WarmupLinearSchedule
import matplotlib.pyplot as plt
%matplotlib inline
数据预处理
class DataPrecessForSingleSentence(object):
"""
对文本进行处理
"""
def __init__(self, bert_tokenizer, max_workers=10):
"""
bert_tokenizer :分词器
dataset :包含列名为'text'与'label'的pandas dataframe
"""
self.bert_tokenizer = bert_tokenizer
# 创建多线程池
self.pool = ThreadPoolExecutor(max_workers=max_workers)
# 获取文本与标签
def get_input(self, dataset, max_seq_len=30):
"""
通过多线程(因为notebook中多进程使用存在一些问题)的方式对输入文本进行分词、ID化、截断、填充等流程得到最终的可用于模型输入的序列。
入参:
dataset : pandas的dataframe格式,包含两列,第一列为文本,第二列为标签。标签取值为{0,1},其中0表示负样本,1代表正样本。
max_seq_len : 目标序列长度,该值需要预先对文本长度进行分别得到,可以设置为小于等于512(BERT的最长文本序列长度为512)的整数。
出参:
seq : 在入参seq的头尾分别拼接了'CLS'与'SEP'符号,如果长度仍小于max_seq_len,则使用0在尾部进行了填充。
seq_mask : 只包含0、1且长度等于seq的序列,用于表征seq中的符号是否是有意义的,如果seq序列对应位上为填充符号,
那么取值为1,否则为0。
seq_segment : shape等于seq,因为是单句,所以取值都为0。
labels : 标签取值为{0,1},其中0表示负样本,1代表正样本。
"""
sentences = dataset.iloc[:, 0].tolist()
labels = dataset.iloc[:, 1].tolist()
# 切词
tokens_seq = list(
self.pool.map(self.bert_tokenizer.tokenize, sentences))
# 获取定长序列及其mask
result = list(
self.pool.map(self.trunate_and_pad, tokens_seq,
[max_seq_len] * len(tokens_seq)))
seqs = [i[0] for i in result]
seq_masks = [i[1] for i in result]
seq_segments = [i[2] for i in result]
return seqs, seq_masks, seq_segments, labels
def trunate_and_pad(self, seq, m