【学习笔记】零基础入门NLP - 新闻文本分类实战

2 篇文章 0 订阅

赛题理解

  首先要理解赛题的背景及描述——赛题以新闻数据为赛题数据,数据集报名后可见并可下载。赛题数据为新闻文本,并按照字符级别进行匿名处理。整合划分出14个候选分类类别:财经、彩票、房产、股票、家居、教育、科技、社会、时尚、时政、体育、星座、游戏、娱乐的文本数据。通过描述,我们可以知道这道题是要通过给定的一段文本来判断是属于哪一种类别的

  赛题数据由以下几个部分构成:训练集20w条样本,测试集A包括5w条样本,测试集B包括5w条样本。为了预防选手人工标注测试集的情况,我们将比赛数据的文本按照字符级别进行了匿名处理。处理后的赛题训练数据如下:

labeltext
657 44 66 56 2 3 3 37 5 41 9 57 44 47 45 33 13 63 58 31 17 47 0 1 1 69 26 60 62 15 21 12 49 18 38 20 50 23 57 44 45 33 25 28 47 22 52 35 30 14 24 69 54 7 48 19 11 51 16 43 26 34 53 27 64 8 4 42 36 46 65 69 29 39 15 37 57 44 45 33 69 54 7 25 40 35 30 66 56 47 55 69 61 10 60 42 36 46 65 37 5 41 32 67 6 59 47 0 1 1 68

标签的对应关系为:

{'科技': 0, '股票': 1, '体育': 2, '娱乐': 3, '时政': 4, '社会': 5, '教育': 6, '财经': 7, '家居': 8, '游戏': 9, '房产': 10, '时尚': 11, '彩票': 12, '星座': 13}

在接触赛题初时,对于这次比赛的评判标准要很清楚!!!
计 算 公 式 : 2 × p r e c i s i o n × r e c a l l p r e c i s i o n + r e c a l l 计算公式:2\times \frac{precision \times recall}{precision + recall} 2×precision+recallprecision×recall
上述公式也就是F1_score,是统计学中用来衡量二分类模型精确度的一种指标。它同时兼顾了分类模型的精确(precision)召回率(recall)。F1分数可以看作是模型精确率和召回率的一种调和平均,它的最大值是1,最小值是0。这里提到了精确率和召回率,要想理解这两个还得先知道TP、FP、TN、FN这四个概念

  • TP:True Positive :做出Positive的判定,而且判定是正确的
  • FP:False Positive :做出Positive的判定,而且判定是错误的
  • TN:True Negative :做出Negative的判定,而且判定是正确的
  • FN:False Negative:错误的Negative判定,而且判断是错误的

p r e c i s i o n = T P T P + F P r e c a l l = T P T P + F N precision = \frac{TP}{TP+FP} \\ recall = \frac{TP}{TP+FN} precision=TP+FPTPrecall=TP+FNTP

数据读取

出题者提到数据集文件用'\t'分割,因此这样读取数据集。

import pandas as pd
train_df = pd.read_csv('../input/train_set.csv', sep='\t')

img

上面图中显示的数据,进行了脱敏处理的。

数据分析

我们希望通过训练集的数据,通过数据分析得出下列的结论:

  • 赛题数据中,新闻文本的长度是多少?
  • 赛题数据的类别分布是怎么样的,哪些类别比较多?
  • 赛题数据中,字符分布是怎么样的?
句子长度分析
%pylab inline
train_df['text_len'] = train_df['text'].apply(lambda x: len(x.split(' ')))
print(train_df['text_len'].describe())

输出结果:

Populating the interactive namespace from numpy and matplotlib
count    200000.000000
mean        907.207110
std         996.029036
min           2.000000
25%         374.000000
50%         676.000000
75%        1131.000000
max       57921.000000
Name: text_len, dtype: float64

通过上面的结果,可以看出200000个句子,每个句子平局由907个字符组成,最短的由两个字符组成,最长的达到57921个字符。

下图将句子长度绘制了直方图,可见大部分句子的长度都几种在2000以内。

