基于深度学习的文本分类

基于深度学习的文本分类

学习目标

PART 1

  • 学习FastText的使用和基础原理
  • 学会使用验证集进行调参

PART 2

  • 学习Word2Vec的使用和基础原理
  • 学习使用TextCNN、TextRNN进行文本表示
  • 学习使用HAN网络结构完成文本分类

PART3

  • 了解Transformer的原理和基于预训练语言模型(Bert)的词表示
  • 学会Bert的使用,具体包括pretrain和finetune

FastText

一、fastText简介

fastText是一个快速文本分类算法,与基于神经网络的分类算法相比有两大优点:
1、fastText在保持高精度的情况下加快了训练速度和测试速度
2、fastText不需要预训练好的词向量,fastText会自己训练词向量
3、fastText两个重要的优化:Hierarchical Softmax、N-gram

二、fastText模型架构

fastText模型架构是一个三层的神经网络,输入层、隐含层和输出层,和word2vec中的CBOW很相似, 不同之处是fastText预测标签,CBOW预测的是中间词,即模型架构类似但是模型的任务不同。

img

其中x~1~,x~2~,…,x~(N−1)~,x~N~表示一个文本中的n-gram向量,每个特征是词向量的平均值。

三、层次softmax

softmax函数常在神经网络输出层充当激活函数,目的就是将输出层的值归一化到0-1区间,将神经元输出构造成概率分布。

在标准的softmax中,计算一个类别的softmax概率时,我们需要对所有类别概率做归一化,在这类别很大情况下非常耗时,因此提出了分层softmax(Hierarchical Softmax),思想是根据类别的频率构造霍夫曼树来代替标准softmax,通过分层softmax可以将复杂度从N降低到logN,下图给出分层softmax示例:

img

四、N-gram特征

基本思想是将文本内容按照子节顺序进行大小为N的窗口滑动操作,最终形成窗口为N的字节片段序列。n-gram可以根据粒度不同有不同的含义,有字粒度的n-gram和词粒度的n-gram。

优点:

1、为罕见的单词生成更好的单词向量

2、在词汇单词中,即使单词没有出现在训练语料库中,仍然可以从字符级n-gram中构造单词的词向量

3、n-gram可以让模型学习到局部单词顺序的部分信息,也可理解为上下文信息,

内存优化:

1、过滤掉出现次数少的单词
2、使用hash存储
3、由采用字粒度变化为采用词粒度

基于FastText的文本分类

准备数据

所有标签__label__均以前缀开头,这是fastText识别标签或单词的方式

1
2
3
4
#转换格式
train_df = pd.read_csv('./train_set.csv', sep='\t',nrows=15000)
train_df['label_ft'] = '__label__' + train_df['label'].astype(str)
train_df[['text','label_ft']].iloc[:-5000].to_csv('train.csv', index = None,header = None, sep='\t')

分类器

1
2
3
4
5
6
7
8
9
import fasttext
model = fasttext.train_supervised('train.csv',
lr = 1.0,
dim = 300
wordNgrams = 2,
verbose = 2,
minCount = 1,
epoch = 25,
loss = 'hs')
参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
input             # training file path (required)    
lr # learning rate [0.1]
dim # size of word vectors [100]
ws # size of the context window [5]
epoch # number of epochs [5]
minCount # minimal number of word occurences [1]
minCountLabel # minimal number of label occurences [1]
minn # min length of char ngram [0]
maxn # max length of char ngram [0]
neg # number of negatives sampled [5]
wordNgrams # max length of word ngram [1]
loss # loss function {ns, hs, softmax, ova} [softmax]
bucket # number of buckets [2000000]
thread # number of threads [number of cpus]
lrUpdateRate # change the rate of updates for the learning rate [100]
t # sampling threshold [0.0001]
label # label prefix ['__label__']
verbose # verbose [2]
pretrainedVectors # pretrained word vectors (.vec file) for supervised learning []

F1

1
2
3
val_pred = [model.predict(x)[0][0].split('__')[-1] for x in train_df.iloc[-5000:]['text']]
s = f1_score(train_df['label'].values[-5000:].astype(str),val_pred,average = 'macro')
print(s)

Task4 基于深度学习的文本分类1

fastText原理和文本分类实战

Word2Vec

word2vec的主要思路:通过单词和上下文彼此预测,对应的两个算法分别为:

  • Skip-grams (SG):预测上下文
  • Continuous Bag of Words (CBOW):预测目标单词

