Advertisement

中文新闻分类 数据集_NLP-新闻文本分类实战

阅读量:

一、赛题理解

赛题名称:零基础入门NLP之新闻文本分类 赛题目标:通过这道赛题可以引导大家走入自然语言处理的世界,带大家接触NLP的预处理、模型构建和模型训练等知识点。 赛题任务:赛题以自然语言处理为背景,要求选手对新闻文本进行分类,这是一个典型的字符识别问题。

赛题数据

赛题以匿名处理后的新闻数据为赛题数据,数据集报名后可见并可下载。赛题数据为新闻文本,并按照字符级别进行匿名处理。整合划分出14个候选分类类别:财经、彩票、房产、股票、家居、教育、科技、社会、时尚、时政、体育、星座、游戏、娱乐的文本数据。

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

数据标签

处理后的赛题训练数据如下:
0b1d1ba1d1f270e57839c49e0b74926c.png

在数据集中标签的对应的关系如下:

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

评测指标

评价标准为类别f1_score的均值,选手提交结果与实际测试集的类别进行对比,结果越大越好。

解题思路

赛题思路分析:赛题本质是一个文本分类问题,需要根据每句的字符进行分类。但赛题给出的数据是匿名化的,不能直接使用中文分词等操作,这个是赛题的难点。

因此本次赛题的难点是需要对匿名字符进行建模,进而完成文本分类的过程。由于文本数据是一种典型的非结构化数据,因此可能涉及到特征提取分类模型两个部分。

  • 思路1:TF-IDF + 机器学习分类器

直接使用TF-IDF对文本提取特征,并使用分类器进行分类。在分类器的选择上,可以使用SVM、LR、或者XGBoost。

  • 思路2:FastText

FastText是入门款的词向量,利用Facebook提供的FastText工具,可以快速构建出分类器。

  • 思路3:WordVec + 深度学习分类器

WordVec是进阶款的词向量,并通过构建深度学习分类完成分类。深度学习分类的网络结构可以选择TextCNN、TextRNN或者BiLSTM。

  • 思路4:Bert词向量

Bert是高配款的词向量,具有强大的建模学习能力。


二、 数据读取与数据分析

本章主要内容为数据读取和数据分析,具体使用Pandas库完成数据读取操作,并对赛题数据进行分析构成。

学习目标

  • 学习使用Pandas读取赛题数据
  • 分析赛题数据的分布规律

数据读取

赛题数据虽然是文本数据,每个新闻是不定长的,但任然使用csv格式进行存储。因此可以直接用Pandas完成数据读取的操作。

复制代码
 import pandas as pd

    
 train_df = pd.read_csv('../data/train_set.csv', sep='t', nrows=100)#读取前100行

这里的read_csv由三部分构成:

  • 读取的文件路径,这里需要根据改成你本地的路径,可以使用相对路径或绝对路径;
  • 分隔符sep,为每列分割的字符,设置为t即可;
  • 读取行数nrows,为此次读取文件的函数,是数值类型(由于数据集比较大,建议先设置为100);
    4149f777832e54ebe54ad956f4623ebc.png

上图是读取好的数据,是表格的形式。第一列为新闻的类别,第二列为新闻的字符。

数据分析

在读取完成数据集后,我们还可以对数据集进行数据分析的操作。虽然对于非结构数据并不需要做很多的数据分析,但通过数据分析还是可以找出一些规律的。

此步骤我们读取了所有的训练集数据,在此我们通过数据分析希望得出以下结论:

  • 赛题数据中,新闻文本的长度是多少?
  • 赛题数据的类别分布是怎么样的,哪些类别比较多?
  • 赛题数据中,字符分布是怎么样的?

句子长度分析

在赛题数据中每行句子的字符使用空格进行隔开,所以可以直接统计单词的个数来得到每个句子的长度。

首先将所有文本通过join函数变成一个长文本(索引),这里使用 lambda匿名函数方法,它允许快速定义单行函数,类似于C语言的宏,可以用在任何需要函数的地方,举个例子:

复制代码
 f = lambda x : x *

    
 f(3)
    
  
    
 Output: 9

同时使用了%pylab ,这是一个魔法函数,详细内容可参考Python魔术方法,其等价于:

复制代码
 import numpy

    
 import matplotlib
    
 from matplotlib import pylab, mlab, pyplot
    
 np = numpy
    
 plt = pyplot
    
  
    
 from IPython.display import display
    
 from IPython.core.pylabtools import figsize, getfigs
    
  
    
 from pylab import *
    
 from numpy import *

以下是句子长度统计的代码与结果

复制代码
 %pylab inline

    
 train_df['text_len'] = train_df['text'].apply(lambda x: len(x.split(' ')))
    
 print(train_df['text_len'].describe())
    
  
    
 Output:
    
 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

对新闻句子的统计可以得出,本次赛题给定的文本比较长,每个句子平均由907个字符构成,最短的句子长度为2,最长的句子长度为57921。

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

复制代码
 _ = plt.hist(train_df['text_len'], bins=200) #使用了%pylab,可以直接使用'plt'

    
 plt.xlabel('Text char count')
    
 plt.title("Histogram of char count")
    
 Text(0.5, 1.0, 'Histogram of char count')
681eeda70b00e6ca750daac8b89d5845.png

新闻类别分布

接下来可以对数据集的类别进行分布统计,具体统计每类新闻的样本个数。

复制代码
 train_df['label'].value_counts().plot(kind='bar')

    
 plt.title('News class count')
    
 plt.xlabel("category")
    
 Text(0.5, 0, 'category')
b4b49a1413f94dbbc5978bd5dc05ad5f.png

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

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

字符分布统计

接下来可以统计每个字符出现的次数,首先可以将训练集中所有的句子进行拼接进而划分为字符,并统计每个字符的个数。

这里我们使用python的常用内建模块 collections。Counter是一个简单计数器,可以用来统计字符出现的个数,举个简单的例子:

复制代码
 from collections import Counter

    
 c = Counter()
    
 for ch in 'datawhale':
    
     c[ch] = c[ch] + 1
    
 c
    
  
    
 Output: Counter({'d': 1, 'a': 3, 't': 1, 'w': 1, 'h': 1, 'l': 1, 'e': 1})

同时Counter也有update功能:

复制代码
 c.update('mynlp')

    
 c
    
  
    
 Output: Counter({'d': 1, 'a': 3, 't': 1, 'w': 1, 'h': 1, 'l': 2, 'e': 1, 'm': 1, 'y': 1, 'n': 1, 'p': 1})

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