_ = plt.hist(train_df['text_len'], bins=200)
plt.xlabel('Text char count')
plt.title("Histogram of char count")
task2_char_hist
新闻类别分布
train_df['label'].value_counts().plot(kind='bar')
plt.title('News class count')
plt.xlabel("category")
task2_class_hist

在数据集中标签的对应的关系如下:{‘科技’: 0, ‘股票’: 1, ‘体育’: 2, ‘娱乐’: 3, ‘时政’: 4, ‘社会’: 5, ‘教育’: 6, ‘财经’: 7, ‘家居’: 8, ‘游戏’: 9, ‘房产’: 10, ‘时尚’: 11, ‘彩票’: 12, ‘星座’: 13}

从统计结果可以看出,赛题的数据集类别分布存在较为不均匀的情况。在训练集中科技类新闻最多,其次是股票类新闻,最少的新闻是星座新闻。

字符分布统计
from collections import Counter
all_lines = ' '.join(list(train_df['text']))
word_count = Counter(all_lines.split(" "))
word_count = sorted(word_count.items(), key=lambda d:d[1], reverse = True)

print(len(word_count))
# 6869

print(word_count[0])
# ('3750', 7482224)

print(word_count[-1])
# ('3133', 1)

从统计结果中可以看出,在训练集中总共包括6869个字,其中编号3750的字出现的次数最多,编号3133的字出现的次数最少。

这里还可以根据字在每个句子的出现情况,反推出标点符号。下面代码统计了不同字符在句子中出现的次数,其中字符3750,字符900和字符648在20w新闻的覆盖率接近99%,很有可能是标点符号。

train_df['text_unique'] = train_df['text'].apply(lambda x: ' '.join(list(set(x.split(' ')))))
all_lines = ' '.join(list(train_df['text_unique']))
word_count = Counter(all_lines.split(" "))
word_count = sorted(word_count.items(), key=lambda d:int(d[1]), reverse = True)

print(word_count[0])
# ('3750', 197997)

print(word_count[1])
# ('900', 197653)

print(word_count[2])
# ('648', 191975)

通过上述分析的总结:

  1. 赛题中每个新闻包含的字符个数平均为900个,还有一些新闻字符较长;
  2. 赛题中新闻类别分布不均匀,科技类新闻样本量接近4w,星座类新闻样本量不到1k;
  3. 赛题总共包括7000左右个字符;

因此得出的结论:

  1. 每个新闻平均字符个数较多,可能需要截断;

  2. 由于类别不均衡,会严重影响模型的精度;

基于机器学习的文本分类

  在自然语言领域,文本的长度是不确定的。文本表示成计算机能够运算的数字或向量的方法一般为词嵌入(Word Embedding)方法。词嵌入将不定长的文本转换到定长的空间去,是文本分类的第一步。

One-hot

这里的One-hot与数据挖掘任务中的操作是一致的,即将每一个单词使用一个离散的向量表示。具体将每个字/词编码一个索引,然后根据索引进行赋值。

One-hot表示方法的例子如下:假设有两个句子如下

句子1:我 爱 北 京 天 安 门
句子2:我 喜 欢 上 海

首先对所有的句子的字进行编号:

{
	'我': 1, '爱': 2, '北': 3, '京': 4, '天': 5,
  '安': 6, '门': 7, '喜': 8, '欢': 9, '上': 10, '海': 11
}

在这里共包括11个字,因此每个字可以转换为一个11维度稀疏向量:

我:[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
爱:[0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]
...
海:[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]
Bag of Words

Bag of Words(词袋表示),也称为Count Vectors,每个文档的字/词可以使用其出现次数来进行表示。

直接统计每个字出现的次数,并进行赋值:

句子1:我 爱 北 京 天 安 门
转换为 [1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0]

句子2:我 喜 欢 上 海
转换为 [1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1]

再看两句英语的例子:

句子1:John likes to watch movies. Mary likes too.
转换为 [1, 2, 1, 1, 1, 0, 0, 0, 1, 1]

句子2:John also likes to watch football games.
转换为 [1, 1, 1, 1, 0, 1, 1, 1, 0, 0]

不过这种表示方法有个缺点,在构造文档向量的过程中可以看到,我们并没有表达单词在原来句子中出现的顺序。

下面来看一个用sklearn实现的例子:

安装sklearn库一句话就搞定了:

pip install scikit-learn
from sklearn.feature_extraction.text import CountVectorizer
corpus = [
    'This is the first document.',
    'This document is the second document.',
    'And this is the third one.',
    'Is this the first document?',
]
vectorizer = CountVectorizer()
vectorizer.fit_transform(corpus).toarray()

# 输出结果
# array([[0, 1, 1, 1, 0, 0, 1, 0, 1],
#        [0, 2, 0, 1, 0, 1, 1, 0, 1],
#        [1, 0, 0, 1, 1, 0, 1, 1, 1],
#        [0, 1, 1, 1, 0, 0, 1, 0, 1]], dtype=int64)
CountVectorizer(*, input='content', encoding='utf-8', decode_error='strict', strip_accents=None, lowercase=True, preprocessor=None, tokenizer=None, stop_words=None, token_pattern='(?u)\b\w\w+\b', ngram_range=(1, 1), analyzer='word', max_df=1.0, min_df=1, max_features=None, vocabulary=None, binary=False, dtype=<class 'numpy.int64'>)

其实上面的代码也是sklearn中一贯的套路了,先创建一个类,然后把数据扔进fit_transform(),就得到结果了,只不过这里还多了一步,把结果转成了ndarray。

TF-IDF

F-IDF 分数由两部分组成:第一部分是词语频率(Term Frequency),第二部分是逆文档频率(Inverse Document Frequency)。其中计算语料库中文档总数除以含有该词语的文档数量,然后再取对数就是逆文档频率。
T F = 在 某 一 类 中 词 条 w 出 现 的 次 数 该 类 中 所 有 的 词 条 数 目 I D F = log ⁡ 语 料 库 的 文 档 总 数 包 含 词 条 w 的 文 档 数 + 1 T F − I D F = T F × I D F TF=\frac{在某一类中词条w出现的次数}{该类中所有的词条数目} \\ IDF=\log{\frac{语料库的文档总数}{包含词条w的文档数+1}}\\ TF-IDF=TF\times IDF TF=wIDF=logw+1TFIDF=TF×IDF
分母之所以要加1,是为了避免分母为0。

某一特定文件内的高词语频率,以及该词语在整个文件集合中的低文件频率,可以产生出高权重的TF-IDF。因此,TF-IDF倾向于过滤掉常见的词语,保留重要的词语。

from sklearn.feature_extraction.text import TfidfVectorizer
corpus = [
    'This is the first document.',
    'This document is the second document.',
    'And this is the third one.',
    'Is this the first document?',
]
vectorizer = TfidfVectorizer()
vectorizer.fit_transform(corpus).toarray()

# 输出结果 
# array([[0.        , 0.46979139, 0.58028582, 0.38408524, 0.        ,
#         0.        , 0.38408524, 0.        , 0.38408524],
#        [0.        , 0.6876236 , 0.        , 0.28108867, 0.        ,
#         0.53864762, 0.28108867, 0.        , 0.28108867],
#        [0.51184851, 0.        , 0.        , 0.26710379, 0.51184851,
#         0.        , 0.26710379, 0.51184851, 0.26710379],
#        [0.        , 0.46979139, 0.58028582, 0.38408524, 0.        ,
#         0.        , 0.38408524, 0.        , 0.38408524]])

跟CountVectors的代码几乎一样就是CountVectorizer()变成了TfidfVectorizer()。

RidgeClassifier(岭回归分类器)

  简单介绍下,因为后面有用到分类。简单理解就跟LogisticRegression差不太多,把他们看成相似的作用就行了,细节就不过多说。

  这个分类器有时被称为带有线性核的最小二乘支持向量机。该分类器首先将二进制目标转换为{- 1,1},然后将该问题视为回归任务,优化与上面相同的目标。预测类对应于回归预测的符号,对于多类分类,将问题视为多输出回归,预测类对应的输出值最大。该分类器使用(惩罚)最小二乘损失来适应分类模型,而不是使用更传统的逻辑或铰链损失(最大边界损失),在实践中,所有这些模型在准确性或精度/召回率方面都可能导致类似的交叉验证分数,而RidgeClassifier使用的惩罚最小二乘损失允许对具有不同计算性能概要的数值求解器进行各自不同的选择。

接下来我们将对比不同文本表示算法的精度,通过代码本地构建验证集计算F1得分。使用sklearn库中的函数实现。

Count Vectors + RidgeClassifier

import pandas as pd

from sklearn.feature_extraction.text import CountVectorizer
from sklearn.linear_model import RidgeClassifier
from sklearn.metrics import f1_score

train_df = pd.read_csv('train_set.csv', sep='\t', nrows=15000)

vectorizer = CountVectorizer(max_features=3000)
train_test = vectorizer.fit_transform(train_df['text'])

clf = RidgeClassifier()
clf.fit(train_test[:10000], train_df['label'].values[:10000])

val_pred = clf.predict(train_test[10000:])
print(f1_score(train_df['label'].values[10000:], val_pred, average='macro'))
# 0.74

TF-IDF + RidgeClassifier

import pandas as pd

from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import RidgeClassifier
from sklearn.metrics import f1_score

train_df = pd.read_csv('train_set.csv', sep='\t', nrows=15000)

tfidf = TfidfVectorizer(ngram_range=(1,3), max_features=3000)
train_test = tfidf.fit_transform(train_df['text'])

clf = RidgeClassifier()
clf.fit(train_test[:10000], train_df['label'].values[:10000])

val_pred = clf.predict(train_test[10000:])
print(f1_score(train_df['label'].values[10000:], val_pred, average='macro'))
# 0.87

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

FastText

FastText是一种典型的深度学习词向量的表示方法,它非常简单通过Embedding层将单词映射到稠密空间,然后将句子中所有的单词在Embedding空间中进行平均,进而完成分类操作。

优点:使用Embedding能提高计算的效率。

如下图所示,FastText是一个三层的神经网络,输入层、隐藏层和输出层。

fast_text

下图是使用keras实现的FastText网络结构:

keras_fasttext

FastText安装:

# 1、pip安装
pip install fasttext

# 2、源码安装
git clone https://github.com/facebookresearch/fastText.git
cd fastText
python setup.py install

python代码实现:

import pandas as pd
from sklearn.metrics import f1_score

# 转换为FastText需要的格式
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')

import fasttext
model = fastfasttext.train_supervised('train.csv', lr=1.0, wordNgrams=2, 
                                  verbose=2, minCount=1, epoch=25, loss="hs")

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

fastfasttext.train_supervised中的参数含义:

    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] char ngram的最小长度
    maxn              # max length of char ngram [0] char ngram的最大长度
    neg               # number of negatives sampled [5] 负样本的个数
    wordNgrams        # max length of word ngram [1] ngram的最大长度
    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 []