另外提出两种更加高效的训练方法:

  • Hierarchical softmax
  • Negative sampling

image-20200731164752291

Skip-grams

对于句子:“The dog barked at the mailman”

skip_window=2 ,则,对于 barked :将会得到 [‘The’, ‘dog’,’barked’, ‘at’, ’the’] .左侧2个词和右侧2个词。

num_skips=2, 将会得到 (’barked’,’dog’),(’barken’,’at’)==>[input,out]

image-20200731171156500

Skip-grams训练

input word和output word都会被我们进行one-hot编码。为了高效计算,它仅仅会选择矩阵中对应的向量中维度值为1的索引行:

image-20200731171539993

Word pairs and “phases

将常见的单词组合(word pairs)或者词组作为单个“words”来处理

对高频次单词进行抽样来减少训练样本的个数

​ 对于“the”这种常用高频单词,我们将会有大量的(”the“,…)这样的训练样本,而这些样本数量远远超过了我们学习“the”这个词向量所需的训练样本数。

Word2Vec通过“抽样”模式来解决这种高频词问题。它的基本思想如下:对于我们在训练原始文本中遇到的每一个单词,它们都有一定概率被我们从文本中删掉,而这个被删除的概率与单词的频率有关。

ωi 是一个单词,Z(ωi) 是 ωi 这个单词在所有语料中出现的频次,例如:如果单词“peanut”在10亿规模大小的语料中出现了1000次,那么 Z(peanut) = 1000/1000000000 = 1e - 6。

P(ωi) 代表着保留某个单词的概率:$P\left(w_{i}\right)=(\sqrt{\frac{Z\left(w_{i}\right)}{0.001}}+1) \times \frac{0.001}{Z\left(w_{i}\right)}$

负采样negative sampling

对优化目标采用“negative sampling”方法,这样每个训练样本的训练只会更新一小部分的模型权重,从而降低计算负担