复制代码
 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))
    
  
    
 print(word_count[0])
    
  
    
 print(word_count[-1])
    
  
    
 Output:
    
 6869
    
 ('3750',7482224)
    
 ('3133',1)

这里还可以根据字在每个句子的出现情况,反推出标点符号。下面代码统计了不同字符在句子中出现的次数,其中字符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])
    
  
    
 print(word_count[1])
    
  
    
 print(word_count[2])
    
  
    
 Output:
    
 ('3750', 197997)
    
 ('900', 197653)
    
 ('648', 191975)

数据分析的结论

通过上述分析我们可以得出以下结论:

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

通过数据分析,我们还可以得出以下结论:

  1. 每个新闻平均字符个数较多,可能需要截断;
  2. 由于类别不均衡,会严重影响模型的精度;

本章小结

本章对赛题数据进行读取,并新闻句子长度、类别和字符进行了可视化分析。


三、基于机器学习的文本分类

在本章我们将开始使用机器学习模型来解决文本分类。机器学习发展比较广,且包括多个分支,本章侧重使用传统机器学习,从下一章开始是基于深度学习的文本分类。

学习目标

  • 学会TF-IDF的原理和使用
  • 使用sklearn的机器学习模型完成文本分类

机器学习模型

机器学习是对能通过经验自动改进的计算机算法的研究。机器学习通过历史数据训练模型 对应于人类对经验进行归纳 的过程,机器学习利用模型 对新数据进行预测 对应于人类利用总结的规律 对新问题进行预测 的过程。

机器学习有很多种分支,对于学习者来说应该优先掌握机器学习算法的分类,然后再其中一种机器学习算法进行学习。由于机器学习算法的分支和细节实在是太多,所以如果你一开始就被细节迷住了眼,你就很难知道全局是什么情况的。

如果你是机器学习初学者,你应该知道如下的事情:

  1. 机器学习能解决一定的问题,但不能奢求机器学习是万能的;
  2. 机器学习算法有很多种,看具体问题需要什么,再来进行选择;
  3. 每种机器学习算法有一定的偏好,需要具体问题具体分析;

文本表示方法

在机器学习算法的训练过程中,假设给定NN个样本,每个样本有MM个特征,这样组成了N×MN×M的样本矩阵,然后完成算法的训练和预测。同样的在计算机视觉中可以将图片的像素看作特征,每张图片看作hight×width×3的特征图,一个三维的矩阵来进入计算机进行计算。