下面稍微调整了下参数,学习率取[1.1, 1.2, 1.3, 1.4, ,1.5],wordNgrams取[1,2,3]。

import pandas as pd
from sklearn.metrics import f1_score
import fasttext

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')
for lr in [1.1, 1.2, 1.3, 1.4, 1.5]:
    print("Current lr is: ", lr)
    for wordNGram in [1, 2, 3]:
        print("Current wordNgrams is:", wordNGram)
        model = fasttext.train_supervised('train.csv', lr=lr, wordNgrams=wordNGram, 
                              verbose=2, minCount=1, epoch=25, loss="hs")
        val_pred = [model.predict(x)[0][0].split('__')[-1] for x in train_df.iloc[-5000:]['text']]
        print(f1_score(train_df['label'].values[-5000:].astype(str), val_pred, average='macro'))

通过简单的调参,发现wordNgrams=3,lr=(1.4,1.5)效果比较好。

基于深度学习的文本分类2

Word2Vec

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

  • Skip-grams (SG):通过给定Input word来预测上下文

  • Continuous Bag of Words (CBOW):通过给定上下文来预测目标单词

1、Skip-grams原理和网络结构
skip_grams

Word2Vec模型实际上分为了两个部分,第一部分为建立模型,第二部分是通过模型获取嵌入词向量。