负采样(是用来提高训练速度并且改善所得到词向量的质量的一种方法。不同于原本每个训练样本更新所有的权重,负采样每次让一个训练样本仅仅更新一小部分的权重,这样就会降低梯度下降过程中的计算量。

当我们用训练样本 ( input word: “fox”,output word: “quick”) 来训练我们的神经网络时,“ fox”和“quick”都是经过one-hot编码的。如果我们的词典大小为10000时,在输出层,我们期望对应“quick”单词的那个神经元结点输出1,其余9999个都应该输出0。在这里,这9999个我们期望输出为0的神经元结点所对应的单词我们称为“negative” word。

当使用负采样时,我们将随机选择一小部分的negative words(比如选5个negative words)来更新对应的权重。我们也会对我们的“positive” word进行权重更新(在我们上面的例子中,这个单词指的是”quick“)。

对于小规模数据集,选择5-20个negative words会比较好,对于大规模数据集可以仅选择2-5个negative words。

我们使用“一元模型分布(unigram distribution)”来选择“negative words”。个单词被选作negative sample的概率跟它出现的频次有关,出现频次越高的单词越容易被选作negative words。

一个单词的负采样概率越大,那么它在这个表中出现的次数就越多,它被选中的概率就越大。

每个单词被选为“negative words”的概率计算公式:$P\left(w_{i}\right)=\frac{f\left(w_{i}\right)^{3 / 4}}{\sum_{j=0}^{n}\left(f\left(w_{j}\right)^{3 / 4}\right)}$

Hierarchical Softmax

为了避免要计算所有词的softmax概率,word2vec采样了霍夫曼树来代替从隐藏层到输出softmax层的映射。

霍夫曼树的建立:

  • 根据标签(label)和频率建立霍夫曼树(label出现的频率越高,Huffman树的路径越短)
  • Huffman树中每一叶子结点代表一个label

image-20200731214743683

image-20200731214814429

使用gensim训练word2vec

1
2
from gensim.models.word2vec import Word2Vec
model = Word2Vec(sentences, workers=num_workers, size=num_features)

TextCNN

采用了100个大小为2,3,4的卷积核,最后得到的文本向量大小为100*3=300维。

image-20200731220108081

TextRNN

TextRNN将句子中每个词的词向量依次输入到双向双层LSTM,分别将两个方向最后一个有效位置的隐藏层拼接成一个向量作为文本的表示。

image-20200731220217036

基于TextCNN、TextRNN的文本表示

TextCNN

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
self.filter_sizes = [2, 3, 4]  # n-gram window
self.out_channel = 100
self.convs = nn.ModuleList([nn.Conv2d(1, self.out_channel, (filter_size, input_size), bias=True) for filter_size in self.filter_sizes])

pooled_outputs = []
for i in range(len(self.filter_sizes)):
filter_height = sent_len - self.filter_sizes[i] + 1
conv = self.convs[i](batch_embed)
hidden = F.relu(conv) # sen_num x out_channel x filter_height x 1

mp = nn.MaxPool2d((filter_height, 1)) # (filter_height, filter_width)
# sen_num x out_channel x 1 x 1 -> sen_num x out_channel
pooled = mp(hidden).reshape(sen_num, self.out_channel)

pooled_outputs.append(pooled)

TextRNN

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
input_size = config.word_dims

self.word_lstm = LSTM(
input_size=input_size,
hidden_size=config.word_hidden_size,
num_layers=config.word_num_layers,
batch_first=True,
bidirectional=True,
dropout_in=config.dropout_input,
dropout_out=config.dropout_hidden,
)

hiddens, _ = self.word_lstm(batch_embed, batch_masks) # sent_len x sen_num x hidden*2
hiddens.transpose_(1, 0) # sen_num x sent_len x hidden*2

if self.training:
hiddens = drop_sequence_sharedmask(hiddens, self.dropout_mlp)

使用HAN用于文本分类

Hierarchical Attention Network for Document Classification(HAN)基于层级注意力,在单词和句子级别分别编码并基于注意力获得文档的表示,然后经过Softmax进行分类。其中word encoder的作用是获得句子的表示.

image-20200803134803347

本章作业

  • 尝试通过Word2Vec训练词向量
  • 尝试使用TextCNN、TextRNN完成文本表示
  • 尝试使用HAN进行文本分类

Transformer

编码部分:结构完全相同,但是并不共享参数,每一个编码器都可以拆解成两部分。在对输入序列做词的向量化之后,它们首先流过一个self-attention层,该层帮助编码器在它编码单词的时候能够看到输入序列中的其他单词。self-attention的输出流向一个前向网络(Feed Forward Neural Network),每个输入位置对应的前向网络是独立互不干扰的。最后将输出传入下一个编码器。

关键特性:每个位置的词仅仅流过它自己的编码器路径。

基于预训练语言模型的词表示

传统方法生成的单词映射表的形式,即先为每个单词生成一个静态的词向量,之后这个单词的表示就被固定住了,不会跟着上下文的变化而做出改变。

基于预训练语言模型的词表示可以建模上下文信息,解决传统静态词向量不能建模“一词多义”语言现象的问题

最早提出的ELMo基于两个单向LSTM,将从左到右和从右到左两个方向的隐藏层向量表示拼接学习上下文词嵌入。而GPT用Transformer代替LSTM作为编码器,首先进行了语言模型预训练,然后在下游任务微调模型参数。但GPT由于仅使用了单向语言模型,因此难以建模上下文信息。为了解决以上问题,研究者们提出了BERT,BERT模型结构如下图所示,它是一个基于Transformer的多层Encoder,通过执行一系列预训练,进而得到深层的上下文表示。

ELMo首先进行了语言模型预训练,然后在下游任务中动态调整Word Embedding,因此最后输出的词表示能够充分表达单词在上下文中的特定语义,进而解决一词多义的问题。

GPT来自于openai,是一种生成式预训练模型。GPT 除了将ELMo中的LSTM替换为Transformer 的Encoder外,更开创了NLP界基于预训练-微调的新范式。尽管GPT采用的也是和ELMo相同的两阶段模式,但GPT在第一个阶段并没有采取ELMo中使用两个单向双层LSTM拼接的结构,而是采用基于自回归式的单向语言模型。

与GPT相同,BERT也采用了预训练-微调这一两阶段模式。在模型结构方面,BERT采用了ELMO的方式式,即使用双向语言模型代替GPT中的单向语言模型。

第一阶段的预训练过程中,BERT提出掩码语言模型,通过上下文来预测单词本身,而不是从右到左或从左到右建模,这允许模型能够自由地编码每个层中来自两个方向的信息;为了学习句子的词序关系,BERT将Transformer中的三角函数位置表示替换为可学习的参数;为了区别单句和双句输入,BERT还引入了句子类型表征。

第二阶段,与GPT相同,BERT也使用Fine-Tuning模式来微调下游任务。并且极大的减少了改造下游任务的要求,只需在BERT模型的基础上,通过额外添加Linear分类器,就可以完成下游任务。

image-20200803140309123

基于Bert的文本分类

Bert Pretrain

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class WhitespaceTokenizer(object):
"""WhitespaceTokenizer with vocab."""
def __init__(self, vocab_file):
self.vocab = load_vocab(vocab_file)
self.inv_vocab = {v: k for k, v in self.vocab.items()}

def tokenize(self, text):
split_tokens = whitespace_tokenize(text)
output_tokens = []
for token in split_tokens:
if token in self.vocab:
output_tokens.append(token)
else:
output_tokens.append("[UNK]")
return output_tokens

def convert_tokens_to_ids(self, tokens):
return convert_by_vocab(self.vocab, tokens)

def convert_ids_to_tokens(self, ids):
return convert_by_vocab(self.inv_vocab, ids)

预训练由于去除了NSP预训练任务,因此将文档处理多个最大长度为256的段,如果最后一个段的长度小于256/2则丢弃。每一个段执行按照BERT原文中执行掩码语言模型,然后处理成tfrecord格式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def create_segments_from_document(document, max_segment_length):
"""Split single document to segments according to max_segment_length."""
assert len(document) == 1
document = document[0]
document_len = len(document)

index = list(range(0, document_len, max_segment_length))
other_len = document_len % max_segment_length
if other_len > max_segment_length / 2:
index.append(document_len)

segments = []
for i in range(len(index) - 1):
segment = document[index[i]: index[i+1]]
segments.append(segment)

return segments

预训练过程中,也只执行掩码语言模型任务

1
2
3
4
5
(masked_lm_loss, masked_lm_example_loss, masked_lm_log_probs) = get_masked_lm_output(
bert_config, model.get_sequence_output(), model.get_embedding_table(),
masked_lm_positions, masked_lm_ids, masked_lm_weights)

total_loss = masked_lm_loss

为了适配句子的长度,以及减小模型的训练时间,采取了BERT-mini模型

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"hidden_size": 256,
"hidden_act": "gelu",
"initializer_range": 0.02,
"vocab_size": 5981,
"hidden_dropout_prob": 0.1,
"num_attention_heads": 4,
"type_vocab_size": 2,
"max_position_embeddings": 256,
"num_hidden_layers": 4,
"intermediate_size": 1024,
"attention_probs_dropout_prob": 0.1
}

