精华内容
下载资源
问答
  • 数据集下载 - ICDAR 2019多语言场景文本检测识别的强大阅读挑战 在训练数据集下方下载每个任务的相关基础事实。 您可能还想阅读本页末尾的“常见问题解答”部分。 任务1:多脚本文本检测 训练集: 训练集由10...

    数据集下载 - ICDAR 2019多语言场景文本检测和识别的强大阅读挑战

    在训练数据集下方下载每个任务的相关基础事实。

    您可能还想阅读本页末尾的“常见问题解答”部分。

    任务1:多脚本文本检测

    训练集:

    训练集由10,000个图像组成,可以从以下2个链接下载:

    TrainSetImagesTask1_Part1  (3.5G)

    TrainSetImagesTask1_Part2  (3.3G)

    基本事实由10,000个文本文件(对应于图像)组成,具有文字级本地化,脚本和转录,可以从以下链接下载:

    TrainSetGT(6.5M)

    请注意,此任务仅需要本地化结果(如任务页面中的结果格式所示),但基本事实还提供每个边界框和转录的脚本ID。任务3和4中将需要此额外信息。

    有关培训集的额外信息(对于专注于一种或几种语言的研究人员而言,可能非常有用,而不是所有的多语言集合):

    10,000个图像在训练集中排序,使得:每个连续的1000个图像包含一种主要语言的文本(当然它可以包含来自1种或2种其他语言的附加文本,全部来自10种语言的集合)
    00001 - 01000 :Arabic 
    01001 - 02000:English 
    02001 - 03000:French 
    03001 - 04000:Chinese 
    04001 - 05000:German 
    05001 - 06000:Korean 
    06001 - 07000:Japanese 
    07001 - 08000:Italian 
    08001 - 09000:Bangla 
    09001 - 10000:Hindi

    测试集: 

    图像(10,000张图像):

    MLT19_TestImagesPart1.zip

    MLT19_TestImagesPart2.zip

    任务2:裁剪的Word脚本标识

    训练集:

    Word_Images_Part1  (单词图像[2个文件]的基本事实也在这里[与图像在同一个文件夹中])

    Word_Images_Part2

    Word_Images_Part3

    测试集: 

    裁剪的文字图片:

    MLT19_images_task2.zip

    任务3:联合文本检测和脚本识别

    训练集:

    与任务1中相同的训练集和基本事实(参见上面的任务1)。

    测试集: 任务1的相同测试集。

    任务-4:端到端文本检测和识别

    训练集:它有两部分:

    1. 真实数据集:与任务1中相同的训练集和基本事实(参见上面的任务1)。
    2. 合成数据集:我们提供一个合成数据集,根据脚本匹配真实数据集,以帮助完成此任务的培训:

    请注意,我们为此任务提供了基准方法:  E2E-MLT。您可以在以下位置找到该方法的详细信息以及合成数据集:

    E2E-MLT - 一种用于多语言场景文本的无约束端到端方法:https//arxiv.org/abs/1801.09919

    测试集:任务1的相同测试集。

    使用条款

    “多语言场景文本检测和脚本识别(MLT)”数据集和相应的注释根据  知识共享署名4.0许可进行许可

    经常问的问题:

    问:如何为任务1,3和4完成排名/评估:

    对于任务1:  
    排名基于f-measure(Hmean)[NOT average precision],并在最后使用方法的平均召回和平均精度计算,其中:
    methodRecall =匹配数/边界框数GT(正确检测到框时匹配的位置)
    methodPrecision = 检测方法中的匹配数/数字边界框
    _Hanan = 2 * methodRecall * methodPrecision /(methodRecall + methodPrecision)

    不会为每个图像单独计算召回率,精度和f度量。它们是基于所有图像中检测到的框计算的(当然,这些框是按图像匹配/处理的)。有一个混乱,因为在MLT-2017的论文中,描述评估协议时出现了错误(在论文中,提到f-measure是按照图像计算的,然后在图像中取平均值 - 这不是我们做了什么)

    对于任务3:除“匹配”的定义外,具有相同的排名和评估。当正确检测到框并且具有正确的脚本标识时,将计算匹配。

    对于任务4:相同的排名和评估,但是当正确检测和识别框时计算匹配。额外信息:1)识别度量是编辑距离,2)包含未出现在列车组中的字符的测试集词将被设置为“不关心”检测和识别。这意味着无论您是否正确检测到它们,或者是否正确识别它们,它们都不会计入评估中。

     

    ============

     

     

    任务 - ICDAR 2019对多语言场景文本检测和识别的强大阅读挑战

    为了参加RRC-MLT-2019挑战,您必须参加至少一项任务。这是任务的描述。前三项任务类似于RRC-MLT-2017中的任务,但它们为RRC-MLT-2019重新开放,为数据集添加了新语言,并提高了整个数据集的基础事实质量。我们还在End-2-End文本检测和识别方面引入了新的第四项任务。

    任务1:多脚本文本检测

    在此任务中,参与者方法应该能够概括为检测不同脚本的文本。此任务的输入是具有各种语言的嵌入文本的场景图像,并且所需的检测是在单词级别。

    地面真相(GT)格式

    注意:为此任务提供的GT包含的信息多于此任务所需的信息,因为此GT也与任务3和4共享。因此,请确保您的方法生成的结果格式如“结果格式”段落中所述。

    根据单词边界框提供了基本事实。边界框不是面向轴的,它们由四个角的坐标以时钟方式指定。对于训练集中的每个图像,按照命名约定提供相应的UTF-8编码文本文件:

    gt_ [图片名称] .txt

    文本文件是逗号分隔文件,其中每行对应于图像中的一个文本块,并以下列格式给出其边界框坐标(四个角,顺时针),其脚本及其转录:

    X1,Y1,X2,Y2,X3,Y3,X4,Y4,脚本,转录

    有效的脚本是:“阿拉伯语”,“拉丁语”,“中文”,“日语”,“韩语”,“孟加拉语”,“印地语”,“符号”,“混合”,“无”

    请注意,转录是第9个逗号后面的任何内容,直到行尾。不使用转义字符。

    如果转录被提供为“###”,则文本块(单词)被认为是“不关心”。一些“不关心”单词具有与语言对应的脚本类,而其他单词具有“无”脚本类。后一种情况是由于低分辨率或其他失真而无法识别单词脚本。

    结果格式

    预期本地化(检测)结果如下:预期每个测试图像一个UTF-8编码的文本文件。要求参与者在单个zip文件中提交所有结果。结果文件应按照命名约定以测试图像ID命名:

    res_ [图像名称] .txt 

    (例如res_1245.txt)。每行应对应图像中的一个单词,并提供其边界框坐标(四个角,顺时针)和格式的置信度分数:

    X1,Y1,X2,Y2,X3,Y3,X4,Y4,信心

    评估

    f-measure(Hmean)用作对参与者方法进行排名的度量。标准f-度量基于检测到的单词边界框的回忆和精确度与基础事实相比较。如果检测到的边界框与GT框具有超过50%的重叠(交叉结合),则认为检测是正确的(真正的正)。有关如何计算得分的详细信息,请参见本文第III-B节:MLT2017

    问题:任务1和3:如果我们检测到“不关心”框(转录为“####”),将如何评估?
    答:“不关心”的盒子不计入评价。这意味着检测或丢失不关心框不会影响您的最终得分。

    任务-2:裁剪的Word脚本标识

    我们的数据集图像中的文本以10种不同的语言显示,其中一些语言共享相同的脚本。此外,标点符号和一些数学符号有时会显示为单独的单词,这些单词会被分配一个名为“符号”的特殊脚本类。因此,我们共有8个不同的脚本。我们已经为此任务排除了具有“混合”脚本的单词。我们也排除了所有“不关心”字样,无论它们是否有识别的脚本。

    地面真相格式

    对于单词脚本识别任务,我们将数据集中的所有单词(裁剪单词)作为单独的图像文件提供,以及相应的地面真实脚本和转录。转录不用于此任务,可以忽略。对于每个文本块,提供了紧密包含文本块的面向轴的区域。

    所有单词的脚本和转录都在整个集合的SINGLE UTF-8文本文件中提供。地面实况文件中的每一行都具有以下格式

    [字图像名称],脚本,转录

    请注意,转录是第二个逗号之后的任何内容,直到行尾。不使用转义字符。有效的脚本是“阿拉伯语”,“拉丁语”,“中文”,“日语”,“韩语”,“孟加拉语”,“印地语”,“符号”。

    此外,我们提供有关从中提取单词图像的原始图像的信息,如下所示:定义切出的文本块图像中的文本块的(非轴定向)边界框的相对坐标是在整个集合的单独的SINGLE文本文件中提供。文本块的坐标是参考切出框给出的,作为时钟方式的边界框的四个角。地面实况文件中的每一行都具有以下格式。

    [字图像名称],x1,y1,x2,y2,x3,y3,x4,y4,[原始图像名称]

    结果格式

    参与者方法应该提供每个图像的脚本,其中每个输入图像是裁剪的单词图像(来自场景图像的剪切文本块)。请求每个图像一个脚本名称。所有输出脚本应该使用以下格式列在单个UTF-8编码的文本文件中,每个单词图像一个脚本:

    [word图像名称],脚本

    评估

    结果与地面实况的评估以下列方式计算:参与者为每个单词图像提供脚本ID,如果结果正确,则递增正确结果的计数。对给定方法的最终评估是这种预测的准确性。这可以通过以下简单定义进行总结:

    设G = {g1,g2 ,. 。。,gi ,. 。。,gm}是基础事实中正确的脚本类的集合,并且T = {t1,t2,.... 。。,ti ,. 。。,tm}是给定方法返回的一组脚本类,其中gi和ti引用相同的原始图像。每个单词的脚本标识被计为正确(一个)如果gi = ti,否则它是假(零),所有m个标识的总和除以m给出该任务的总体准确度。

    任务-3:联合文本检测和脚本识别

    此任务结合了多脚本文本识别所需的所有准备步骤。参与者方法应该将完整的场景图像作为输入,然后找到所有单词的边界框,并根据脚本id找到关于每个单词的信息。

    地面真相格式

    基本事实的格式与任务1中的相同。

    结果格式

    应在单个zip文件中提供联合检测和脚本识别结果。期望每个图像的文本文件。应使用以下命名约定在测试映像ID后面命名该文件:

    res_ [图像名称] .txt 

    在每个文本文件中,应提供检测到的边界框坐标列表(四个角,顺时针),以及检测和脚本类的置信度:

    X1,Y1,X2,Y2,X3,Y3,X4,Y4,信心,脚本

    评估

    此任务的评估是文本框的正确本地化(检测)正确的脚本分类的级联。如果根据任务1的评估标准正确检测到单词边界框,并且正如在任务2中正确识别该正确检测到的单词的脚本,则将该单词的联合检测和脚本识别计为正确。

    任务-4:端到端文本检测和识别

    对于多语言的统一OCR来说,这是一项非常具有挑战性的任务。多语言设置中的端到端场景文本检测和识别任务与其英语对应物一致。给定输入场景图像,目标是定位一组边界框及其对应的转录。

    地面真相格式

    基本事实的格式与任务1中的相同。

    结果格式

    联合检测和识别结果应在单个zip文件中提供。期望每个图像的文本文件。应使用以下命名约定在测试映像ID后面命名该文件:

    res_ [图像名称] .txt 

    在每个文本文件中,应提供检测到的边界框坐标列表(四个角,顺时针),以及检测的转录:

    X1,Y1,X2,Y2,X3,Y3,X4,Y4,信心,转录

    评估

    该任务的评估是文本框的正确定位(检测)  正确识别(单词转录)的级联  。如果根据任务1的评估标准正确检测到单词边界框,并且正确识别该正确检测到的单词的转录(根据编辑距离测量),则将该单词的联合检测和识别计为正确。

    测试集中包含未出现在训练集中的字符的所有单词将被设置为“不关心”,因此无论是否通过您的方法正确检测/识别它们,都不会影响评估,他们根本不计算在内。这意味着您可以根据训练集的词典进行训练。

    展开全文
  • 自然语言处理实战:新闻文本分类(附代码)

    千次阅读 多人点赞 2020-08-13 18:06:15
    自然语言处理实战:新闻文本分类 ——本文比赛来源于天池零基础入门NLP - 新闻文本分类。 目录自然语言处理实战:新闻文本分类一、赛题理解1、学习目标2、赛题数据3、数据标签4、评测指标5、数据读取6、解题思路二...

    自然语言处理实战:新闻文本分类

    ——本文比赛来源于天池零基础入门NLP - 新闻文本分类

    一、赛题理解

      本章将会对新闻文本分类进行赛题讲解,对赛题数据进行说明,并给出解题思路。

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

    1、学习目标

    • 理解赛题背景与赛题数据
    • 完成赛题报名和数据下载,理解赛题的解题思路

    2、赛题数据

      赛题以匿名处理后的新闻数据为赛题数据,数据集报名后可见并可下载。赛题数据为新闻文本,并按照字符级别进行匿名处理。整合划分出14个候选分类类别:财经、彩票、房产、股票、家居、教育、科技、社会、时尚、时政、体育、星座、游戏、娱乐的文本数据。
      赛题数据由以下几个部分构成:训练集20w条样本,测试集A包括5w条样本,测试集B包括5w条样本。为了预防选手人工标注测试集的情况,我们将比赛数据的文本按照字符级别进行了匿名处理。

    3、数据标签

      处理后的赛题训练数据如下:

    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}

    4、评测指标

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

    5、数据读取

      使用Pandas库完成数据读取操作,并对赛题数据进行分析。

    6、解题思路

      赛题思路分析:赛题本质是一个文本分类问题,需要根据每句的字符进行分类。但赛题给出的数据是匿名化的,不能直接使用中文分词等操作,这个是赛题的难点。
      因此本次赛题的难点是需要对匿名字符进行建模,进而完成文本分类的过程。由于文本数据是一种典型的非结构化数据,因此可能涉及到特征提取和分类模型两个部分。为了减低参赛难度,我们提供了一些解题思路供大家参考:

    • 思路1TF-IDF + 机器学习分类器
        直接使用TF-IDF对文本提取特征,并使用分类器进行分类。在分类器的选择上,可以使用SVM、LR、或者XGBoost。
    • 思路2FastText
        FastText是入门款的词向量,利用Facebook提供的FastText工具,可以快速构建出分类器。
    • 思路3WordVec + 深度学习分类器
        WordVec是进阶款的词向量,并通过构建深度学习分类完成分类。深度学习分类的网络结构可以选择TextCNN、TextRNN或者BiLSTM。
    • 思路4Bert词向量
        Bert是高配款的词向量,具有强大的建模学习能力。

    二、数据读取与数据分析

      在上一章节,我们给大家简单介绍了赛题的内容和几种解决方案。从本章开始我们将会逐渐带着大家使用思路1到思路4来完成本次赛题。在讲解工具使用的同时,我们还会讲解一些算法的原理和相关知识点,并会给出一定的参考文献供大家深入学习。

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

    1、学习目标

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

    2、数据读取

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

    import pandas as pd
    
    train_df = pd.read_csv('./data/train_set.csv', sep='\t', nrows=100)
    print(train_df.head())
    

      这里的read_csv由三部分构成:

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

    在这里插入图片描述

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

    3、数据分析

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

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

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

    3.1句子长度分析

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

    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

    在这里插入图片描述

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

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

    _ = plt.hist(train_df['text_len'], bins=200)
    plt.xlabel('Text char count')
    plt.title("Histogram of char count")
    plt.savefig('./text_chart_count.png')
    plt.show()
    

    在这里插入图片描述

    3.2新闻类别分布

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

    train_df['label'].value_counts().plot(kind='bar')
    plt.title('News class count')
    plt.xlabel("category")
    plt.savefig('./category.png')
    plt.show()
    

    在这里插入图片描述

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

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

    3.3字符分布统计

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

    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): ", len(word_count)) # 6869
    print("word_count[0]: ", word_count[0]) # ('3750', 7482224)
    print("word_count[-1]: ", 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]: ", word_count[0]) # ('3750', 197997)
    print("word_count[1]: ", word_count[1]) # ('900', 197653)
    print("word_count[2]: ", word_count[2]) # ('648', 191975)
    

    在这里插入图片描述

    4、数据分析的结论

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

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

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

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

    5、本章小结

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

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

      在上一章节,我们对赛题的数据进行了读取,在本章我们将使用传统机器学习算法来完成新闻分类的过程,将会结束到赛题的核心知识点。

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

    1、学习目标

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

    2、机器学习模型

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

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

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

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

    在这里插入图片描述

    3、文本表示方法 Part1

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

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

    3.1One-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]
    

    3.2Bag of Words

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

    句子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来实现这一步骤:

    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()
    

    3.3N-gram

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

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

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

    3.4TF-IDF

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

    TF(t)= 该词语在当前文档出现的次数 / 当前文档中词语的总数
    
    IDF(t)= log_e(文档总数 / 出现该词语的文档总数)
    

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

      接下来我们将对比不同文本表示算法的精度,通过本地构建验证集计算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
    

    在这里插入图片描述

      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
    

    在这里插入图片描述

    5、本章小结

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

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

      在上一章节,我们使用传统机器学习算法来解决了文本分类问题,从本章开始我们将尝试使用深度学习方法。

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

    1、学习目标

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

    2、文本表示方法 Part2

    2.1现有文本表示方法的缺陷

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

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

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

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

    2.2FastText

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

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

    在这里插入图片描述

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

    在这里插入图片描述

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

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

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

    Bag of Tricks for Efficient Text Classification

    3、基于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安装。

    • 分类模型
    # 基于FastText的文本分类
    import pandas as pd
    from sklearn.metrics import f1_score
    import fasttext
    
    # 转换为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')
    
    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左右

    4、如何使用验证集调参

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

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

    在这里插入图片描述

      这里我们使用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的作为训练集,然后基于验证集的结果调整超参数,使得模型性能更优。

    5、本章小结

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

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

      在上一章节,我们通过FastText快速实现了基于深度学习的文本分类模型,但是这个模型并不是最优的。在本章我们将继续深入。
    本章将继续学习基于深度学习的文本分类。

    1、学习目标

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

    2、文本表示方法 Part3

    2.1词向量

      本节通过word2vec学习词向量。word2vec模型背后的基本思想是对出现在上下文环境里的词进行预测。对于每一条输入文本,我们选取一个上下文窗口和一个中心词,并基于这个中心词去预测窗口里其他词出现的概率。因此,word2vec模型可以方便地从新增语料中学习到新增词的向量表达,是一种高效的在线学习算法(online learning)

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

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

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

    Hierarchical softmax
    Negative sampling

      1)Skip-grams原理和网络结构

      Word2Vec模型中,主要有Skip-Gram和CBOW两种模型,从直观上理解,Skip-Gram是给定input word来预测上下文。而CBOW是给定上下文,来预测input word
    在这里插入图片描述

      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,方框内代表位于窗口内的单词。

    在这里插入图片描述
    在这里插入图片描述

      我们的模型将会从每对单词出现的次数中习得统计结果。例如,我们的神经网络可能会得到更多类似(“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”方法,这样每个训练样本的训练只会更新一小部分的模型权重,从而降低计算负担

      (1)Word pairs and "phases"

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

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

      (2)对高频词抽样

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

    在这里插入图片描述

      但是对于“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) 代表着保留某个单词的概率:
    在这里插入图片描述

      (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即可。一个单词的负采样概率越大,那么它在这个表中出现的次数就越多,它被选中的概率就越大。

      3) Hierarchical Softmax

      (1) 霍夫曼树

    输入:权值为(w1,w2,…wn)的n个节点
    输出:对应的霍夫曼树
    
    1. 将(w1,w2,…wn)看做是有n棵树的森林,每个树仅有一个节点
    2. 在森林中选择根节点权值最小的两棵树进行合并,得到一个新的树,这两颗树分布作为新树的左右子树。新树的根节点权重为左右子树的根节点权重之和
    3. 将之前的根节点权值最小的两棵树从森林删除,并把新树加入森林
    4. 重复步骤 2 和 3 直到森林里只有一棵树为止

      下面我们用一个具体的例子来说明霍夫曼树建立的过程,我们有(a,b,c,d,e,f)共6个节点,节点的权值分布是(16,4,8,6,20,3)。

      首先是最小的b和f合并,得到的新树根节点权重是7.此时森林里5棵树,根节点权重分别是16,8,6,20,7。此时根节点权重最小的6,7合并,得到新子树,依次类推,最终得到下面的霍夫曼树。
    在这里插入图片描述

      那么霍夫曼树有什么好处呢?一般得到霍夫曼树后我们会对叶子节点进行霍夫曼编码,由于权重高的叶子节点越靠近根节点,而权重低的叶子节点会远离根节点,这样我们的高权重节点编码值较短,而低权重值编码值较长。这保证的树的带权路径最短,也符合我们的信息论,即我们希望越常用的词拥有更短的编码。如何编码呢?一般对于一个霍夫曼树的节点(根节点除外),可以约定左子树编码为0,右子树编码为1。如上图,则可以得到c的编码是00。

      在word2vec中,约定编码方式和上面的例子相反,即约定左子树编码为1,右子树编码为0,同时约定左子树的权重不小于右子树的权重。

      更多原理可参考:霍夫曼树原理

      (2)Hierarchical Softmax过程

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

      霍夫曼树的建立:

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

    在这里插入图片描述

      如上图所示:

    在这里插入图片描述在这里插入图片描述

      注意:此时的theta是一个待定系数,它是由推导最大似然之后求解得到迭代式子。

    在这里插入图片描述

      使用gensim训练word2vec

    # 导入包
    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
    
    train_set = pd.read_csv(data_file, sep='\t', nrows=1000)
    train_set
    
    # data to fold
    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("./data/word2vec.bin")
    
    # 加载模型并格式化
    # load model
    model = Word2Vec.load("./data/word2vec.bin")
    
    # convert format
    model.wv.save_word2vec_format('./data/word2vec.txt', binary=False)
    

    在这里插入图片描述
    在这里插入图片描述

    2.2TextCNN

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

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

    2.3TextRNN

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

    在这里插入图片描述

    3、基于TextCNN、TextRNN的文本表示

    3.1TextCNN

    import numpy as np
    import torch
    import torch.nn as nn
    import torch.optim as optim
    from torch.autograd import Variable
    import torch.nn.functional as F
    
    dtype = torch.FloatTensor
    
    # Text-CNN Parameter
    embedding_size = 2 # n-gram
    sequence_length = 3
    num_classes = 2  # 0 or 1
    filter_sizes = [2, 2, 2] # n-gram window
    num_filters = 3
    
    # 3 words sentences (=sequence_length is 3)
    sentences = ["i love you", "he loves me", "she likes baseball", "i hate you", "sorry for that", "this is awful"]
    labels = [1, 1, 1, 0, 0, 0]  # 1 is good, 0 is not good.
    
    word_list = " ".join(sentences).split()
    word_list = list(set(word_list))
    word_dict = {w: i for i, w in enumerate(word_list)}
    vocab_size = len(word_dict)
    
    inputs = []
    for sen in sentences:
        inputs.append(np.asarray([word_dict[n] for n in sen.split()]))
    
    targets = []
    for out in labels:
        targets.append(out) # To using Torch Softmax Loss function
    
    input_batch = Variable(torch.LongTensor(inputs))
    target_batch = Variable(torch.LongTensor(targets))
    
    
    class TextCNN(nn.Module):
        def __init__(self):
            super(TextCNN, self).__init__()
    
            self.num_filters_total = num_filters * len(filter_sizes)
            self.W = nn.Parameter(torch.empty(vocab_size, embedding_size).uniform_(-1, 1)).type(dtype)
            self.Weight = nn.Parameter(torch.empty(self.num_filters_total, num_classes).uniform_(-1, 1)).type(dtype)
            self.Bias = nn.Parameter(0.1 * torch.ones([num_classes])).type(dtype)
    
        def forward(self, X):
            embedded_chars = self.W[X] # [batch_size, sequence_length, sequence_length]
            embedded_chars = embedded_chars.unsqueeze(1) # add channel(=1) [batch, channel(=1), sequence_length, embedding_size]
    
            pooled_outputs = []
            for filter_size in filter_sizes:
                # conv : [input_channel(=1), output_channel(=3), (filter_height, filter_width), bias_option]
                conv = nn.Conv2d(1, num_filters, (filter_size, embedding_size), bias=True)(embedded_chars)
                h = F.relu(conv)
                # mp : ((filter_height, filter_width))
                mp = nn.MaxPool2d((sequence_length - filter_size + 1, 1))
                # pooled : [batch_size(=6), output_height(=1), output_width(=1), output_channel(=3)]
                pooled = mp(h).permute(0, 3, 2, 1)
                pooled_outputs.append(pooled)
    
            h_pool = torch.cat(pooled_outputs, len(filter_sizes)) # [batch_size(=6), output_height(=1), output_width(=1), output_channel(=3) * 3]
            h_pool_flat = torch.reshape(h_pool, [-1, self.num_filters_total]) # [batch_size(=6), output_height * output_width * (output_channel * 3)]
    
            model = torch.mm(h_pool_flat, self.Weight) + self.Bias # [batch_size, num_classes]
            return model
    
    model = TextCNN()
    
    criterion = nn.CrossEntropyLoss()
    optimizer = optim.Adam(model.parameters(), lr=0.001)
    
    # Training
    for epoch in range(5000):
        optimizer.zero_grad()
        output = model(input_batch)
    
        # output : [batch_size, num_classes], target_batch : [batch_size] (LongTensor, not one-hot)
        loss = criterion(output, target_batch)
        if (epoch + 1) % 1000 == 0:
            print('Epoch:', '%04d' % (epoch + 1), 'cost =', '{:.6f}'.format(loss))
    
        loss.backward()
        optimizer.step()
    
    # Test
    test_text = 'sorry hate you'
    tests = [np.asarray([word_dict[n] for n in test_text.split()])]
    test_batch = Variable(torch.LongTensor(tests))
    
    # Predict
    predict = model(test_batch).data.max(1, keepdim=True)[1]
    if predict[0][0] == 0:
        print(test_text, "is Bad Mean...")
    else:
        print(test_text, "is Good Mean!!")
    

    在这里插入图片描述

    3.2TextRNN

    import numpy as np
    import torch
    import torch.nn as nn
    import torch.optim as optim
    from torch.autograd import Variable
    
    dtype = torch.FloatTensor
    
    sentences = [ "i like dog", "i love coffee", "i hate milk"]
    
    word_list = " ".join(sentences).split()
    word_list = list(set(word_list))
    word_dict = {w: i for i, w in enumerate(word_list)}
    number_dict = {i: w for i, w in enumerate(word_list)}
    n_class = len(word_dict)
    
    # TextRNN Parameter
    batch_size = len(sentences)
    n_step = 2 # number of cells(= number of Step)
    n_hidden = 5 # number of hidden units in one cell
    
    def make_batch(sentences):
        input_batch = []
        target_batch = []
    
        for sen in sentences:
            word = sen.split()
            input = [word_dict[n] for n in word[:-1]]
            target = word_dict[word[-1]]
    
            input_batch.append(np.eye(n_class)[input])
            target_batch.append(target)
    
        return input_batch, target_batch
    
    # to Torch.Tensor
    input_batch, target_batch = make_batch(sentences)
    input_batch = Variable(torch.Tensor(input_batch))
    target_batch = Variable(torch.LongTensor(target_batch))
    
    class TextRNN(nn.Module):
        def __init__(self):
            super(TextRNN, self).__init__()
    
            self.rnn = nn.RNN(input_size=n_class, hidden_size=n_hidden)
            self.W = nn.Parameter(torch.randn([n_hidden, n_class]).type(dtype))
            self.b = nn.Parameter(torch.randn([n_class]).type(dtype))
    
        def forward(self, hidden, X):
            X = X.transpose(0, 1) # X : [n_step, batch_size, n_class]
            outputs, hidden = self.rnn(X, hidden)
            # outputs : [n_step, batch_size, num_directions(=1) * n_hidden]
            # hidden : [num_layers(=1) * num_directions(=1), batch_size, n_hidden]
            outputs = outputs[-1] # [batch_size, num_directions(=1) * n_hidden]
            model = torch.mm(outputs, self.W) + self.b # model : [batch_size, n_class]
            return model
    
    model = TextRNN()
    
    criterion = nn.CrossEntropyLoss()
    optimizer = optim.Adam(model.parameters(), lr=0.001)
    
    # Training
    for epoch in range(5000):
        optimizer.zero_grad()
    
        # hidden : [num_layers * num_directions, batch, hidden_size]
        hidden = Variable(torch.zeros(1, batch_size, n_hidden))
        # input_batch : [batch_size, n_step, n_class]
        output = model(hidden, input_batch)
    
        # output : [batch_size, n_class], target_batch : [batch_size] (LongTensor, not one-hot)
        loss = criterion(output, target_batch)
        if (epoch + 1) % 1000 == 0:
            print('Epoch:', '%04d' % (epoch + 1), 'cost =', '{:.6f}'.format(loss))
    
        loss.backward()
        optimizer.step()
    
    input = [sen.split()[:2] for sen in sentences]
    
    # Predict
    hidden = Variable(torch.zeros(1, batch_size, n_hidden))
    predict = model(hidden, input_batch).data.max(1, keepdim=True)[1]
    print([sen.split()[:2] for sen in sentences], '->', [number_dict[n.item()] for n in predict.squeeze()])
    

    在这里插入图片描述

    4、使用HAN用于文本分类

      Hierarchical Attention Network for Document Classification(HAN)基于层级注意力,在单词和句子级别分别编码并基于注意力获得文档的表示,然后经过Softmax进行分类。其中word encoder的作用是获得句子的表示,可以替换为上节提到的TextCNN和TextRNN,也可以替换为下节中的BERT。

    在这里插入图片描述

    5、本章小结

      本章介绍了Word2Vec的使用,以及TextCNN、TextRNN的原理和训练,最后介绍了用于长文档分类的HAN

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

    1、学习目标

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

    2、文本表示方法Part4

    2.1Transformer原理

      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操作,将如下所示:

    在这里插入图片描述

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

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

    在这里插入图片描述

      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大的多。

    在这里插入图片描述

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

    在这里插入图片描述

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

    3、基于Bert的文本分类

    3.1Bert Pretrain

      预训练过程使用了Google基于Tensorflow发布的BERT源代码。首先从原始文本中创建训练数据,由于本次比赛的数据都是ID,这里重新建立了词表,并且建立了基于空格的分词器。

    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格式。

    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
    

      在预训练过程中,也只执行掩码语言模型任务,因此不再计算下一句预测任务的loss。

    (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模型,详细配置如下。

    {
      "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,因此需要将最后一个检查点转换成Pytorch的权重。

    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)
    

      预训练消耗的资源较大,硬件条件不允许的情况下建议直接下载开源的模型

    3.2Bert Finetune

    在这里插入图片描述

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

    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)
    

    4、本章小结

      本章介绍了Bert的原理和使用,具体包括pretrain和finetune两部分

    【github地址】

      源码和数据已经在Github项目中给出,网址为:暂定。

    【参考资料】

    1、https://github.com/datawhalechina/team-learning
    2、https://mp.weixin.qq.com/s/I-yeHQopTFdNk67Ir_iWiA
    3、https://github.com/hecongqing/2018-daguan-competition
    4、Task 2: Word Vectors and Word Senses (附代码)(Stanford CS224N NLP with Deep Learning Winter 2019)

    展开全文
  • 此笔记适用于对于机器学习Python应用有基本了解的人。本人环境是python3.6。本程序基于jupyter notebook。 目前现存很多NLP的技术与工具,但聚类分类永远是我们在面对这类问题时会首先考虑...

    本文是基于 Emmanuel Ameisen 的 Concrete solutions to real problems的学习笔记(翻译+整理+扩充),代码部分根据自己的环境进行了微改。此笔记适用于对于机器学习和Python应用有基本了解的人。本人环境是python3.6。本程序基于jupyter notebook。

    目前现存很多NLP的技术与工具,但聚类和分类永远是我们在面对这类问题时会首先考虑的手段。这两种方法不仅使用简单,而且可帮助企业在一定程度上快速的初步解决一些问题:

    • 怎样自动的区分不同类型的语句?
    • 怎样从数据中找出与给定语句意义相似的语句?
    • 怎样从语句中抽取并表示出相对完善而简明的语意以适用于一系列的各种应用?
    • 最重要的是,怎样快速判断这些应用在你的数据之上是能够实现的?

    下面作者给出文本内容分类的清晰简明的指南。一、二、部分介绍了文本分类,三部分简要介绍了文本生成。文中代码里均可直接复制按顺序运行,用到的模块如果没有需要自行下载。

    一、 数据

    数据来自于 Figure Eight 中的 Disasters on social media,其中包含了10000多条包含各种跟灾难有关的关键词的推特,但这些词同时也可能并不是形容灾难的(比如用来形容心情等等),我们的任务是通过自然语言处理来让计算机可以识别出这条推特是否是形容一场灾难(疾病爆发,恐怖袭击等),从而发出预警并提升相关部门的反应速度。

    1. 数据清理

    导入所需要的基本库:

    import keras
    import nltk
    import pandas as pd
    import numpy as np
    import re
    import codecs
    import warnings
    
    warnings.filterwarnings('ignore')
    

    从网上下载数据并把无法解码的地方用“?”代替并另存为’socialmedia_relevant_cols_clean.csv’:

    input_file = codecs.open('socialmedia-disaster-tweets-DFE.csv',"r",encoding='utf-8',errors='replace')
    output_file = open('socialmedia_relevant_cols_clean.csv','w', encoding='utf-8')
    
    def sanitize_characters(raw, clean):
        for line in input_file:
            output_file.write(line)
    sanitize_characters(input_file,output_file)
    

    留下我们需要的字段,“text” 和 “choose_one”,分别为推特的内容及标签。标签有三个,分别是“Not Relevant”,“Relevant”,以及“Can’t Decide”。分别为“不相关”,“相关”,以及“无法确定”。并新增一个“class_label”字段来用数字表示这三个标签,0表示不相关,1表示相关,2表示无法确定。

    questions = pd.read_csv('socialmedia_relevant_cols_clean.csv')
    questions = questions[['text', 'choose_one']]
    A = {'Not Relevant':0,'Relevant':1,"Can't Decide":2}
    for i in A:
        questions.loc[questions.choose_one==i,'class_label'] = int(A[i])
    questions.head()
    

    把网址(http及后面的东西)和“@”后面的东西(一般是用户名)删除掉,因为这些信息一般跟语义本身没什么关系。然后把“@”替换为 “at”,因为有人用“@”来表示“在…地方”。再把各种标点符号替换为空格以方便处理。最后把所有的内容都用小写表示(大小写对语义一般没什么影响)。把清理后的数据另存为’clean_data.csv’。

    def standardize_text(df, text_field):
        df[text_field] = df[text_field].apply(lambda x: re.sub(re.compile(r'http\S+'), '',x))
        df[text_field] = df[text_field].apply(lambda x: re.sub(re.compile(r'http'), '',x))
        df[text_field] = df[text_field].apply(lambda x: re.sub(re.compile(r'@\S+'), '',x))
        df[text_field] = df[text_field].apply(lambda x: re.sub(re.compile(r'@'), 'at',x))
        df[text_field] = df[text_field].apply(lambda x: re.sub(re.compile(r'[^A-Za-z0-9(),!?@\'\`\"\_\n]'), ' ',x))
        df[text_field] = df[text_field].str.lower()
        return df
    questions = standardize_text(questions,'text')
    questions.to_csv('clean_data.csv', encoding='utf-8')
    questions.head()
    

    2. 分词处理

    首先看一下数据的分布:

    clean_questions = pd.read_csv("clean_data.csv")
    clean_questions.groupby('class_label').count().head()
    

    结果如图:
    在这里插入图片描述可以看出数据偏斜不严重,“相关”和“不相关”两组几乎均衡。

    接下来是把句子转化为算法能够理解的形式。首先进行分词处理。

    新建一个分词字段显示推特内容分词处理后的结果:

    from nltk.tokenize import RegexpTokenizer
    tokenizer = RegexpTokenizer(r'\w+')
    clean_questions.loc[:,'tokens'] = clean_questions['text'].apply(tokenizer.tokenize)
    clean_questions.head()
    

    查看总词数及词典大小:

    from keras.preprocessing.text import Tokenizer
    from keras.preprocessing.sequence import pad_sequences
    from keras.utils import to_categorical
    
    all_words = [word for tokens in clean_questions['tokens'] for word in tokens]
    sentence_lengths = [len(tokens) for tokens in clean_questions['tokens']]
    VOCAB = sorted(list(set(all_words)))
    print("%s words total, with a vocabulary size of %s" % (len(all_words),len(VOCAB)))
    print("Max sentence length is %s" % max(sentence_lengths))
    

    输出结果:
    “154475 words total, with a vocabulary size of 18091
    Max sentence length is 34”

    查看语句长度分布:

    import matplotlib.pyplot as plt
    
    fig = plt.figure(figsize=(10, 10))
    plt.xlabel('Sentence length')
    plt.ylabel('Number of sentences')
    plt.hist(sentence_lengths)
    plt.show()
    

    在这里插入图片描述

    二、 算法

    现在数据已经清理干净并初步准备好,现在进入机器学习阶段。

    在图像处理中,机器学习算法的输入是原始像素,欺诈检测算法用的是用户的特征作为输入,那么自然语言处理用什么作为输入呢?

    一种自然而然的方法是给每一个字一个编码,但这种方法并不能很好地帮助算法去“理解”一个句子。我们的目标是找到一种合适的词嵌入方式去表示每一条推特,然后用这些嵌入来为每一条推特分类。

    1. bag of words counts

    我们从最简单的实现开始,bag of words即我们所说的词袋模型,它只是简单地把句子向量化,忽略其词序和语法,句法,将其仅仅看做是一个词集合,上面我们看到我们的字典长度是18091,则用0去表示所有的词,形成一个1*10891的向量,对于每一条推特,把出现的词的位置置1,这样把每一条推特进行向量化。

    首先把数据集分成训练集和测试集,测试集占总数据的百分之二十,再把训练集和测试集的输入部分(推特内容)做向量化。

    from sklearn.model_selection import train_test_split
    from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer
    
    def cv(data):
        count_vectorizer = CountVectorizer()
        emb = count_vectorizer.fit_transform(data)
        return emb, count_vectorizer
    
    list_corpus = clean_questions.text.tolist()
    list_labels = clean_questions.class_label.tolist()
    
    X_train, X_test, y_train, y_test = train_test_split(list_corpus, list_labels, test_size=0.2, random_state=3)
    X_train_counts, count_vectorizer = cv(X_train)
    X_test_counts = count_vectorizer.transform(X_test)
    

    现在我们对数据进行了基本的向量化,接下来我们试着把向量化后的数据可视化看看是否能够找到一定的规律。在最理想的情况下,我们希望不同类别的句子能够很好地区分,因为我们的向量维度接近两万,所以首先得把它降为2维再来做可视化。

    这里我们用TruncatedSVD来降维,找出数据集中的两个主题,用每一条推特在两个主题中的权重值来作为横纵坐标,把“不相关”标记为橘色,“相关”和“无法确定”都标记为蓝色:

    from sklearn.decomposition import TruncatedSVD
    import matplotlib
    import matplotlib.patches as mpatches
    
    def plot_LSA(test_data, test_labels, savepath='PCA_demo.csv', plot=True):
        lsa = TruncatedSVD(n_components=2)
        lsa.fit(test_data)
        lsa_scores = lsa.transform(test_data)
        colors = ['orange','blue', 'blue']
        if plot:
            plt.scatter(lsa_scores[:, 0], lsa_scores[:, 1], s=8, alpha=.8, c=test_labels, cmap=matplotlib.colors.ListedColormap(colors))
            red_patch = mpatches.Patch(color='orange', label='Not Relevant')
            green_patch = mpatches.Patch(color='blue', label='Relevant')
            plt.legend(handles=[red_patch, green_patch], prop={'size': 30})
            
    fig = plt.figure(figsize=(16, 16))
    plot_LSA(X_train_counts, y_train)
    plt.show()
    

    得到如下结果:
    在这里插入图片描述看起来两个颜色的推特都混在了一起,接下来我们看看机器学习算法是否能够把他们分类。这里选择最简单易行的逻辑回归算法。

    from sklearn.linear_model import LogisticRegression
    
    clf = LogisticRegression(C=30, class_weight='balanced', solver='newton-cg',
                            multi_class='multinomial', n_jobs=-1, random_state=40)
    clf.fit(X_train_counts, y_train)
    y_predicted_counts = clf.predict(X_test_counts)
    

    接下来看看这个模型在我的数据集上表现如何。

    from sklearn.metrics import accuracy_score, f1_score, precision_score, recall_score, classification_report
    
    def get_metrics(y_test, y_predicted):
        # true positives / (true positives + false positives)
        precision = precision_score(y_test, y_predicted, pos_label=None, average='weighted')
        # true positives / (true positives + false negatives)
        recall = recall_score(y_test, y_predicted, pos_label=None, average='weighted')
        # harmonic mean of precision and recall
        f1 = f1_score(y_test, y_predicted, pos_label=None, average='weighted')
        # (true positives + true negatives)/total
        accuracy = accuracy_score(y_test, y_predicted)
        return accuracy, precision, recall, f1
    accuracy, precision, recall, f1 = get_metrics(y_test, y_predicted_counts)
    print("accuracy = %0.3f, precision = %0.3f, recall= %0.3f, f1 = %0.3f" % (accuracy, precision, recall, f1))
    

    运行得到结果:
    accuracy = 0.779, precision = 0.778, recall= 0.779, f1 = 0.778

    看起来还可接受,但是为了更准确地做出决定,我们需要查看我们的模型究竟在什么地方犯了错。首先我们可以通过混淆矩阵来看一看。

    import numpy as np
    import itertools
    from sklearn.metrics import confusion_matrix
    
    def plot_confusion_matrix(cm, classes, 
                              normalize=False, 
                              title='Confusion matrix', 
                              cmap=plt.cm.Blues):
        if normalize:
            cm = cm.astype('float')/cm.sum(axis=1)[:, np.newaxis]
        plt.imshow(cm, interpolation='nearest', cmap=cmap)
        plt.title(title, fontsize=30)
        plt.colorbar()
        tick_marks = np.arange(len(classes)) 
        plt.xticks(tick_marks, classes, fontsize=20)
        plt.yticks(tick_marks, classes, fontsize=20)
        
        fmt = '.2f' if normalize else 'd'
        thresh = cm.max()/2.
        
        for i,j in itertools.product(range(cm.shape[0]), range(cm.shape[1])):
            plt.text(j, i, format(cm[i, j], fmt), horizontalalignment='center',
                    color='black' if cm[i, j] < thresh else 'white', fontsize=40)
            
            plt.tight_layout()
            plt.ylabel('True label', fontsize=30)
            plt.xlabel('Predicted label', fontsize=30)
            
        return plt
    
    cm = confusion_matrix(y_test, y_predicted_counts)
    fig = plt.figure(figsize=(10,10))
    plot = plot_confusion_matrix(cm, 
                                 classes= ['Not Relevant','Relevant',"Can't Decide"], 
                                 normalize=False,
                                 title='Confusion Matrix')
    plt.show()
    print(cm)
    

    结果如下:
    在这里插入图片描述
    看得出第三种分类并没有被预测对,这是正常的,因为他在总体中占比过小,这里忽略它对整体的影响并不大。比起假正例(预测为灾难实际无关)来说我们的模型生成了更多的假负例(预测为无关实际为灾难),根据实际情况来看这应是比较理想的,因为误警报可能会造成很大的资源浪费。

    除了混淆矩阵而外,我们还可以查看对我们的模型影响比较大的特征:

    def get_most_important_features(vectorizer, model, n=5):
        index_to_word = {v:k for k,v in vectorizer.vocabulary_.items()}
        
        # loop fpr each class
        classes = {}
        for class_index in range(model.coef_.shape[0]):
            word_importances = [(el, index_to_word[i]) for i, el in enumerate(model.coef_[class_index])]
            sorted_coeff = sorted(word_importances, key=lambda x: x[0], reverse=True)
            tops = sorted_coeff[:n]
            bottom = sorted_coeff[-n:]
            classes[class_index] = {'tops': tops,
                                   'bottom': bottom}
        return classes
    
    importance = get_most_important_features(count_vectorizer, clf, 10)
    
    def plot_important_words(top_scores, top_words, bottom_scores, bottom_words, name):
        y_pos = np.arange(len(top_words))
        top_pairs = [(a, b) for a,b in zip(top_words, top_scores)]
        top_pairs = sorted(top_pairs, key=lambda x: x[1])
        
        bottom_pairs = [(a, b) for a,b in zip(bottom_words, bottom_scores)]
        bottom_pairs = sorted(bottom_pairs, key=lambda x: x[1], reverse=True)
        
        top_words = [a[0] for a in top_pairs]
        top_scores = [a[1] for a in top_pairs]
        
        bottom_words = [a[0] for a in bottom_pairs]
        bottom_scores = [a[1] for a in bottom_pairs]
        
        fig = plt.figure(figsize=(10,10))
        
        plt.subplot(121)
        plt.barh(y_pos, bottom_scores, align='center', alpha=0.5)
        plt.title('Not Relevant',fontsize=20)
        plt.yticks(y_pos, bottom_words, fontsize=14)
        plt.suptitle('Key words', fontsize=16)
        plt.xlabel('Importance',fontsize=20)
        
        plt.subplot(122)
        plt.barh(y_pos, top_scores, align='center', alpha=0.5)
        plt.title('Relevant', fontsize=20)
        plt.yticks(y_pos, top_words, fontsize=14)
        plt.suptitle(name, fontsize=16)
        plt.xlabel('Importance', fontsize=20)
        
        plt.subplots_adjust(wspace=0.8)
        plt.show()
        
    top_scores = [a[0] for a in importance[1]['tops']]
    top_words = [a[1] for a in importance[1]['tops']]
    bottom_scores = [a[0] for a in importance[1]['bottom']]
    bottom_words = [a[1] for a in importance[1]['bottom']]
    
    plot_important_words(top_scores, top_words, bottom_scores, bottom_words, 
                         "Most important words for relevance")
    

    结果如下:
    在这里插入图片描述可以看出我们的模型确实找到了一些关键词,但是也有一些噪音混在其中(比如“heyoo”,“_”等),他们被选中可能因为他们在整个文档中的出镜频率都很高,甚至高过了关键词。

    2. TFIDF Bag of Words

    因为上述的问题,我们稍微调整一下词袋模型,考虑进TF-IDF(词频-逆文本频率指数),来过滤掉一些噪音,在这种方法下,词的重要性会随着它在样本中的出现次数增加,同时随着它在整个语料库中的出现次数降低。即如果一个词在一篇文章中频率很高,但在其他所有文章中频率比较低的时候他的重要性会更高。

    def tfidf(data):
            tfidf_vectorizer = TfidfVectorizer()
            train = tfidf_vectorizer.fit_transform(data)
            return train, tfidf_vectorizer
    
    X_train_tfidf, tfidf_vectorizer = tfidf(X_train)
    X_test_tfidf = tfidf_vectorizer.transform(X_test)
    

    同样的画出数据的降维分布图、混淆矩阵、以及特征重要程度。

    fig = plt.figure(figsize=(16,16))
    plot_LSA(X_train_tfidf, y_train)
    plt.show()
    
    clf_tfidf = LogisticRegression(C=30, class_weight='balanced', solver='newton-cg',
                                  multi_class='multinomial', n_jobs=-1, random_state=40)
    clf_tfidf.fit(X_train_tfidf, y_train)
    y_predicted_tfidf = clf_tfidf.predict(X_test_tfidf)
    accuracy_tfidf, precision_tfidf, recall_tfidf, f1_tfidf = get_metrics(y_test, y_predicted_tfidf)
    print("accuracy = %.3f, precision = %.3f, recall = %.3f, f1 = %.3f" % 
          (accuracy_tfidf, precision_tfidf, recall_tfidf, f1_tfidf))
    
    cm2 = confusion_matrix(y_test, y_predicted_tfidf)
    fig = plt.figure(figsize=(10, 10))
    plot = plot_confusion_matrix(cm2, classes=['Not Relevant','Relevant',"Can't Decide"], normalize=False,
                                title='Confusion Matrix')
    plt.show()
    print("TFIDF confusion mmatrix")
    print(cm2)
    print("BoW confusion matrix")
    print(cm)
    
    importance_tfidf = get_most_important_features(tfidf_vectorizer, clf_tfidf, 10)
    top_scores = [a[0] for a in importance_tfidf[1]['tops']]
    top_words = [a[1] for a in importance_tfidf[1]['tops']]
    bottom_scores = [a[0] for a in importance_tfidf[1]['bottom']]
    bottom_words = [a[1] for a in importance_tfidf[1]['bottom']]
    plot_important_words(top_scores, top_words, bottom_scores, bottom_words,
                         "Most important words for relevance")
    

    结果如下:
    在这里插入图片描述
    可以看出使用这种方法不同分类的样本区分度变得更好一些了。各项指标也略高于前一种方法。

    在这里插入图片描述
    从混淆矩阵来看这个模型的表现也微好过上一个模型。

    在这里插入图片描述
    从词的重要程度我们能看到,上一个模型中的噪声在一定程度上被过滤掉了一部分。选出来的词汇也与主题更相关一些了。

    3. word2vec

    上面的模型中我们从文本中取出了重要的词汇,从而判断文本的分类。但是不可能所有的重要词汇都包含在了我们的训练数据中,所以当有新的数据进来包含新的重要词汇的话用之前的模型就无法识别。于是我们需要引入词汇的语义,这意味着我们能够通过我们的词嵌入来识别近义词,反义词等等。

    Word2vec是一个预训练好的语料库,其中的词嵌入可以很好地表示语义相近的词汇,一个快速地表示一个文本的方法就是取文本中所含所有词汇word2vec值的均值。

    先去网上下载训练好的word2vec集“GoogleNews-vectors-negative300.bin.gz”,放入目录中。再在我们的数据上生成相应的词嵌入。

    import gensim
    
    word2vec_path = "E:\\study\\research\\NLP\\word2vec\\GoogleNews-vectors-negative300.bin.gz"
    word2vec = gensim.models.KeyedVectors.load_word2vec_format(word2vec_path, binary=True)
    
    def get_average_word2vec(tokens_list, vector, generate_missing=False, k=300):
        if len(tokens_list)<1:
            return np.zeros(k)
        if generate_missing:
            vectorized = [vector[word] if word in vector else np.random.rand(k) for word in tokens_list]
        else:
            vectorized = [vector[word] if word in vector else np.zeros(k) for word in tokens_list]
        length = len(vectorized)
        summed = np.sum(vectorized, axis=0)
        averaged = np.divide(summed,length)
        return averaged
    
    def get_word2vec_embeddings(vectors, clean_questions, generate_missing=False):
        embeddings = clean_questions['tokens'].apply(lambda x: get_average_word2vec(x, vectors,
                                                                    generate_missing=generate_missing))
        return list(embeddings)
    
    embeddings = get_word2vec_embeddings(word2vec, clean_questions)
    X_train_word2vec, X_test_word2vec, y_train_word2vec, y_test_word2vec = train_test_split(embeddings,
                                                                            list_labels, test_size=0.2,
                                                                            random_state=3)
    

    同样使用逻辑回归,看这种嵌入方式在在模型上的表现。

    fig = plt.figure(figsize=(16, 16))
    plot_LSA(embeddings, list_labels)
    plt.show()
    
    clf_w2v = LogisticRegression(C=30, class_weight='balanced', solver='newton-cg',
                                  multi_class='multinomial', n_jobs=-1, random_state=40)
    clf_w2v.fit(X_train_word2vec, y_train_word2vec)
    y_predicted_word2vec = clf_w2v.predict(X_test_word2vec)
    accuracy_w2v, precision_w2v, recall_w2v, f1_w2v = get_metrics(y_test_word2vec, y_predicted_word2vec)
    print("accuracy = %.3f, precision = %.3f, recall = %.3f, f1 = %.3f" % 
          (accuracy_w2v, precision_w2v, recall_w2v, f1_w2v ))
    
    cm_w2v = confusion_matrix(y_test_word2vec, y_predicted_word2vec)
    fig = plt.figure(figsize=(10, 10))
    plot = plot_confusion_matrix(cm_w2v, classes=['Not Relevant','Relevant',"Can't Decide"], normalize=False, title='Confusion matrix')
    plt.show()
    print("Word2Vec confusion matrix")
    print(cm_w2v)
    print("TFIDF confusion matrix")
    print(cm2)
    print("BoW confusion matrix")
    print(cm)
    

    结果如下:

    在这里插入图片描述在这里插入图片描述看得出各方面的表现都优于前面的两个模型。

    如果要再进一步查看我们的模型,比如像上面一样看看词在主题当中的重要程度,对于这一方法来说变得比较麻烦,因为每一个词都是用一个多维向量来表示,使模型很难直观判断其重要程度,但我们可以使用一个黑箱解释模型LIME来可视化各词在文本中产生的影响。

    from lime import lime_text
    from sklearn.pipeline import make_pipeline
    from lime.lime_text import LimeTextExplainer
    
    X_train_data, X_test_data, y_train_data, y_test_data = train_test_split(list_corpus, list_labels,
                                                                           test_size=0.2, random_state=40)
    vector_store = word2vec
    def word2vec_pipeline(examples):
        global vector_store
        tokenizer = RegexpTokenizer(r'\w+')
        tokenized_list = []
        for example in examples:
            example_tokens = tokenizer.tokenize(example)
            vectorized_example = get_average_word2vec(example_tokens, vector_store,generate_missing=False, k=300)
            tokenized_list.append(vectorized_example)
        return clf_w2v.predict_proba(tokenized_list)
    
    
    def explain_one_instance(instance, class_names):
        explainer = LimeTextExplainer(class_names=class_names)
        exp = explainer.explain_instance(instance, word2vec_pipeline, num_features=6)
        return exp
    
    def visualize_one_exp(features, labels, index, class_names = ['Not Relevant','Relevant',"Can't Decide"]):
        exp = explain_one_instance(features[index], class_names=class_names)
        print('Index:%d' % index)
        print('True class: %s' % class_names[int(labels[index])])
        exp.show_in_notebook(text=True)
        
        
    c = make_pipeline(count_vectorizer, clf)
    visualize_one_exp(X_test_data, y_test_data, 65)
    visualize_one_exp(X_test_data, y_test_data, 60)
    

    随机查看两段文本的解释如下:

    在这里插入图片描述绘出词的重要程度排行:

    import random
    from collections import defaultdict
    
    random.seed(40)
    
    def get_statistical_explanation(test_set, sample_size, word2vec_pipline, label_dict):
        sample_sentences = random.sample(test_set, sample_size)
        explainer = LimeTextExplainer()
        labels_to_sentences = defaultdict(list)
        contributors = defaultdict(dict)
        
        # first, find contributing words to each class
        for sentence in sample_sentences:
            probabilities = word2vec_pipeline([sentence])
            curr_label = probabilities[0].argmax()
            labels_to_sentences[curr_label].append(sentence)
            exp = explainer.explain_instance(sentence, word2vec_pipline, num_features=6, labels=[curr_label])
            listed_explanation = exp.as_list(label=curr_label)
            
            for word,contribution_weight in listed_explanation:
                if word in contributors[curr_label]:
                    contributors[curr_label][word].append(contribution_weight)
                else:
                    contributors[curr_label][word] = [contribution_weight]
                    
        # average each word's contribution to a class, and sort them by impact
        average_contributions = {}
        sorted_contributions = {}
        for label,lexica in contributors.items():
            curr_label = label
            curr_lexica = lexica
            average_contributions[curr_label] = pd.Series(index=curr_lexica.keys())
            for word,scores in curr_lexica.items():
                average_contributions[curr_label].loc[word] = np.sum(np.array(scores))/sample_size
            detractors = average_contributions[curr_label].sort_values()
            supporters = average_contributions[curr_label].sort_values(ascending=False)
            sorted_contributions[label_dict[curr_label]] = {'detractors': detractors,
                                                           'supporters': supporters}
        return sorted_contributions
            
    label_to_text = {
        0: 'Not Relevant',
        1: 'Relevant',
        2: "Can't Decide"
    }
    
    sorted_contributions = get_statistical_explanation(X_test_data, 100, word2vec_pipeline, label_to_text)
    
    # First index is the class
    # Second index is 0 for detractors, 1 for supporters
    # Third is how many words we sample
    top_words = sorted_contributions['Relevant']['supporters'][:10].index.tolist()
    top_scores = sorted_contributions['Relevant']['supporters'][:10].tolist()
    bottom_words = sorted_contributions['Relevant']['detractors'][:10].index.tolist()
    bottom_scores = sorted_contributions['Relevant']['detractors'][:10].tolist()
    
    plot_important_words(top_scores, top_words, bottom_scores, bottom_words, "Most important words for relevance")
    

    结果如下:
    在这里插入图片描述
    可以看出词语主题的相关性得到了更进一步的提升。

    4. CNN

    虽然模型考虑了语义后表现得更好了,但是word2vec算法彻底忽略掉了句子的结构,最后我们尝试一个更加复杂的模型,看看考虑进句子结构是否有帮助。

    这里我们会使用卷积神经网络(CNN)来做文本分类,虽然他不像循环神经网络(RNN)那么受欢迎,但是它也能得到很有竞争力的结果,而且训练很迅速,这对于这篇教程来说很合适。

    首先,用word2vec对我们的文本进行词嵌入。

    from keras.preprocessing.text import Tokenizer
    from keras.preprocessing.sequence import pad_sequences
    from keras.utils import to_categorical
    
    EMBEDDING_DIM = 300
    MAX_SEQUENCE_LENGTH = 35
    VOCAB_SIZE = len(VOCAB)
    VALIDATION_SPLIT = .2
    
    tokenizer = Tokenizer(num_words=VOCAB_SIZE)
    tokenizer.fit_on_texts(clean_questions['text'].tolist())
    sequences = tokenizer.texts_to_sequences(clean_questions['text'].tolist())
    word_index = tokenizer.word_index
    print("Found %s unique tokens." % len(word_index))
    
    
    cnn_data = pad_sequences(sequences, maxlen=MAX_SEQUENCE_LENGTH)
    labels = to_categorical(np.asarray(clean_questions['class_label']))
    
    indices = np.arange(cnn_data.shape[0])
    np.random.shuffle(indices)
    cnn_data = cnn_data[indices]
    labels = labels[indices]
    num_validation_samples = int(VALIDATION_SPLIT * cnn_data.shape[0])
    embedding_weights = np.zeros((len(word_index)+1, EMBEDDING_DIM))
    for word,index in word_index.items():
        embedding_weights[index, :] = word2vec[word] if word in word2vec else np.random.rand(EMBEDDING_DIM)
    print(embedding_weights.shape)
    

    输出:
    “Found 19092 unique tokens.
    (19093, 300)”

    接下来定义一个简单卷积神经网络。这里我们参考 Yoon Kim提出的模型,并做点小的改变。构建并训练我们的模型。

    from keras.layers import Dense, Input, Flatten, Dropout, concatenate
    from keras.layers import Conv1D, MaxPooling1D, Embedding
    from keras.layers import LSTM, Bidirectional
    from keras.models import Model
    
    def ConvNet(embeddings, max_sequence_length, num_words, embedding_dim, labels_index, trainable=False, extra_conv=True):
        embedding_layer = Embedding(num_words, embedding_dim, weights=[embeddings],
                                    input_length=max_sequence_length, trainable=trainable)
        sequence_input = Input(shape=(max_sequence_length,), dtype='int32')
        embedded_sequences = embedding_layer(sequence_input)
        
        #  Yoon Kim model (https://arxiv.org/abs/1408.5882)
        convs = []
        filter_sizes = [3, 4, 5]
        for filter_size in filter_sizes:
            l_conv = Conv1D(filters=128, kernel_size=filter_size, activation='relu')(embedded_sequences)
            l_pool = MaxPooling1D(pool_size=3)(l_conv)
            convs.append(l_pool)
        l_merge = concatenate(convs, axis=1)
        
        # add a 1D convnet with global maxpooling, instead of Yoon Kim model
        conv = Conv1D(filters=128, kernel_size=3, activation='relu')(embedded_sequences)
        pool = MaxPooling1D(pool_size=3)(conv)
        
        if extra_conv==True:
            x = Dropout(0.5)(l_merge)
        else:
            #Original Yoon Kim model
            x = Dropout(0.5)(pool)
        x = Flatten()(x)
        x = Dense(128, activation='relu')(x)
        # x = Dropout(0.5)(x)
        
        preds = Dense(labels_index, activation='softmax')(x)
        model = Model(sequence_input, preds)
        model.compile(loss='categorical_crossentropy',
                     optimizer='adam',
                     metrics=['acc'])
        return model,x
    
    x_train = cnn_data[:-num_validation_samples]
    y_train = labels[:-num_validation_samples]
    x_val = cnn_data[-num_validation_samples:]
    y_val = labels[-num_validation_samples:]
    
    model,x = ConvNet(embedding_weights, MAX_SEQUENCE_LENGTH, len(word_index)+1, EMBEDDING_DIM,
                   len(list(clean_questions["class_label"].unique())), False)
    print(model.summary())
    
    model.fit(x_train, y_train, validation_data=(x_val, y_val), epochs=3, batch_size=128)
    
    y_pred = model.predict(x_val)
    

    模型构造及效果如下:
    在这里插入图片描述我们画出他的混淆矩阵并与前三种比较,绘出分类效果:

    def to_numerical(y):
        y_tmp = y.round()
        y = []
        for i,j in enumerate(y_tmp):
            if j[0] == 1:
                y.append(0)
            elif j[1] == 1:
                y.append(1)
            else:
                y.append(2)
        return y
    
    y_pred = to_numerical(y_pred)
    y_val = to_numerical(y_val)
    cm_cnn = confusion_matrix(y_val, y_pred)
    fig = plt.figure(figsize=(10, 10))
    plot = plot_confusion_matrix(cm_cnn, classes=['Not Relevant','Relevant',"Can't Decide"], normalize=False, title='Confusion matrix')
    plt.show()
    print("CNN confusion matrix")
    print(cm_cnn)
    print("Word2Vec confusion matrix")
    print(cm_w2v)
    print("TFIDF confusion matrix")
    print(cm2)
    print("BoW confusion matrix")
    print(cm)
    
    dense1_layer_model = Model(inputs=model.input,outputs=model.get_layer('dense_2').output)
    x = dense1_layer_model.predict(x_val)
    fig = plt.figure(figsize=(16, 16))
    plot_LSA(x, y_val)
    plt.show()
    

    结果如下:
    在这里插入图片描述看的出这个模型倾向于判断出更少的假正例,所以在这一场景下也许在实际应用中这一模型会更加理想。

    在这里插入图片描述这个分类效果图是在模型的最后一层画出,可以看出两组数据的分离较之前其实更为明显了。

    三、文本生成

    接下来训练一个基于字节的文本生成模型。这一模型来自于ajmanser的项目详情可以上github去查看,按照指示准备相关文件。

    from keras import layers
    import sys
    
    # Dictionary mapping unique characters to their index in 'chars'
    text = open('seed_text.txt').read()
    chars = ['\n', ' ', '!', '"', '#', '$', '%', '&', "'", '(', ')', '*', ',', '-', '.', '/', '0', '1',
             '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?', '@', 'A', 'B', 'C',
             'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U',
             'V', 'W', 'X', 'Y', 'Z', '[', '\\', ']', '^', '_', '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g',
             'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 
             'z', '{', '|', '}', '~']
    char_indices = dict((char, chars.index(char)) for char in chars)
    maxlen = 60
    step = 1
    
    model = keras.models.Sequential()
    model.add(layers.LSTM(1024, input_shape=(maxlen, len(chars)), return_sequences=True))
    model.add(layers.LSTM(1024, input_shape=(maxlen, len(chars))))
    model.add(layers.Dense(len(chars), activation='softmax'))
    model.load_weights("pretrained-yelp.hdf5")
    optimizer = keras.optimizers.Adam(lr=0.0002)
    model.compile(loss='categorical_crossentropy', optimizer=optimizer)
    
    def sample(preds, temperature=1.0):
        preds = np.asarray(preds).astype('float64')
        preds = np.log(preds) / temperature
        exp_preds = np.exp(preds)
        preds = exp_preds / np.sum(exp_preds)
        probas = np.random.multinomial(1, preds, 1)
        return np.argmax(probas)
        
    def random_reviews():
        start_index = np.random.randint(0, len(text)-maxlen-1)
        generated_text = text[start_index: start_index + maxlen]
        print("Coming up with several reviews for you...")
        
        for temperature in [0.8]:
            sys.stdout.write(generated_text)
            #  we generate 600 characters
            for i in range(600):
                sampled = np.zeros((1, maxlen, len(chars)))
                for t,char in enumerate(generated_text):
                    sampled[0, t, char_indices[char]] = 1.
                    
                preds = model.predict(sampled, verbose=0)[0]
                next_index = sample(preds, temperature)
                next_char = chars[next_index]
    
                generated_text += next_char
                generated_text = generated_text[1:]
                
                sys.stdout.write(next_char)
                sys.stdout.flush()
                
            print(generated_text)
            
    random_reviews()    
    

    结果:
    在这里插入图片描述修改模型,使之能基于输入的关键词生成评价:

    # substitude food words in the generated reviews for ones from a pre-established list
    
    from nltk.corpus import wordnet as wn
    
    def food_related(nouns):
        food = wn.synset('food.n.01')
        final_list = []
        for word in nouns:
            temp = word
            word = word+'.n.01'
            try:
                if food.wup_similarity(wn.synset(word))>0.20 and temp!='food':
                    final_list.append(temp)
            except:
                pass
        return final_list
    
    def user_custom(foods):
        # enter foods as a string separated by commas. For example 'sushi, sashimi, maki'
        start_index = np.random.randint(0, len(text) - maxlen - 1)
        generated_text = text[start_index:start_index + maxlen]
        print('Coming up with two ideas for you...')
        final = generated_text+''
        for temperature in [0.8]:
            # generate 600 characters
             for i in range(1000):
                    sampled = np.zeros((1, maxlen, len(chars)))
                    for t, char in enumerate(generated_text):
                        sampled[0, t, char_indices[char]] = 1.
                    preds = model.predict(sampled, verbose=0)[0]
                    next_index = sample(preds, temperature)
                    next_char = chars[next_index]
                    final += next_char
                    
                    generated_text += next_char
                    generated_text = generated_text[1:]
        # print first review, then second via SOR/EOR
        temp = personalized_clean_up(final, foods)
        start = temp.find('SOR')
        stop = findStrAfterStr(temp, 'EOR', 'SOR')
        end_first = temp[start+4:stop]
        
        new = temp[get_second_index(temp, 'SOR')+4:]
        ending = new.find('EOR')
        print(temp[start+4:stop])
        print("")
        print(new[:ending])
        
    def personalized_clean_up(review,user_items):
        #take generic review, and replace with user generated words
        generic_nouns = review_to_nouns(review)
        food_generic = food_related(generic_nouns)
        
        user_picked_items = user_items.split(",")
        
        final = []
        for word in re.findall(r"[\w']+|[.,!?;]", review):
            if word in food_generic and len(user_picked_items)>1:
                word = np.random.choice(user_picked_items)
                final.append(word)
            else:
                final.append(word)
                
        new_review = " ".join(final)
        return re.sub(r'\s+([?.!","])', r'\1', new_review)
    
    def review_to_nouns(review):
        is_noun = lambda pos: pos[:2] == 'NN'
        token = nltk.word_tokenize(review)
        nouns = [word for (word, pos) in nltk.pos_tag(token) if is_noun(pos)]
        return nouns
    
    def findStrAfterStr(myString, searchText, afterText):
        after_index = myString.index(afterText)
        return myString.find(searchText, after_index)
    
    def get_second_index(input_string, sub_string):
        return input_string.index(sub_string, input_string.index(sub_string)+1)
    
    user_custom('burrito, taco, guac')
    

    输出结果如下:

    在这里插入图片描述

    四、总结

    本文梳理了NLP中对文本的几种预处理方式,包括BOW,TF-IDF BOW, Word2vec,整体走了一遍NLP文本分类的流程并对不同处理进行了比较。最后简单介绍了NLP文本生成模型的流程,细节处有待进一步探讨。

    展开全文
  • 文本分析 说明:一种使用自然语言处理来分析文本主体的工具。 在输入输入任何文章的URL,该文章将得到处理并提供分析返回。 使用的NLP API: :// www。意义cloud.com
  • 自然语言处理之文本标注问题

    万次阅读 2017-12-05 14:45:07
    文本标注 (tagging) 是一个监督学习问题,可以认为标注问题是分类问题的一个推广,标注问题又是更复杂的结构预测 (structure prediction) 问题的简单形式,标注问题的输入是一个观测序列,输出是一个标记序列护着...

    文本标注 (tagging) 是一个监督学习问题,可以认为标注问题是分类问题的一个推广,标注问题又是更复杂的结构预测 (structure prediction) 问题的简单形式,标注问题的输入是一个观测序列,输出是一个标记序列护着状态序列,标注问题的目标在于学习一个模型,使它能够对观测序列给出标记序列作为预测,注意的是可能的标记个数是有限的,但其组合所成的标记序列的个数是依序列长度呈指数级增长的。



    标注问题氛围学习和标注两个过程(如上图所示),首先给定一个训练数据集:


    在这里xi为输入观测序列 (一维向量),yi为相应的输出观测序列 (一维向量),每个输入观测序列向量的长度为n,对不同样本具有不一样的值,学习系统基于训练数据集构建一个模型,表示为条件概率分布:


    这里的每个xi(i=1,2,...,n)取值为所有可能的观测,每个Yi (i = 1,2..., n)取值为所有可能的标记,一般n远小于N,标注系统按照学习得到的条件概率分布模型,对新输入观测序列找到相应的输出标记序列。具体的对每一个观测序列,找到上式中概率最大的标记序列。


    评价标注模型的指标与评价分类模型的指标一样,常用的有标注准确率,精确率和召回率。


    标注问题常用的统计学方法有:详解隐马尔可夫模型(HMM)自然语言模型之条件随机场理论(CRF),这两个模型,之前的文章有介绍过。


    标注问题在信息抽取,自然语言处理等领域被广泛应用,是这些领域的基本问题。例如,自然语言处理中的词性标注就是一个典型的标注问题:给定一个由单词组成的句子,对这个句子中的每一个单词进行词性标注,即对一个单词序列预测其对应的词性标记序列。


    举一个信息抽取的例子,从英文文章中抽取基本名词短语,为此,要对文章进行标注。英文单词是一个观测,英文句子是一个观测序列,标记表示名词短语的"开始"、"结束"或“其它”。标记序列表示英文句子中基本名词短语的所在位置。信息抽取时,将标记“开始”到标记“结束”的单词作为名词短语。


    标注模型的评价指标

    标注问题常用的评价指标是精确率 (precision ),召回率 (recall) 和F1值,它和分类问题的评价指标相同,为了简便,这里使用分类来进行说,通常标注模型在测试数据集上的预测和或正确或不正确,4中情况出现的总数分别记作:

    TP:将正确类预测为正类数

    FP:将正类预测为负类数

    FP:将负类预测为正类数

    TN:将负类预测为负类数

    那么精确率定义为:P = TP / (TP + FP)

    召回率定义为: R = TP / (TP + FN)

    F1值是根据精确率和召回率来进行计算的表达式为:

    2/ F1 = 1/ P + 1/ R

    即:F1 = 2TP /( 2TP + FP + FN)

    一般精确率和召回率都高时,F1值也会很高。


    参考学习资料:

    [1] 统计学习方法: 李航


    文章来源于微信公众号:言处理技术,更多内容请访问该公众号。


    欢迎关注公众号学习

    展开全文
  • 文本分类(三)专栏主要是对Github优秀文本分类项目的解析,该文本分类项目,主要基于预训练语言模型,包括bert、bert + CNN/RNN/RCNN/DPCNN、ERNIE等,使用PyTorch实现。 本博客还讲解了一种预训练语言模型的通用...
  • 在问答系统中,比如说人工客服,我们需要提前准备好问题一些答案,让用户输入的问题与题库中的问题进行相似度的比较,最后输出答案。 在推荐系统中,我们需要提取一个用户的所有物品,在根据这个物品找到对应的...
  • 支持英语,德语西班牙语作为输入语言 清理HTML以进行文本处理显示 为用户提供编辑器,以工作下载源文本html 突出显示找到的实体 允许用户浏览目标语言(当前支持10种语言)的实体并将参考下载到CSV文件 对...
  • [编程语言][汇编语言]计算机与汇编语言

    千次阅读 多人点赞 2015-11-30 18:47:31
    汇编语言
  • 文本标记语言 -- HTML

    万次阅读 2018-06-28 17:42:47
    HTML是Hyper Text Markup Language(超文本标记语言)的缩写,是构成Web页面的基本元素,是一种规范,一种标准。 HTML不是一种编程语言,而是一种描述性的标记语言,通过标识符来标识网页中内容的显示方式,例如图片...
  • 转载地址1:http://blog.csdn.net/droyon/article/details/41809187  转载地址2:... ...转载请注明:萦蘭小築 » ubuntu14.04安装了im-switch后系统设置中不见了语言支持
  • 机器语言、汇编语言(低级语言)、高级语言

    万次阅读 多人点赞 2019-11-10 00:00:57
    机器语言、汇编语言(低级语言)、高级语言 【原文:https://zhuanlan.zhihu.com/p/37524989】 编程语言的机器级表示:机器语言、汇编语言、高级语言 ​ 首先下面一张图是C语言、汇编语言以及翻译过的机器语言,大家...
  • 自然语言处理入门练习(一):基于机器学习的文本分类及实站(附代码) 目录自然语言处理入门练习(一):基于机器学习的文本分类及实站(附代码)任务一:基于机器学习的文本分类1 文本分类任务简介2 向量化2.1 词...
  • 本文分享主题:Faissbert提供的模型实现了一个中文问答系统。旨在提供一个用Faiss结合各种AI模型实现语义相似度匹配的解决方案。
  • 智能客服的本质,就是充分理解用户的意图,在知识体系中精准地找到与之相匹配的内容,回答用户问题或提供解决方案。问题相似度计算,是贯穿智能客服离线、在线运营等几乎所有环节最核心的技术,广泛应用于搜索、...
  • 摘要:目前的输入法大多采用输入法管理器-输入法编辑器(IMM-IME)进行开发,对于微软发布的新型输入法技术―文本服务框架(TSF)的研究一直比较滞后,该文论述了 TSF 的基本构成、主要接口、输入法的具体实现方法...
  • R语言数据的输入

    千次阅读 2020-02-14 15:33:24
    R语言数据的输入 在安装好R语言、RStudio、掌握了R语言的6种数据结构之后(如需要欢迎访问本人博客主页学习),进行数据分析的第一步是将合适的数据导入到R语言的工作环境中,进而对其进行相应的分析。R语言支持输入...
  • R语言输入

    千次阅读 2018-08-20 00:01:02
    R,作为一个非常灵活的平台,是专用于探索、展示理解数据的语言,一种为统计计算绘图而生的语言和环境,也是统计、预测分析数据可视化的全球通用语言,它提供各种用于分析理解数据的方法,从最基础的到最...
  • R语言文本挖掘 Part3文本聚类

    万次阅读 热门讨论 2015-03-16 20:54:35
    Part3文本聚类 分类聚类算法,都是数据挖掘中最常接触到的算法,分类聚类算法分别有很多种。可以看下下面两篇文章对常见的分类聚类算法的简介: 分类算法:http://blog.csdn.net/chl033/article/details/5204220...
  • 任务-ICDAR 2019多语言场景文本检测识别的稳健阅读挑战 为了参加RRC-MLT-2019挑战赛,您必须至少参加一项任务。这是任务的描述。前三个任务与RRC-MLT-2017中的任务相似,但针对RRC-MLT-2019重新打开了它们,为...
  • 文章目录一、项目演示:1:诗歌创作2:律诗与绝句3:小说篇4:自己的经济新闻篇二、原理解读Gpt-2简述何为语言模型与 BERT 的区别三、代码详解与训练教程训练数据半精度模型使用预训练步骤:四、生成文本文件结构...
  • spaCy不仅包含一些基本的文本处理操作,还包含一些预训练的模型词向量等,之后我们还会学习一些更高级的模型或方法,不过这些基本处理要熟练掌握,因为他们可以对我们的数据进行一些预处理,作为更高级模型或工具...
  • 文本识别(自然语言处理,NLP)

    千次阅读 2018-12-19 18:32:26
    目录语音识别NLTK - 自然语言工具包分词词干词形还原词袋词频文档频率(DF)逆文档频率(IDF)词频你文档频率(TF-IDF)基于多项分布朴素贝叶斯的情感分析主题抽取 语音识别 语音-----------------------&gt;...
  • 记住任何页面上的文本输入字段 记住页面上输入字段的值 TIRE尝试保存用户可以在其中输入文本的所有页面字段(密码除外)的内容。一旦存储了文本并重新访问了页面,就可以使用上次输入的文本填充字段。完整的说明可在...
  • 文本分析:主题建模 library(tidyverse) theme_set(theme_bw()) 目标 定义主题建模 解释Latent Dirichlet分配以及此过程的工作原理 演示如何使用LDA从一组已知主题中恢复主题结构 演示如何使用LDA从一组...
  • 出品:贪心科技(公众号:贪心科技)作者:Artem Oppermann(贪心科技编译)字数:2300阅读时长:5分钟前言在本教程中,您将发现如何使用 Python 中的深层学习来开发统计语言模型。神经网络模型是开发统计语言模型...
  • ROS总结——ROS消息ROS服务

    千次阅读 2017-03-12 11:34:14
    本节详细讲解了如何创建并编译ROS消息和服务,以及rosmsg、rossrv和roscp命令行工具的使用。1.消息(msg)和服务(srv)介绍 消息(msg): msg文件就是一个描述ROS中所使用消息类型的简单文本。它们会被用来生成不同语言的...
  • 1. 什么是有用的文本语料词汇资源,我们如何使用 Python 获取它们?2. 哪些 Python 结构最适合这项工作?3. 编写 Python 代码时我们如何避免重复的工作?2.1 获取文本语料库古腾堡语料库import nltk print(nltk....
  • 文本和输入:创建一个IME:简介

    千次阅读 2017-05-08 09:55:22
    输入法编辑器(IME)是用户可以控制,以让用户输入文本的编辑器。 Android提供了一个可扩展的输入法框架,允许应用程序为用户提供替代输入法,如屏幕键盘,甚至语音输入。 安装所需的IME后,用户可以从系统设置中...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 182,446
精华内容 72,978
关键字:

怎样找到文本服务和输入语言