Word2Vec的整个建模过程实际上与自编码器(auto-encoder)的思想很相似,即先基于训练数据构建一个神经网络,当这个模型训练好以后,我们并不会用这个训练好的模型处理新的任务,我们真正需要的是这个模型通过训练数据所学得的参数,例如隐层的权重矩阵——后面我们将会看到这些权重在Word2Vec中实际上就是我们试图去学习的“word vectors”。

Skip-grams过程

假如我们有一个句子“The dog barked at the mailman”。

  1. 首先我们选句子中间的一个词作为我们的输入词,例如我们选取“dog”作为input word;

  2. 有了input word以后,我们再定义一个叫做skip_window的参数,它代表着我们从当前input word的一侧(左边或右边)选取词的数量。如果我们设置skip_window=2,那么我们最终获得窗口中的词(包括input word在内)就是[‘The’, ‘dog’,‘barked’, ‘at’]。skip_window=2代表着选取左input word左侧2个词和右侧2个词进入我们的窗口,所以整个窗口大小span=2x2=4。另一个参数叫num_skips,它代表着我们从整个窗口中选取多少个不同的词作为我们的output word,当skip_window=2,num_skips=2时,我们将会得到两组 (input word, output word) 形式的训练数据,即 (‘dog’, ‘barked’),(‘dog’, ‘the’)。

  3. 神经网络基于这些训练数据将会输出一个概率分布,这个概率代表着我们的词典中的每个词作为input word的output word的可能性。这句话有点绕,我们来看个例子。第二步中我们在设置skip_window和num_skips=2的情况下获得了两组训练数据。假如我们先拿一组数据 (‘dog’, ‘barked’) 来训练神经网络,那么模型通过学习这个训练样本,会告诉我们词汇表中每个单词当’dog’作为input word时,其作为output word的可能性。

也就是说模型的输出概率代表着到我们词典中每个词有多大可能性跟input word同时出现。例如:如果我们向神经网络模型中输入一个单词“Soviet“,那么最终模型的输出概率中,像“Union”, ”Russia“这种相关词的概率将远高于像”watermelon“,”kangaroo“非相关词的概率。因为”Union“,”Russia“在文本中更大可能在”Soviet“的窗口中出现。

我们将通过给神经网络输入文本中成对的单词来训练它完成上面所说的概率计算。下面的图中给出了一些我们训练样本的例子。我们选定句子“The quick brown fox jumps over lazy dog”,设定我们的窗口大小为2(window_size=2),也就是说我们仅选输入词前后各两个词和输入词进行组合。下图中,蓝色代表input word,方框内代表位于窗口内的单词。

1 2

我们的模型将会从每对单词出现的次数中习得统计结果。例如,我们的神经网络可能会得到更多类似(“Soviet“,”Union“)这样的训练样本对,而对于(”Soviet“,”Sasquatch“)这样的组合却看到的很少。因此,当我们的模型完成训练后,给定一个单词”Soviet“作为输入,输出的结果中”Union“或者”Russia“要比”Sasquatch“被赋予更高的概率。

PS:input word和output word都会被我们进行one-hot编码。仔细想一下,我们的输入被one-hot编码以后大多数维度上都是0(实际上仅有一个位置为1),所以这个向量相当稀疏,那么会造成什么结果呢。如果我们将一个1 x 10000的向量和10000 x 300的矩阵相乘,它会消耗相当大的计算资源,为了高效计算,它仅仅会选择矩阵中对应的向量中维度值为1的索引行:

2. Skip-grams训练

由上部分可知,Word2Vec模型是一个超级大的神经网络(权重矩阵规模非常大)。例如:我们拥有10000个单词的词汇表,我们如果想嵌入300维的词向量,那么我们的输入-隐层权重矩阵和隐层-输出层的权重矩阵都会有 10000 x 300 = 300万个权重,在如此庞大的神经网络中进行梯度下降是相当慢的。更糟糕的是,你需要大量的训练数据来调整这些权重并且避免过拟合。百万数量级的权重矩阵和亿万数量级的训练样本意味着训练这个模型将会是个灾难

解决方案:

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

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

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

2.1 Word pairs and “phases”