使用Pytorch

1
2
3
4
5
6
7
8
9
10
11
12
def convert_tf_checkpoint_to_pytorch(tf_checkpoint_path, bert_config_file, pytorch_dump_path):
# Initialise PyTorch model
config = BertConfig.from_json_file(bert_config_file)
print("Building PyTorch model from configuration: {}".format(str(config)))
model = BertForPreTraining(config)

# Load weights from tf checkpoint
load_tf_weights_in_bert(model, config, tf_checkpoint_path)

# Save pytorch-model
print("Save PyTorch model to {}".format(pytorch_dump_path))
torch.save(model.state_dict(), pytorch_dump_path)

Bert Finetune

微调将最后一层的第一个token即[CLS]的隐藏向量作为句子的表示,然后输入到softmax层进行分类。

1
2
3
4
5
6
7
8
9
10
sequence_output, pooled_output = \
self.bert(input_ids=input_ids, token_type_ids=token_type_ids)

if self.pooled:
reps = pooled_output
else:
reps = sequence_output[:, 0, :] # sen_num x 256

if self.training:
reps = self.dropout(reps)

本章作业

  • 完成Bert Pretrain和Finetune的过程
  • 阅读Bert官方文档,找到相关参数进行调参

本文标题:基于深度学习的文本分类

文章作者:ZQ Liu

发布时间:2020年07月27日 - 08:20:38

最后更新:2020年08月03日 - 14:07:26

原始链接:http://yoursite.com/2020/07/27/%E5%9F%BA%E4%BA%8E%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0%E7%9A%84%E6%96%87%E6%9C%AC%E5%88%86%E7%B1%BB/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

-------------本文结束感谢您的阅读-------------

欢迎关注我的其它发布渠道