但是在自然语言领域,上述方法却不可行:文本是不定长度的。文本表示成计算机能够运算的数字或向量的方法一般称为词嵌入(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. 在训练集中每一个出现在任意文中的单词分配一个特定的整数 id(比如,通过建立一个从单词到整数索引的字典)。
  2. 对于每个文档 #i,计算每个单词 w 的出现次数并将其存储在 X[i, j] 中作为特征 #j 的值,其中 j 是在字典中词 w 的索引。

在这种方法中 n_features 是在整个文集(文章集合的缩写,下同)中不同单词的数量: 这个值一般来说超过 100,000 。

如果 n_samples == 10000 , 存储 X 为 “float32” 型的 numpy 数组将会需要 10000 x 100000 x 4 bytes = 4GB内存 ,在当前的计算机中非常不好管理的。

幸运的是, X 数组中大多数的值为 0 ,是因为特定的文档中使用的单词数量远远少于总体的词袋单词个数。 因此我们可以称词袋模型是典型的 high-dimensional sparse datasets(高维稀疏数据集) 。 我们可以通过只在内存中保存特征向量中非 0 的部分以节省大量内存。

复制代码
 句子1:我 爱 北 京 天 安 门

    
 句子2:我 喜 欢 上 海

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

复制代码
 句子1:我 爱 北 京 天 安 门

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

在 sklearn 中可以直接用 CountVectorizer 来实现这一步骤(关于sklearn文本处理可参考:sklearn处理文本数据):

复制代码
 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()

N-gram

N-gram与Count Vectors类似,不过加入了相邻单词组合成为新的单词,并进行计数。

如果N取值为2,则句子1和句子2就变为:

复制代码
 句子1:我爱 爱北 北京 京天 天安 安门

    
 句子2:我喜 喜欢 欢上 上海

TF-IDF

从出现次数进行统计存在一个根本性的问题:长的文本相对于短的文本有更高的单词平均出现次数,尽管他们可能在描述同一个主题。

为了避免这些潜在的差异,只需将各文档中每个单词的出现次数除以该文档中所有单词的总数:这些新的特征称之为词频 tf (Term Frequencies)。

另一个在词频的基础上改良是,降低在该训练文集中的很多文档中均出现的单词的权重,从而突出那些仅在该训练文集中在一小部分文档中出现的单词的信息量。

TF-IDF基于上述原理进行词频分数统计。TF-IDF 分数由两部分组成:第一部分是词语频率 (Term Frequency),第二部分是逆文档频率 (Inverse Document Frequency)。其中计算语料库中文档总数除以含有该词语的文档数量,然后再取对数就是逆文档频率。

复制代码
 TF(t)= 该词语在当前文档出现的次数 / 当前文档中词语的总数

    
 IDF(t)= log_e(文档总数 / (出现该词语的文档总数+1))#在实际工作中,可能先有词表,再处理文档语料,该词有可能是不存在任何文档中的

基于机器学习的文本分类

接下来我们将对比不同文本表示算法的精度,通过本地构建验证集计算F1得分。

复制代码
 # 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('../data/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

0.7422037924439758

复制代码
 # 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('../data/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

0.8721598830546126

本章小结

本章我们介绍了基于机器学习的文本分类方法,并完成了两种方法的对比。


四、基于深度学习的文本分类1-fastText

与传统机器学习不同,深度学习既提供特征提取功能,也可以完成分类的功能。从本章开始我们将学习如何使用深度学习来完成文本表示。

学习目标

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

文本表示方法 Part2

现有文本表示方法的缺陷

在上一章节,我们介绍几种文本表示方法:

  • One-hot
  • Bag of Words
  • N-gram
  • TF-IDF

也通过sklean进行了相应的实践,相信你也有了初步的认知。但上述方法都或多或少存在一定的问题:转换得到的向量维度很高,需要较长的训练实践;没有考虑单词与单词之间的关系,只是进行了统计。

与这些表示方法不同,深度学习也可以用于文本表示,还可以将其映射到一个低纬空间。其中比较典型的例子有:FastText、Word2Vec和Bert。在本章我们将介绍FastText,将在后面的内容介绍Word2Vec和Bert。

FastText

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

所以FastText是一个三层的神经网络,输入层、隐含层和输出层。
67b8bf2cdf16f0a912d2da6c7ec47dce.png

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

FastText在文本分类任务上,是优于TF-IDF的:

  • FastText用单词的Embedding叠加获得的文档向量,将相似的句子分为一类
  • FastText学习到的Embedding空间维度比较低,可以快速进行训练

如果想深度学习,可以参考论文:

Bag of Tricks for Efficient Text Classification, https://arxiv.org/abs/1607.01759

基于FastText的文本分类

FastText可以快速的在CPU上进行训练,最好的实践方法就是官方开源的版本: https://github.com/facebookresearch/fastText/tree/master/python

  • pip安装
复制代码
    pip install fasttext
  • 源码安装
复制代码
 git clone https://github.com/facebookresearch/fastText.git

    
 cd fastText
    
 sudo pip install .

两种安装方法都可以安装,如果你是初学者可以优先考虑使用pip安装。

复制代码
 import pandas as pd

    
 from sklearn.metrics import f1_score
    
  
    
 # 转换为FastText需要的格式
    
 train_df = pd.read_csv('../data/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 = fasttext.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

此时数据量比较小得分为0.82,当不断增加训练集数量时,FastText的精度也会不断增加5w条训练样本时,验证集得分可以到0.89-0.90左右。

如何使用验证集调参

在使用TF-IDF和FastText中,有一些模型的参数需要选择,这些参数会在一定程度上影响模型的精度,那么如何选择这些参数呢?

  • 通过阅读文档,要弄清楚这些参数的大致含义,那些参数会增加模型的复杂度
  • 通过在验证集上进行验证模型精度,找到模型在是否过拟合还是欠拟合
    dcef94527a7307268bddd2dbd7535ef7.png

这里我们使用10折交叉验证,每折使用9/10的数据进行训练,剩余1/10作为验证集检验模型的效果。这里需要注意每折的划分必须保证标签的分布与整个数据集的分布一致。

复制代码
 label2id = {}

    
 for i in range(total):
    
     label = str(all_labels[i])
    
     if label not in label2id:
    
     label2id[label] = [i]
    
     else:
    
     label2id[label].append(i)

通过10折划分,我们一共得到了10份分布一致的数据,索引分别为0到9,每次通过将一份数据作为验证集,剩余数据作为训练集,获得了所有数据的10种分割。不失一般性,我们选择最后一份完成剩余的实验,即索引为9的一份做为验证集,索引为1-8的作为训练集,然后基于验证集的结果调整超参数,使得模型性能更优。

本章小结

本章介绍了FastText的原理和基础使用,并进行相应的实践。然后介绍了通过10折交叉验证划分数据集。

5 基于深度学习的文本分类2-1Word2Vec

复制代码
 import logging

    
 import random
    
  
    
 import numpy as np
    
 import torch
    
  
    
 logging.basicConfig(level=logging.INFO, format='%(asctime)-15s %(levelname)s: %(message)s')
    
  
    
 # set seed 
    
 seed = 666
    
 random.seed(seed)
    
 np.random.seed(seed)
    
 torch.cuda.manual_seed(seed)
    
 torch.manual_seed(seed)
    
  
    
 # split data to 10 fold
    
 fold_num = 10
    
 data_file = '../data/train_set.csv'
    
 import pandas as pd
    
  
    
  
    
 def all_data2fold(fold_num, num=10000):
    
     fold_data = []
    
     f = pd.read_csv(data_file, sep='t', encoding='UTF-8')
    
     texts = f['text'].tolist()[:num]
    
     labels = f['label'].tolist()[:num]
    
  
    
     total = len(labels)
    
  
    
     index = list(range(total))
    
     np.random.shuffle(index)
    
  
    
     all_texts = []
    
     all_labels = []
    
     for i in index:
    
     all_texts.append(texts[i])
    
     all_labels.append(labels[i])
    
  
    
     label2id = {}
    
     for i in range(total):
    
     label = str(all_labels[i])
    
     if label not in label2id:
    
         label2id[label] = [i]
    
     else:
    
         label2id[label].append(i)
    
  
    
     all_index = [[] for _ in range(fold_num)]
    
     for label, data in label2id.items():
    
     # print(label, len(data))
    
     batch_size = int(len(data) / fold_num)
    
     other = len(data) - batch_size * fold_num
    
     for i in range(fold_num):
    
         cur_batch_size = batch_size + 1 if i < other else batch_size
    
         # print(cur_batch_size)
    
         batch_data = [data[i * batch_size + b] for b in range(cur_batch_size)]
    
         all_index[i].extend(batch_data)
    
  
    
     batch_size = int(total / fold_num)
    
     other_texts = []
    
     other_labels = []
    
     other_num = 0
    
     start = 0
    
     for fold in range(fold_num):
    
     num = len(all_index[fold])
    
     texts = [all_texts[i] for i in all_index[fold]]
    
     labels = [all_labels[i] for i in all_index[fold]]
    
  
    
     if num > batch_size:
    
         fold_texts = texts[:batch_size]
    
         other_texts.extend(texts[batch_size:])
    
         fold_labels = labels[:batch_size]
    
         other_labels.extend(labels[batch_size:])
    
         other_num += num - batch_size
    
     elif num < batch_size:
    
         end = start + batch_size - num
    
         fold_texts = texts + other_texts[start: end]
    
         fold_labels = labels + other_labels[start: end]
    
         start = end
    
     else:
    
         fold_texts = texts
    
         fold_labels = labels
    
  
    
     assert batch_size == len(fold_labels)
    
  
    
     # shuffle
    
     index = list(range(batch_size))
    
     np.random.shuffle(index)
    
  
    
     shuffle_fold_texts = []
    
     shuffle_fold_labels = []
    
     for i in index:
    
         shuffle_fold_texts.append(fold_texts[i])
    
         shuffle_fold_labels.append(fold_labels[i])
    
  
    
     data = {'label': shuffle_fold_labels, 'text': shuffle_fold_texts}
    
     fold_data.append(data)
    
  
    
     logging.info("Fold lens %s", str([len(data['label']) for data in fold_data]))
    
  
    
     return fold_data
    
  
    
  
    
 fold_data = all_data2fold(10)
    
  
    
 # build train data for word2vec
    
 fold_id = 9
    
  
    
 train_texts = []
    
 for i in range(0, fold_id):
    
     data = fold_data[i]
    
     train_texts.extend(data['text'])
    
     
    
 logging.info('Total %d docs.' % len(train_texts))
    
  
    
 logging.info('Start training...')
    
 from gensim.models.word2vec import Word2Vec
    
  
    
 num_features = 100     # Word vector dimensionality
    
 num_workers = 8       # Number of threads to run in parallel
    
  
    
 train_texts = list(map(lambda x: list(x.split()), train_texts))
    
 model = Word2Vec(train_texts, workers=num_workers, size=num_features)
    
 model.init_sims(replace=True)
    
  
    
 # save model
    
 model.save("./word2vec.bin")
    
  
    
 # load model
    
 model = Word2Vec.load("./word2vec.bin")
    
  
    
 # convert format
    
 model.wv.save_word2vec_format('./word2vec.txt', binary=False)

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

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

复制代码
 import logging

    
 import random
    
  
    
 import numpy as np
    
 import torch
    
  
    
 logging.basicConfig(level=logging.INFO, format='%(asctime)-15s %(levelname)s: %(message)s')
    
  
    
 # set seed
    
 seed = 666
    
 random.seed(seed)
    
 np.random.seed(seed)
    
 torch.cuda.manual_seed(seed)
    
 torch.manual_seed(seed)
    
  
    
 # set cuda
    
 gpu = 0
    
 use_cuda = gpu >= 0 and torch.cuda.is_available()
    
 if use_cuda:
    
     torch.cuda.set_device(gpu)
    
     device = torch.device("cuda", gpu)
    
 else:
    
     device = torch.device("cpu")
    
 logging.info("Use cuda: %s, gpu id: %d.", use_cuda, gpu)
    
  
    
 # split data to 10 fold
    
 fold_num = 10
    
 data_file = '../data/train_set.csv'
    
 import pandas as pd
    
  
    
  
    
 def all_data2fold(fold_num, num=10000):
    
     fold_data = []
    
     f = pd.read_csv(data_file, sep='t', encoding='UTF-8')
    
     texts = f['text'].tolist()[:num]
    
     labels = f['label'].tolist()[:num]
    
  
    
     total = len(labels)
    
  
    
     index = list(range(total))
    
     np.random.shuffle(index)
    
  
    
     all_texts = []
    
     all_labels = []
    
     for i in index:
    
     all_texts.append(texts[i])
    
     all_labels.append(labels[i])
    
  
    
     label2id = {}
    
     for i in range(total):
    
     label = str(all_labels[i])
    
     if label not in label2id:
    
         label2id[label] = [i]
    
     else:
    
         label2id[label].append(i)
    
  
    
     all_index = [[] for _ in range(fold_num)]
    
     for label, data in label2id.items():
    
     # print(label, len(data))
    
     batch_size = int(len(data) / fold_num)
    
     other = len(data) - batch_size * fold_num
    
     for i in range(fold_num):
    
         cur_batch_size = batch_size + 1 if i < other else batch_size
    
         # print(cur_batch_size)
    
         batch_data = [data[i * batch_size + b] for b in range(cur_batch_size)]
    
         all_index[i].extend(batch_data)
    
  
    
     batch_size = int(total / fold_num)
    
     other_texts = []
    
     other_labels = []
    
     other_num = 0
    
     start = 0
    
     for fold in range(fold_num):
    
     num = len(all_index[fold])
    
     texts = [all_texts[i] for i in all_index[fold]]
    
     labels = [all_labels[i] for i in all_index[fold]]
    
  
    
     if num > batch_size:
    
         fold_texts = texts[:batch_size]
    
         other_texts.extend(texts[batch_size:])
    
         fold_labels = labels[:batch_size]
    
         other_labels.extend(labels[batch_size:])
    
         other_num += num - batch_size
    
     elif num < batch_size:
    
         end = start + batch_size - num
    
         fold_texts = texts + other_texts[start: end]
    
         fold_labels = labels + other_labels[start: end]
    
         start = end
    
     else:
    
         fold_texts = texts
    
         fold_labels = labels
    
  
    
     assert batch_size == len(fold_labels)
    
  
    
     # shuffle
    
     index = list(range(batch_size))
    
     np.random.shuffle(index)
    
  
    
     shuffle_fold_texts = []
    
     shuffle_fold_labels = []
    
     for i in index:
    
         shuffle_fold_texts.append(fold_texts[i])
    
         shuffle_fold_labels.append(fold_labels[i])
    
  
    
     data = {'label': shuffle_fold_labels, 'text': shuffle_fold_texts}
    
     fold_data.append(data)
    
  
    
     logging.info("Fold lens %s", str([len(data['label']) for data in fold_data]))
    
  
    
     return fold_data
    
  
    
  
    
 fold_data = all_data2fold(10)
    
  
    
 # build train, dev, test data
    
 fold_id = 9
    
  
    
 # dev
    
 dev_data = fold_data[fold_id]
    
  
    
 # train
    
 train_texts = []
    
 train_labels = []
    
 for i in range(0, fold_id):
    
     data = fold_data[i]
    
     train_texts.extend(data['text'])
    
     train_labels.extend(data['label'])
    
  
    
 train_data = {'label': train_labels, 'text': train_texts}
    
  
    
 # test
    
 test_data_file = '../data/test_a.csv'
    
 f = pd.read_csv(test_data_file, sep='t', encoding='UTF-8')
    
 texts = f['text'].tolist()
    
 test_data = {'label': [0] * len(texts), 'text': texts}
    
  
    
 # build vocab
    
 from collections import Counter
    
 from transformers import BasicTokenizer
    
  
    
 basic_tokenizer = BasicTokenizer()
    
  
    
  
    
 class Vocab():
    
     def __init__(self, train_data):
    
     self.min_count = 5
    
     self.pad = 0
    
     self.unk = 1
    
     self._id2word = ['[PAD]', '[UNK]']
    
     self._id2extword = ['[PAD]', '[UNK]']
    
  
    
     self._id2label = []
    
     self.target_names = []
    
  
    
     self.build_vocab(train_data)
    
  
    
     reverse = lambda x: dict(zip(x, range(len(x))))
    
     self._word2id = reverse(self._id2word)
    
     self._label2id = reverse(self._id2label)
    
  
    
     logging.info("Build vocab: words %d, labels %d." % (self.word_size, self.label_size))
    
  
    
     def build_vocab(self, data):
    
     self.word_counter = Counter()
    
  
    
     for text in data['text']:
    
         words = text.split()
    
         for word in words:
    
             self.word_counter[word] += 1
    
  
    
     for word, count in self.word_counter.most_common():
    
         if count >= self.min_count:
    
             self._id2word.append(word)
    
  
    
     label2name = {0: '科技', 1: '股票', 2: '体育', 3: '娱乐', 4: '时政', 5: '社会', 6: '教育', 7: '财经',
    
                   8: '家居', 9: '游戏', 10: '房产', 11: '时尚', 12: '彩票', 13: '星座'}
    
  
    
     self.label_counter = Counter(data['label'])
    
  
    
     for label in range(len(self.label_counter)):
    
         count = self.label_counter[label]
    
         self._id2label.append(label)
    
         self.target_names.append(label2name[label])
    
  
    
     def load_pretrained_embs(self, embfile):
    
     with open(embfile, encoding='utf-8') as f:
    
         lines = f.readlines()
    
         items = lines[0].split()
    
         word_count, embedding_dim = int(items[0]), int(items[1])
    
  
    
     index = len(self._id2extword)
    
     embeddings = np.zeros((word_count + index, embedding_dim))
    
     for line in lines[1:]:
    
         values = line.split()
    
         self._id2extword.append(values[0])
    
         vector = np.array(values[1:], dtype='float64')
    
         embeddings[self.unk] += vector
    
         embeddings[index] = vector
    
         index += 1
    
  
    
     embeddings[self.unk] = embeddings[self.unk] / word_count
    
     embeddings = embeddings / np.std(embeddings)
    
  
    
     reverse = lambda x: dict(zip(x, range(len(x))))
    
     self._extword2id = reverse(self._id2extword)
    
  
    
     assert len(set(self._id2extword)) == len(self._id2extword)
    
  
    
     return embeddings
    
  
    
     def word2id(self, xs):
    
     if isinstance(xs, list):
    
         return [self._word2id.get(x, self.unk) for x in xs]
    
     return self._word2id.get(xs, self.unk)
    
  
    
     def extword2id(self, xs):
    
     if isinstance(xs, list):
    
         return [self._extword2id.get(x, self.unk) for x in xs]
    
     return self._extword2id.get(xs, self.unk)
    
  
    
     def label2id(self, xs):
    
     if isinstance(xs, list):
    
         return [self._label2id.get(x, self.unk) for x in xs]
    
     return self._label2id.get(xs, self.unk)
    
  
    
     @property
    
     def word_size(self):
    
     return len(self._id2word)
    
  
    
     @property
    
     def extword_size(self):
    
     return len(self._id2extword)
    
  
    
     @property
    
     def label_size(self):
    
     return len(self._id2label)
    
  
    
  
    
 vocab = Vocab(train_data)
    
  
    
 # build module
    
 import torch.nn as nn
    
 import torch.nn.functional as F
    
  
    
  
    
 class Attention(nn.Module):
    
     def __init__(self, hidden_size):
    
     super(Attention, self).__init__()
    
     self.weight = nn.Parameter(torch.Tensor(hidden_size, hidden_size))
    
     self.weight.data.normal_(mean=0.0, std=0.05)
    
  
    
     self.bias = nn.Parameter(torch.Tensor(hidden_size))
    
     b = np.zeros(hidden_size, dtype=np.float32)
    
     self.bias.data.copy_(torch.from_numpy(b))
    
  
    
     self.query = nn.Parameter(torch.Tensor(hidden_size))
    
     self.query.data.normal_(mean=0.0, std=0.05)
    
  
    
     def forward(self, batch_hidden, batch_masks):
    
     # batch_hidden: b x len x hidden_size (2 * hidden_size of lstm)
    
     # batch_masks:  b x len
    
  
    
     # linear
    
     key = torch.matmul(batch_hidden, self.weight) + self.bias  # b x len x hidden
    
  
    
     # compute attention
    
     outputs = torch.matmul(key, self.query)  # b x len
    
  
    
     masked_outputs = outputs.masked_fill((1 - batch_masks).bool(), float(-1e32))
    
  
    
     attn_scores = F.softmax(masked_outputs, dim=1)  # b x len
    
  
    
     # 对于全零向量,-1e32的结果为 1/len, -inf为nan, 额外补0
    
     masked_attn_scores = attn_scores.masked_fill((1 - batch_masks).bool(), 0.0)
    
  
    
     # sum weighted sources
    
     batch_outputs = torch.bmm(masked_attn_scores.unsqueeze(1), key).squeeze(1)  # b x hidden
    
  
    
     return batch_outputs, attn_scores
    
  
    
  
    
 # build word encoder
    
 bert_path = '../emb/bert-mini/'
    
 dropout = 0.15
    
  
    
 from transformers import BertModel
    
  
    
  
    
 class WordBertEncoder(nn.Module):
    
     def __init__(self):
    
     super(WordBertEncoder, self).__init__()
    
     self.dropout = nn.Dropout(dropout)
    
  
    
     self.tokenizer = WhitespaceTokenizer()
    
     self.bert = BertModel.from_pretrained(bert_path)
    
  
    
     self.pooled = False
    
     logging.info('Build Bert encoder with pooled {}.'.format(self.pooled))
    
  
    
     def encode(self, tokens):
    
     tokens = self.tokenizer.tokenize(tokens)
    
     return tokens
    
  
    
     def get_bert_parameters(self):
    
     no_decay = ['bias', 'LayerNorm.weight']
    
     optimizer_parameters = [
    
         {'params': [p for n, p in self.bert.named_parameters() if not any(nd in n for nd in no_decay)],
    
          'weight_decay': 0.01},
    
         {'params': [p for n, p in self.bert.named_parameters() if any(nd in n for nd in no_decay)],
    
          'weight_decay': 0.0}
    
     ]
    
     return optimizer_parameters
    
  
    
     def forward(self, input_ids, token_type_ids):
    
     # input_ids: sen_num x bert_len
    
     # token_type_ids: sen_num  x bert_len
    
  
    
     # sen_num x bert_len x 256, sen_num x 256
    
     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)
    
  
    
     return reps
    
  
    
  
    
 class WhitespaceTokenizer():
    
     """WhitespaceTokenizer with vocab."""
    
  
    
     def __init__(self):
    
     vocab_file = bert_path + 'vocab.txt'
    
     self._token2id = self.load_vocab(vocab_file)
    
     self._id2token = {v: k for k, v in self._token2id.items()}
    
     self.max_len = 256
    
     self.unk = 1
    
  
    
     logging.info("Build Bert vocab with size %d." % (self.vocab_size))
    
  
    
     def load_vocab(self, vocab_file):
    
     f = open(vocab_file, 'r')
    
     lines = f.readlines()
    
     lines = list(map(lambda x: x.strip(), lines))
    
     vocab = dict(zip(lines, range(len(lines))))
    
     return vocab
    
  
    
     def tokenize(self, tokens):
    
     assert len(tokens) <= self.max_len - 2
    
     tokens = ["[CLS]"] + tokens + ["[SEP]"]
    
     output_tokens = self.token2id(tokens)
    
     return output_tokens
    
  
    
     def token2id(self, xs):
    
     if isinstance(xs, list):
    
         return [self._token2id.get(x, self.unk) for x in xs]
    
     return self._token2id.get(xs, self.unk)
    
  
    
     @property
    
     def vocab_size(self):
    
     return len(self._id2token)
    
  
    
  
    
 # build sent encoder
    
 sent_hidden_size = 256
    
 sent_num_layers = 2
    
  
    
  
    
 class SentEncoder(nn.Module):
    
     def __init__(self, sent_rep_size):
    
     super(SentEncoder, self).__init__()
    
     self.dropout = nn.Dropout(dropout)
    
  
    
     self.sent_lstm = nn.LSTM(
    
         input_size=sent_rep_size,
    
         hidden_size=sent_hidden_size,
    
         num_layers=sent_num_layers,
    
         batch_first=True,
    
         bidirectional=True
    
     )
    
  
    
     def forward(self, sent_reps, sent_masks):
    
     # sent_reps:  b x doc_len x sent_rep_size
    
     # sent_masks: b x doc_len
    
  
    
     sent_hiddens, _ = self.sent_lstm(sent_reps)  # b x doc_len x hidden*2
    
     sent_hiddens = sent_hiddens * sent_masks.unsqueeze(2)
    
  
    
     if self.training:
    
         sent_hiddens = self.dropout(sent_hiddens)
    
  
    
     return sent_hiddens
    
  
    
 # build model
    
 class Model(nn.Module):
    
     def __init__(self, vocab):
    
     super(Model, self).__init__()
    
     self.sent_rep_size = 256
    
     self.doc_rep_size = sent_hidden_size 
    
     self.all_parameters = {}
    
     parameters = []
    
     self.word_encoder = WordBertEncoder()
    
     bert_parameters = self.word_encoder.get_bert_parameters()
    
  
    
     self.sent_encoder = SentEncoder(self.sent_rep_size)
    
     self.sent_attention = Attention(self.doc_rep_size)
    
     parameters.extend(list(filter(lambda p: p.requires_grad, self.sent_encoder.parameters())))
    
     parameters.extend(list(filter(lambda p: p.requires_grad, self.sent_attention.parameters())))
    
  
    
     self.out = nn.Linear(self.doc_rep_size, vocab.label_size, bias=True)
    
     parameters.extend(list(filter(lambda p: p.requires_grad, self.out.parameters())))
    
  
    
     if use_cuda:
    
         self.to(device)
    
  
    
     if len(parameters) > 0:
    
         self.all_parameters["basic_parameters"] = parameters
    
     self.all_parameters["bert_parameters"] = bert_parameters
    
  
    
     logging.info('Build model with bert word encoder, lstm sent encoder.')
    
  
    
     para_num = sum([np.prod(list(p.size())) for p in self.parameters()])
    
     logging.info('Model param num: %.2f M.' % (para_num / 1e6))
    
  
    
     def forward(self, batch_inputs):
    
     # batch_inputs(batch_inputs1, batch_inputs2): b x doc_len x sent_len
    
     # batch_masks : b x doc_len x sent_len
    
     batch_inputs1, batch_inputs2, batch_masks = batch_inputs
    
     batch_size, max_doc_len, max_sent_len = batch_inputs1.shape[0], batch_inputs1.shape[1], batch_inputs1.shape[2]
    
     batch_inputs1 = batch_inputs1.view(batch_size * max_doc_len, max_sent_len)  # sen_num x sent_len
    
     batch_inputs2 = batch_inputs2.view(batch_size * max_doc_len, max_sent_len)  # sen_num x sent_len
    
     batch_masks = batch_masks.view(batch_size * max_doc_len, max_sent_len)  # sen_num x sent_len
    
  
    
     sent_reps = self.word_encoder(batch_inputs1, batch_inputs2)  # sen_num x sent_rep_size
    
  
    
     sent_reps = sent_reps.view(batch_size, max_doc_len, self.sent_rep_size)  # b x doc_len x sent_rep_size
    
     batch_masks = batch_masks.view(batch_size, max_doc_len, max_sent_len)  # b x doc_len x max_sent_len
    
     sent_masks = batch_masks.bool().any(2).float()  # b x doc_len
    
  
    
     sent_hiddens = self.sent_encoder(sent_reps, sent_masks)  # b x doc_len x doc_rep_size
    
     doc_reps, atten_scores = self.sent_attention(sent_hiddens, sent_masks)  # b x doc_rep_size
    
  
    
     batch_outputs = self.out(doc_reps)  # b x num_labels
    
  
    
     return batch_outputs
    
     
    
 model = Model(vocab)
    
  
    
 # build optimizer
    
 learning_rate = 2e-4
    
 bert_lr = 5e-5
    
 decay = .75
    
 decay_step = 1000
    
 from transformers import AdamW, get_linear_schedule_with_warmup
    
  
    
  
    
 class Optimizer:
    
     def __init__(self, model_parameters, steps):
    
     self.all_params = []
    
     self.optims = []
    
     self.schedulers = []
    
  
    
     for name, parameters in model_parameters.items():
    
         if name.startswith("basic"):
    
             optim = torch.optim.Adam(parameters, lr=learning_rate)
    
             self.optims.append(optim)
    
  
    
             l = lambda step: decay ** (step // decay_step)
    
             scheduler = torch.optim.lr_scheduler.LambdaLR(optim, lr_lambda=l)
    
             self.schedulers.append(scheduler)
    
             self.all_params.extend(parameters)
    
         elif name.startswith("bert"):
    
             optim_bert = AdamW(parameters, bert_lr, eps=1e-8)
    
             self.optims.append(optim_bert)
    
  
    
             scheduler_bert = get_linear_schedule_with_warmup(optim_bert, 0, steps)
    
             self.schedulers.append(scheduler_bert)
    
  
    
             for group in parameters:
    
                 for p in group['params']:
    
                     self.all_params.append(p)
    
         else:
    
             Exception("no nameed parameters.")
    
  
    
     self.num = len(self.optims)
    
  
    
     def step(self):
    
     for optim, scheduler in zip(self.optims, self.schedulers):
    
         optim.step()
    
         scheduler.step()
    
         optim.zero_grad()
    
  
    
     def zero_grad(self):
    
     for optim in self.optims:
    
         optim.zero_grad()
    
  
    
     def get_lr(self):
    
     lrs = tuple(map(lambda x: x.get_lr()[-1], self.schedulers))
    
     lr = ' %.5f' * self.num
    
     res = lr % lrs
    
     return res
    
  
    
 # build dataset
    
 def sentence_split(text, vocab, max_sent_len=256, max_segment=16):
    
     words = text.strip().split()
    
     document_len = len(words)
    
  
    
     index = list(range(0, document_len, max_sent_len))
    
     index.append(document_len)
    
  
    
     segments = []
    
     for i in range(len(index) - 1):
    
     segment = words[index[i]: index[i + 1]]
    
     assert len(segment) > 0
    
     segment = [word if word in vocab._id2word else '<UNK>' for word in segment]
    
     segments.append([len(segment), segment])
    
  
    
     assert len(segments) > 0
    
     if len(segments) > max_segment:
    
     segment_ = int(max_segment / 2)
    
     return segments[:segment_] + segments[-segment_:]
    
     else:
    
     return segments
    
  
    
  
    
 def get_examples(data, word_encoder, vocab, max_sent_len=256, max_segment=8):
    
     label2id = vocab.label2id
    
     examples = []
    
  
    
     for text, label in zip(data['text'], data['label']):
    
     # label
    
     id = label2id(label)
    
  
    
     # words
    
     sents_words = sentence_split(text, vocab, max_sent_len-2, max_segment)
    
     doc = []
    
     for sent_len, sent_words in sents_words:
    
         token_ids = word_encoder.encode(sent_words)
    
         sent_len = len(token_ids)
    
         token_type_ids = [0] * sent_len
    
         doc.append([sent_len, token_ids, token_type_ids])
    
     examples.append([id, len(doc), doc])
    
  
    
     logging.info('Total %d docs.' % len(examples))
    
     return examples
    
  
    
 # build loader
    
  
    
 def batch_slice(data, batch_size):
    
     batch_num = int(np.ceil(len(data) / float(batch_size)))
    
     for i in range(batch_num):
    
     cur_batch_size = batch_size if i < batch_num - 1 else len(data) - batch_size * i
    
     docs = [data[i * batch_size + b] for b in range(cur_batch_size)]
    
  
    
     yield docs
    
  
    
  
    
 def data_iter(data, batch_size, shuffle=True, noise=1.0):
    
     """
    
     randomly permute data, then sort by source length, and partition into batches
    
     ensure that the length of  sentences in each batch
    
     """
    
  
    
     batched_data = []
    
     if shuffle:
    
     np.random.shuffle(data)
    
  
    
     lengths = [example[1] for example in data]
    
     noisy_lengths = [- (l + np.random.uniform(- noise, noise)) for l in lengths]
    
     sorted_indices = np.argsort(noisy_lengths).tolist()
    
     sorted_data = [data[i] for i in sorted_indices]
    
     else:
    
     sorted_data =data
    
     
    
     batched_data.extend(list(batch_slice(sorted_data, batch_size)))
    
  
    
     if shuffle:
    
     np.random.shuffle(batched_data)
    
  
    
     for batch in batched_data:
    
     yield batch
    
  
    
 # some function
    
 from sklearn.metrics import f1_score, precision_score, recall_score
    
  
    
  
    
 def get_score(y_ture, y_pred):
    
     y_ture = np.array(y_ture)
    
     y_pred = np.array(y_pred)
    
     f1 = f1_score(y_ture, y_pred, average='macro') 
    
     p = precision_score(y_ture, y_pred, average='macro') 
    
     r = recall_score(y_ture, y_pred, average='macro') 
    
  
    
     return str((reformat(p, 2), reformat(r, 2), reformat(f1, 2))), reformat(f1, 2)
    
  
    
  
    
 def reformat(num, n):
    
     return float(format(num, '0.' + str(n) + 'f'))
    
  
    
 # build trainer
    
  
    
 import time
    
 from sklearn.metrics import classification_report
    
  
    
 clip = 5.0
    
 epochs = 1
    
 early_stops = 3
    
 log_interval = 50
    
  
    
 test_batch_size = 16
    
 train_batch_size = 16
    
  
    
 save_model = './bert.bin'
    
 save_test = './bert.csv'
    
  
    
 class Trainer():
    
     def __init__(self, model, vocab):
    
     self.model = model
    
     self.report = True
    
     
    
     self.train_data = get_examples(train_data, model.word_encoder, vocab)
    
     self.batch_num = int(np.ceil(len(self.train_data) / float(train_batch_size)))
    
     self.dev_data = get_examples(dev_data, model.word_encoder, vocab)
    
     self.test_data = get_examples(test_data, model.word_encoder, vocab)
    
  
    
     # criterion
    
     self.criterion = nn.CrossEntropyLoss()
    
  
    
     # label name
    
     self.target_names = vocab.target_names
    
  
    
     # optimizer
    
     self.optimizer = Optimizer(model.all_parameters, steps=self.batch_num * epochs)
    
  
    
     # count
    
     self.step = 0
    
     self.early_stop = -1
    
     self.best_train_f1, self.best_dev_f1 = 0, 0
    
     self.last_epoch = epochs
    
  
    
     def train(self):
    
     logging.info('Start training...')
    
     for epoch in range(1, epochs + 1):
    
         train_f1 = self._train(epoch)
    
  
    
         dev_f1 = self._eval(epoch)
    
  
    
         if self.best_dev_f1 <= dev_f1:
    
             logging.info(
    
                 "Exceed history dev = %.2f, current dev = %.2f" % (self.best_dev_f1, dev_f1))
    
             torch.save(self.model.state_dict(), save_model)
    
  
    
             self.best_train_f1 = train_f1
    
             self.best_dev_f1 = dev_f1
    
             self.early_stop = 0
    
         else:
    
             self.early_stop += 1
    
             if self.early_stop == early_stops:
    
                 logging.info(
    
                     "Eearly stop in epoch %d, best train: %.2f, dev: %.2f" % (
    
                         epoch - early_stops, self.best_train_f1, self.best_dev_f1))
    
                 self.last_epoch = epoch
    
                 break
    
     def test(self):
    
     self.model.load_state_dict(torch.load(save_model))
    
     self._eval(self.last_epoch + 1, test=True)
    
  
    
     def _train(self, epoch):
    
     self.optimizer.zero_grad()
    
     self.model.train()
    
  
    
     start_time = time.time()
    
     epoch_start_time = time.time()
    
     overall_losses = 0
    
     losses = 0
    
     batch_idx = 1
    
     y_pred = []
    
     y_true = []
    
     for batch_data in data_iter(self.train_data, train_batch_size, shuffle=True):
    
         torch.cuda.empty_cache()
    
         batch_inputs, batch_labels = self.batch2tensor(batch_data)
    
         batch_outputs = self.model(batch_inputs)
    
         loss = self.criterion(batch_outputs, batch_labels)
    
         loss.backward()
    
  
    
         loss_value = loss.detach().cpu().item()
    
         losses += loss_value
    
         overall_losses += loss_value
    
  
    
         y_pred.extend(torch.max(batch_outputs, dim=1)[1].cpu().numpy().tolist())
    
         y_true.extend(batch_labels.cpu().numpy().tolist())
    
  
    
         nn.utils.clip_grad_norm_(self.optimizer.all_params, max_norm=clip)
    
         for optimizer, scheduler in zip(self.optimizer.optims, self.optimizer.schedulers):
    
             optimizer.step()
    
             scheduler.step()
    
         self.optimizer.zero_grad()
    
  
    
         self.step += 1
    
  
    
         if batch_idx % log_interval == 0:
    
             elapsed = time.time() - start_time
    
  
    
             lrs = self.optimizer.get_lr()
    
             logging.info(
    
                 '| epoch {:3d} | step {:3d} | batch {:3d}/{:3d} | lr{} | loss {:.4f} | s/batch {:.2f}'.format(
    
                     epoch, self.step, batch_idx, self.batch_num, lrs,
    
                     losses / log_interval,
    
                     elapsed / log_interval))
    
  
    
             losses = 0
    
             start_time = time.time()
    
  
    
         batch_idx += 1
    
  
    
     overall_losses /= self.batch_num
    
     during_time = time.time() - epoch_start_time
    
  
    
     # reformat
    
     overall_losses = reformat(overall_losses, 4)
    
     score, f1 = get_score(y_true, y_pred)
    
  
    
     logging.info(
    
         '| epoch {:3d} | score {} | f1 {} | loss {:.4f} | time {:.2f}'.format(epoch, score, f1,
    
                                                                               overall_losses,
    
                                                                               during_time))
    
     if set(y_true) == set(y_pred) and self.report:
    
         report = classification_report(y_true, y_pred, digits=4, target_names=self.target_names)
    
         logging.info('n' + report)
    
  
    
     return f1
    
  
    
     def _eval(self, epoch, test=False):
    
     self.model.eval()
    
     start_time = time.time()
    
     data = self.test_data if test else self.dev_data
    
     y_pred = []
    
     y_true = []
    
     with torch.no_grad():
    
         for batch_data in data_iter(data, test_batch_size, shuffle=False):
    
             torch.cuda.empty_cache()
    
             batch_inputs, batch_labels = self.batch2tensor(batch_data)
    
             batch_outputs = self.model(batch_inputs)
    
             y_pred.extend(torch.max(batch_outputs, dim=1)[1].cpu().numpy().tolist())
    
             y_true.extend(batch_labels.cpu().numpy().tolist())
    
  
    
         score, f1 = get_score(y_true, y_pred)
    
  
    
         during_time = time.time() - start_time
    
         
    
         if test:
    
             df = pd.DataFrame({'label': y_pred})
    
             df.to_csv(save_test, index=False, sep=',')
    
         else:
    
             logging.info(
    
                 '| epoch {:3d} | dev | score {} | f1 {} | time {:.2f}'.format(epoch, score, f1,
    
                                                                           during_time))
    
             if set(y_true) == set(y_pred) and self.report:
    
                 report = classification_report(y_true, y_pred, digits=4, target_names=self.target_names)
    
                 logging.info('n' + report)
    
  
    
     return f1
    
  
    
     def batch2tensor(self, batch_data):
    
     '''
    
         [[label, doc_len, [[sent_len, [sent_id0, ...], [sent_id1, ...]], ...]]
    
     '''
    
     batch_size = len(batch_data)
    
     doc_labels = []
    
     doc_lens = []
    
     doc_max_sent_len = []
    
     for doc_data in batch_data:
    
         doc_labels.append(doc_data[0])
    
         doc_lens.append(doc_data[1])
    
         sent_lens = [sent_data[0] for sent_data in doc_data[2]]
    
         max_sent_len = max(sent_lens)
    
         doc_max_sent_len.append(max_sent_len)
    
  
    
     max_doc_len = max(doc_lens)
    
     max_sent_len = max(doc_max_sent_len)
    
  
    
     batch_inputs1 = torch.zeros((batch_size, max_doc_len, max_sent_len), dtype=torch.int64)
    
     batch_inputs2 = torch.zeros((batch_size, max_doc_len, max_sent_len), dtype=torch.int64)
    
     batch_masks = torch.zeros((batch_size, max_doc_len, max_sent_len), dtype=torch.float32)
    
     batch_labels = torch.LongTensor(doc_labels)
    
  
    
     for b in range(batch_size):
    
         for sent_idx in range(doc_lens[b]):
    
             sent_data = batch_data[b][2][sent_idx]
    
             for word_idx in range(sent_data[0]):
    
                 batch_inputs1[b, sent_idx, word_idx] = sent_data[1][word_idx]
    
                 batch_inputs2[b, sent_idx, word_idx] = sent_data[2][word_idx]
    
                 batch_masks[b, sent_idx, word_idx] = 1
    
  
    
     if use_cuda:
    
         batch_inputs1 = batch_inputs1.to(device)
    
         batch_inputs2 = batch_inputs2.to(device)
    
         batch_masks = batch_masks.to(device)
    
         batch_labels = batch_labels.to(device)
    
  
    
     return (batch_inputs1, batch_inputs2, batch_masks), batch_labels
    
  
    
 # train
    
 trainer = Trainer(model, vocab)
    
 trainer.train()
    
  
    
 # test
    
 trainer.test()

全部评论 (0)

还没有任何评论哟~