一些单词组合(或者词组)的含义和拆开以后具有完全不同的意义。比如“Boston Globe”是一种报刊的名字,而单独的“Boston”和“Globe”这样单个的单词却表达不出这样的含义。因此,在文章中只要出现“Boston Globe”,我们就应该把它作为一个单独的词来生成其词向量,而不是将其拆开。同样的例子还有“New York”,“United Stated”等。

在Google发布的模型中,它本身的训练样本中有来自Google News数据集中的1000亿的单词,但是除了单个单词以外,单词组合(或词组)又有3百万之多。

2.2 对高频词抽样

在上一部分中,对于原始文本为“The quick brown fox jumps over the laze dog”,如果使用大小为2的窗口,那么我们可以得到图中展示的那些训练样本。

1

但是对于“the”这种常用高频单词,这样的处理方式会存在下面两个问题:

  1. 当我们得到成对的单词训练样本时,(“fox”, “the”) 这样的训练样本并不会给我们提供关于“fox”更多的语义信息,因为“the”在每个单词的上下文中几乎都会出现

  2. 由于在文本中“the”这样的常用词出现概率很大,因此我们将会有大量的(”the“,…)这样的训练样本,而这些样本数量远远超过了我们学习“the”这个词向量所需的训练样本数

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

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

P(ωi) 代表着保留某个单词的概率:

2.3 Negative sampling

训练一个神经网络意味着要输入训练样本并且不断调整神经元的权重,从而不断提高对目标的准确预测。每当神经网络经过一个训练样本的训练,它的权重就会进行一次调整。

所以,词典的大小决定了我们的Skip-Gram神经网络将会拥有大规模的权重矩阵,所有的这些权重需要通过数以亿计的训练样本来进行调整,这是非常消耗计算资源的,并且实际中训练起来会非常慢。

负采样(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“)。

PS: 在论文中,作者指出指出对于小规模数据集,选择5-20个negative words会比较好,对于大规模数据集可以仅选择2-5个negative words。

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

每个单词被选为“negative words”的概率计算公式:

其中 f(ωi)代表着单词出现的频次,而公式中开3/4的根号完全是基于经验的。

在代码负采样的代码实现中,unigram table有一个包含了一亿个元素的数组,这个数组是由词汇表中每个单词的索引号填充的,并且这个数组中有重复,也就是说有些单词会出现多次。那么每个单词的索引在这个数组中出现的次数该如何决定呢,有公式,也就是说计算出的负采样概率*1亿=单词在表中出现的次数。

有了这张表以后,每次去我们进行负采样时,只需要在0-1亿范围内生成一个随机数,然后选择表中索引号为这个随机数的那个单词作为我们的negative word即可。一个单词的负采样概率越大,那么它在这个表中出现的次数就越多,它被选中的概率就越大。

使用gensim训练word2vec

class gensim.models.word2vec.Word2Vec(sentences=None, corpus_file=None, size=100, alpha=0.025, window=5, 
min_count=5,  max_vocab_size=None, sample=0.001, seed=1, workers=3, min_alpha=0.0001, sg=0, hs=0, 
negative=5, ns_exponent=0.75, cbow_mean=1, hashfxn=<built-in function hash>, iter=5, null_word=0, trim_rule=None, 
sorted_vocab=1, batch_words=10000, compute_loss=False, callbacks=(), max_final_vocab=None)

参数说明:

  • sentences (iterable of iterables, optional) – 供训练的句子,可以使用简单的列表,但是对于大语料库,建议直接从磁盘/网络流迭代传输句子。参阅word2vec模块中的BrownCorpus,Text8Corpus或LineSentence。
  • corpus_file (str, optional) – LineSentence格式的语料库文件路径。
  • size (int, optional) – word向量的维度。
  • window (int, optional) – 一个句子中当前单词和被预测单词的最大距离。
  • min_count (int, optional) – 忽略词频小于此值的单词。
  • workers (int, optional) – 训练模型时使用的线程数。
  • sg ({0, 1}, optional) – 模型的训练算法: 1: skip-gram; 0: CBOW.
  • hs ({0, 1}, optional) – 1: 采用hierarchical softmax训练模型; 0: 使用负采样。
  • negative (int, optional) – > 0: 使用负采样,设置多个负采样(通常在5-20之间)。
  • ns_exponent (float, optional) – 负采样分布指数。1.0样本值与频率成正比,0.0样本所有单词均等,负值更多地采样低频词。
  • cbow_mean ({0, 1}, optional) – 0: 使用上下文单词向量的总和; 1: 使用均值,适用于使用CBOW。
  • alpha (float, optional) – 初始学习率。
  • min_alpha (float, optional) – 随着训练的进行,学习率线性下降到min_alpha。
  • seed (int, optional) – 随机数发生器种子。
  • max_vocab_size (int, optional) – 词汇构建期间RAM的限制; 如果有更多的独特单词,则修剪不常见的单词。 每1000万个类型的字需要大约1GB的RAM。
  • max_final_vocab (int, optional) – 自动选择匹配的min_count将词汇限制为目标词汇大小。
  • sample (float, optional) – 高频词随机下采样的配置阈值,范围是(0,1e-5)。
  • hashfxn (function, optional) – 哈希函数用于随机初始化权重,以提高训练的可重复性。
  • iter (int, optional) – 迭代次数。
  • trim_rule (function, optional) – 词汇修剪规则,指定某些词语是否应保留在词汇表中,修剪掉或使用默认值处理。
  • sorted_vocab ({0, 1}, optional) – 如果为1,则在分配单词索引前按降序对词汇表进行排序。
  • batch_words (int, optional) – 每一个batch传递给线程单词的数量。
  • compute_loss (bool, optional) – 如果为True,则计算并存储可使用get_latest_training_loss()检索的损失值。
  • callbacks (iterable of CallbackAny2Vec, optional) – 在训练中特定阶段执行回调序列。

官方文档链接

2、TextCNN

TextCNN利用CNN(卷积神经网络)进行文本特征抽取,不同大小的卷积核分别抽取n-gram特征,卷积计算出的特征图经过MaxPooling保留最大的特征值,然后将拼接成一个向量作为文本的表示。

这里我们基于TextCNN原始论文的设定,分别采用了100个大小为2,3,4的卷积核,最后得到的文本向量大小为100*3=300维。

3、TextRNN

extRNN利用RNN(循环神经网络)进行文本特征抽取,由于文本本身是一种序列,而LSTM天然适合建模序列数据。TextRNN将句子中每个词的词向量依次输入到双向双层LSTM,分别将两个方向最后一个有效位置的隐藏层拼接成一个向量作为文本的表示。

5

代码实现(Pytorch):

TextCNN

  • 模型搭建
import torch.nn as nn

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

  • 模型搭建
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)

基于深度学习的文本分类3

Transformer原理

Transformer是在"Attention is All You Need"中提出的,模型的编码部分是一组编码器的堆叠(论文中依次堆叠六个编码器),模型的解码部分是由相同数量的解码器的堆叠。

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

这里能看到Transformer的一个关键特性,每个位置的词仅仅流过它自己的编码器路径。在self-attention层中,这些路径两两之间是相互依赖的。前向网络层则没有这些依赖性,但这些路径在流经前向网络时可以并行执行。

Self-Attention中使用多头机制,使得不同的attention heads所关注的的部分不同。

编码"it"时,一个attention head集中于"the animal",另一个head集中于“tired”,某种意义上讲,模型对“it”的表达合成了的“animal”和“tired”两者。

对于自注意力的详细计算,欢迎大家参考Jay Alammar关于Transformer的博客,这里不再展开。

除此之外,为了使模型保持单词的语序,模型中添加了位置编码向量。如下图所示,每行对应一个向量的位置编码。因此,第一行将是我们要添加到输入序列中第一个单词的嵌入的向量。每行包含512个值—每个值都在1到-1之间。因为左侧是用sine函数生成,右侧是用cosine生成,所以可以观察到中间显著的分隔。

编码器结构中值得提出注意的一个细节是,在每个子层中(Self-attention, FFNN),都有残差连接,并且紧跟着layer-normalization。如果我们可视化向量和LayerNorm操作,将如下所示:

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

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

bert_elmo

ELMo论文题目中Deep是指双向双层LSTM,而更关键的在于context。传统方法生成的单词映射表的形式,即先为每个单词生成一个静态的词向量,之后这个单词的表示就被固定住了,不会跟着上下文的变化而做出改变。事实上,由于一词多义的语言现象,静态词向量是有很大的弊端的。以bank为例,如果训练语料的足够大,事先学好的词向量中混杂着所有的语义。而当下游应用时,即使在新句子中,bank的上下文里包含money等词,我们基本可以确定bank是“银行”的语义而不是在其他上下文中的“河床”的语义,但是由于静态词向量不能跟随上下文而进行变化,所以bank的表示中还是混杂着多种语义。为了解决这一问题,ELMo首先进行了语言模型预训练,然后在下游任务中动态调整Word Embedding,因此最后输出的词表示能够充分表达单词在上下文中的特定语义,进而解决一词多义的问题。

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

Google在NAACL 2018发表的论文中提出了BERT,与GPT相同,BERT也采用了预训练-微调这一两阶段模式。但在模型结构方面,BERT采用了ELMO的范式,即使用双向语言模型代替GPT中的单向语言模型,但是BERT的作者认为ELMo使用两个单向语言模型拼接的方式太粗暴,因此在第一阶段的预训练过程中,BERT提出掩码语言模型,即类似完形填空的方式,通过上下文来预测单词本身,而不是从右到左或从左到右建模,这允许模型能够自由地编码每个层中来自两个方向的信息。而为了学习句子的词序关系,BERT将Transformer中的三角函数位置表示替换为可学习的参数,其次为了区别单句和双句输入,BERT还引入了句子类型表征。BERT的输入如图所示。此外,为了充分学习句子间的关系,BERT提出了下一个句子预测任务。具体来说,在训练时,句子对中的第二个句子有50%来自与原有的连续句子,而其余50%的句子则是通过在其他句子中随机采样。同时,消融实验也证明,这一预训练任务对句间关系判断任务具有很大的贡献。除了模型结构不同之外,BERT在预训练时使用的无标签数据规模要比GPT大的多。

bert_input

在第二阶段,与GPT相同,BERT也使用Fine-Tuning模式来微调下游任务。如下图所示,BERT与GPT不同,它极大的减少了改造下游任务的要求,只需在BERT模型的基础上,通过额外添加Linear分类器,就可以完成下游任务。具体来说,对于句间关系判断任务,与GPT类似,只需在句子之间加个分隔符,然后在两端分别加上起始和终止符号。在进行输出时,只需把句子的起始符号[CLS]在BERT最后一层中对应的位置接一个Softmax+Linear分类层即可;对于单句分类问题,也与GPT类似,只需要在句子两段分别增加起始和终止符号,输出部分和句间关系判断任务保持一致即可;对于问答任务,由于需要输出答案在给定段落的起始和终止位置,因此需要先将问题和段落按照句间关系判断任务构造输入,输出只需要在BERT最后一层中第二个句子,即段落的每个单词对应的位置上分别接判断起始和终止位置的分类器;最后,对于NLP中的序列标注问题,输入与单句分类任务一致,不同的是在BERT最后一层中每个单词对应的位置上接分类器即可。

bert_task

更重要的是,BERT开启了NLP领域“预训练-微调”这种两阶段的全新范式。在第一阶段首先在海量无标注文本上预训练一个双向语言模型,这里特别值得注意的是,将Transformer作为特征提取器在解决并行性和长距离依赖问题上都要领先于传统的RNN或者CNN,通过预训练的方式,可以将训练数据中的词法、句法、语法知识以网络参数的形式提炼到模型当中,在第二阶段使用下游任务的数据Fine-tuning不同层数的BERT模型参数,或者把BERT当作特征提取器生成BERT Embedding,作为新特征引入下游任务。这种两阶段的全新范式尽管是来自于计算机视觉领域,但是在自然语言处理领域一直没有得到很好的运用,而BERT作为近些年NLP突破性进展的集大成者,最大的亮点可以说不仅在于模型性能好,并且几乎所有NLP任务都可以很方便地基于BERT进行改造,进而将预训练学到的语言学知识引入下游任务,进一步提升模型的性能。

参考:

  1. https://mp.weixin.qq.com/s/I-yeHQopTFdNk67Ir_iWiA
  2. https://github.com/hecongqing/2018-daguan-competition
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值