面向医疗保健的 Keras 和 TensorFlow2 人工智能教程(二)

原文:AI for Healthcare with Keras and Tensorflow 2.0

协议:CC BY-NC-SA 4.0

四、根据临床记录预测医疗账单代码

临床记录包含关于医生开出的处方程序和诊断的信息,并且在当前的医疗系统中用于准确计费,但是它们并不容易获得。我们必须手动提取它们,或者使用一些辅助技术来无缝地执行这个过程。

这增加了支付者和提供者的管理成本。仅医疗服务提供商在保险和医疗账单成本上就花费了大约 2820 亿美元。良好的记录和质量跟踪是额外的成本。与每种类型的就诊相关的专业收入相比,急诊就诊产生的账单成本最高,相当于收入的 25.2%。

在本章中,您将深入了解 BERT 和 transformer 架构,探索最新的 transformer 模型。您还将了解如何将不同的微调技术应用于 transformer 模型。最后,您将学习在 NLP 中使用迁移学习的概念,并将多标签分类作为下游任务。

从非结构化临床记录中预测诊断和程序可以节省时间、消除错误并最大限度地降低成本,所以让我们开始吧。

介绍

首先,我说的这些 ICD 电码是什么?那些熟悉 ICD 电码的人可能会混淆 ICD-9 和 ICD-10 电码之间的区别。

ICD 代表国际疾病分类,它是由卫生与公众服务部管理和维护的一套标准代码(还记得第一章的 HHS 吗?).这些代码用于准确测量结果和为患者提供的护理,同时还为研究和临床决策提供了一种结构化的疾病和症状报告方式。

HHS 要求 HIPAA 法案下的所有实体必须将其 ICD 代码转换为 ICD-10 格式。这样做有各种原因,但最主要的是

  • 跟踪新的疾病和健康状况:旧系统包含大约 17.8K 个不同的 ICD 代码,但 ICD-10 将超过 15 万种状况和疾病映射到不同的代码。

  • 更大的空间允许更好和更准确地定义 ICD 编码,并支持流行病学研究,如疾病的共病或严重程度等。

  • 防止报销欺诈

由于 MIMIC 3 包含新的代码系统被授权之前的 EHR 数据,您可以轻松地继续使用现有的 ICD 数据,但请记住这一点,以防您看到新的 EHR 数据。别担心。您可以亲自动手,将从这里学到的知识应用到新的 ICD 公约中。

由于有许多 ICD-9 代码,实际上,您只需尝试识别前 15 个 ICD-9 代码,这取决于有多少住院患者贴上了特定的 ICD-9 代码标签。

我已经深入讨论了 MIMIC 3 数据,所以让我们只关注选择正确的表和概述准备数据的步骤。让我们深入研究一下。

图 4-1 显示了 ICD-9 和 ICD-10 CM 代码的差异。注意,ICD 码有两种类型。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4-1

ICD 9 厘米和 ICD 10 厘米诊断编码系统

  • CM(临床修改) :住院和门诊数据的诊断编码

  • PCS(程序编码系统) :住院数据的程序编码

数据

我在上一章深入讨论了 MIMIC 3 数据集,所以让我们直接开始创建数据。

注释事件

此表包含与患者入院后记录的所有临床记录相关的文本。在NOTEEVENTS表中要查看的两个重要列是CATEGORYDESCRIPTIONCATEGORY包含匿名的临床记录,DESCRIPTION告诉我们这些是完整的报告还是附录。

因为用例的中心是降低提供商和支付者的管理成本,所以该信息的最佳来源是“出院总结-报告”。

    n_rows = 100000

# create the iterator
noteevents_iterator = pd.read_csv(
        "./Data/NOTEEVENTS.csv",
    iterator=True,
    chunksize=n_rows)

# concatenate according to a filter to get our noteevents data
    noteevents = pd.concat( [noteevents_chunk[np.logical_and(noteevents_chunk.CATEGORY.isin(["Discharge summary"]), noteevents_chunk.DESCRIPTION.isin(["Report"]))]
    for noteevents_chunk in noteevents_iterator])

noteevents.HADM_ID = noteevents.HADM_ID.astype(int)

现在您已经有了自己的数据集,让我们稍微探索一下。

主键上的重复:尽管SUBJECT_IDHADM_ID对应该有一个唯一的记录,但是NOTEEVENTS数据集中还是有重复的。

经过进一步调查,看起来记录在不同的日期对相同的入院 ID 有不同的出院摘要文本。这看起来像一个不可能的事件,因此是一个更数据的问题。现在,您将对CHARTDATE列中的数据进行排序,并保留第一个条目。

try:
        assert len(noteevents.drop_duplicates(["SUBJECT_ID","HADM_ID"])) == len(noteevents)
except AssertionError as e:
        print("There are duplicates on Primary Key Set")

    noteevents.CHARTDATE  = pd.to_datetime(noteevents.CHARTDATE , format = '%Y-%m-%d %H:%M:%S', errors = 'coerce')
    pd.set_option('display.max_colwidth',50)
    noteevents.sort_values(["SUBJECT_ID","HADM_ID","CHARTDATE"], inplace =True)
    noteevents.drop_duplicates(["SUBJECT_ID","HADM_ID"], inplace = True)

noteevents.reset_index(drop = True, inplace = True)

在移动到下一个数据源查看文本数据之前,还有一件事要做。您可以在下面看到文本的样本摘要:

    Admission Date: [**2118-6-2**] Discharge Date: [**2118-6-14**]

Date of Birth: Sex: F

Service: MICU and then to [**Doctor Last Name **] Medicine

    HISTORY OF PRESENT ILLNESS: This is an 81-year-old female
with a history of emphysema (not on home O2), who presents
with three days of shortness of breath thought by her primary
care doctor to be a COPD flare. Two days prior to admission,
she was started on a prednisone taper and one day prior to
admission she required oxygen at home in order to maintain
    oxygen saturation greater than 90%. She has also been on
levofloxacin and nebulizers, and was not getting better, and
    presented to the [**Hospital1 18**] Emergency Room.

您可以看到某些可用于清理文本的图案:

  1. 匿名日期、患者姓名、医院和医生姓名

  2. 使用一种模式如“主题:文本”如“入院日期:[**2118-6-2**]:, "HISTORY OF PRESENT ILLNESS: This is an 81-year-old female....

  3. 使用换行符 ("\n ")

您将利用所有这些模式来清理数据,并确保每个独特的句子得到正确记录。

你要做两件事。首先,你要确保所有不相关的话题都从出院小结中删除。对于这一点,你会发现最常见的话题。

import re
import itertools

    def clean_text(text):
        return [x for x in list(itertools.chain.from_iterable([t.split("<>") for t in text.replace("\n"," ").split("|")])) if len(x) > 0]

    most_frequent_tags = [re.match("^(.*?):",x).group() for text in noteevents.TEXT for x in text.split("\n\n") if pd.notnull(re.match("^(.*?):",x))]
    pd.Series(most_frequent_tags).value_counts().head(10)

图 4-2 中显示了最常见主题标签的摘录。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图。4-2

出院小结中最常见的话题

    irrelevant_tags = ["Admission Date:", "Date of Birth:", "Service:", "Attending:", "Facility:", "Medications on Admission:", "Discharge Medications:", "Completed by:", "Dictated By:" , "Department:" , "Provider:"]

    updated_text = ["<>".join(["|".join(re.split("\n\d|\n\s+",re.sub("^(.*?):","",x).strip())) for x in text.split("\n\n") if pd.notnull(re.match("^(.*?):",x)) and re.match("^(.*?):",x).group() not in irrelevant_tags ]) for text in noteevents.TEXT]
    updated_text = [re.sub("(\[.*?\])", "", text) for text in updated_text]

    updated_text = ["|".join(clean_text(x)) for x in updated_text]
    noteevents["CLEAN_TEXT"] = updated_text

对于上面的示例,下面是清理后的文本。很漂亮,对吧?

    'This is an 81-year-old female with a history of emphysema (not on home O2), who presents with three days of shortness of breath thought by her primary care doctor to be a COPD flare. Two days prior to admission, she was started on a prednisone taper and one day prior to admission she required oxygen at home in order to maintain oxygen saturation greater than 90%. She has also been on levofloxacin and nebulizers, and was not getting better, and presented to the Emergency Room.',

     'Fevers, chills, nausea, vomiting, night sweats, change in weight, gastrointestinal complaints, neurologic changes

, rashes, palpitations, orthopnea. Is positive for the following: Chest pressure occasionally with shortness of breath with exertion, some shortness of breath that is positionally related, but is improved with nebulizer treatment.'

诊断 _ICD

这是 ICD-9 代码表。它包含与受试者入院事件相关的所有 ICD-9 代码。正如引言中所讨论的,您正在为手头的问题寻找前 15 个最常见的 ICD-9 代码。

    top_values = (icd9_code.groupby('ICD9_CODE').
                  agg({"SUBJECT_ID": "nunique"}).
                  reset_index().sort_values(['SUBJECT_ID'], ascending = False).ICD9_CODE.tolist()[:15])

icd9_code = icd9_code[icd9_code.ICD9_CODE.isin(top_values)]

理解语言建模如何工作

在您直接使用 BERT 之前,让我们先了解一下它是如何工作的,构建模块是什么,为什么需要它,等等。

谷歌 AI 语言团队在 2018 年发布的题为“BERT:用于语言理解的深度双向变压器的预训练”的论文,是非研究社区对语言建模的新形式和变压器模型的应用真正感到兴奋的时候。2017 年,谷歌大脑团队在一篇题为“注意力是你所需要的一切”的论文中介绍了变形金刚模型。

很有趣,对吧?注意力被引入是为了以更人性化的方式学习语言,例如通过关联句子中的单词。注意力有助于更好地为 NLP 中的转导问题建立句子模型,从而改进编码器-解码器架构。

编码器-解码器架构依次建立在 RNNs、LSTMs 和 Bi-LSTMs 之上,它们在某个阶段是序列建模的最新技术。它们都属于循环网络类。因为一个句子是一个单词序列,所以你需要一个序列建模网络,在这个网络中,当前输入在序列的第二个元素中重复出现,以便更好地理解单词。这个信息链有助于对一个句子的有意义的表达进行编码。

我在这里想说的是,要真正理解 BERT 或任何其他基于 transformer 的架构模型,您需要对许多相互关联的概念有深刻的理解。为了让讨论集中在 BERT 上,我将主要讨论注意力和 BERT 架构。

集中注意力

先说个例子。如果我让你告诉我下列句子的意思,你会怎么说?

  1. 狗很可爱。我喜欢和他们在一起。

  2. 狗很可爱。我喜欢和他们在一起。

对于这两个句子,人们很容易理解说话者对狗有积极的情感。但是下面这句话呢?

  1. 狗很可爱。我喜欢和他们在一起。

对于这句话,虽然不是决定性的,但我们可以说这句话应该是关于狗的积极的东西。这叫做注意力。为了理解一个句子,我们只依靠某些单词,而其他的都是垃圾(从理解的角度来看)。

递归网络族虽然有助于建模序列,但对于非常大的句子来说是失败的,因为编码上下文信息的固定长度表示只能捕获这么多的相关性。但是如果我们只从一个大句子中挑选重要的句子呢?那我们就不用担心残留了。

我喜欢从信息论的角度来理解这一点。我们只需要使用 2 的幂的不同组合就可以对所有的整数建模,如图 4-3 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4-3

作为 2 的幂的整数

所以要得到任何数,我们要做的就是取两个向量的点积:

)

)

注意力以非常相似的方式工作。它采用序列的上下文向量或编码向量,只对重要的方面进行加权。虽然在我们的例子中,我们从整数转移到实数。参见图 4-4 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4-4

展示了添加前馈层如何帮助我们学习注意力权重

Dzmitry Bahdanau 等人在 2014 年发表的题为“通过联邦学习对齐和翻译进行神经机器翻译”的论文中首次讨论了注意力的概念。

在图 4-4 中,注意来自最后一个解码器单元的绿色箭头。这是由 S t-1 表示的解码器状态。我们结合隐藏状态和最后一个隐藏层的输出的方式可以提供各种关注,如表 4-1 所示。这也称为分数或编码器输出的能量。该组合函数或评分函数被设计成最大化解码器的隐藏状态和编码器输出之间的相似性。这样就产生了更多连贯的单词,给 MTL(多语言翻译)系统带来了更大的能力。

表 4-1

用于计算解码器和编码器状态之间相似性的不同评分函数

|

注意名称

|

|
| — | — |
| 加法或串联:最后一个解码器单元的隐藏状态被添加到编码器单元的隐藏状态。假设维度是 d,那么连接的维度就变成了 2d。 | Bahdanau 等人,2014,“通过联邦学习对齐和翻译的神经机器翻译” |
| 点积:最后一个解码器单元的隐藏状态乘以编码器单元的隐藏状态。假设维度是 d,那么连接的维度就变成了 d。 | Luong 等人,2015,“基于注意力的神经机器翻译的有效方法” |
| 比例点积:同上,只是增加了一个比例因子,使数值标准化,并在 Softmax 函数的可微分范围内。 | 瓦斯瓦尼等人,2017,“注意力是你所需要的一切” |
| 一般(点积):编码器隐藏状态在计算分数之前通过一个前馈网。 | Luong 等人,2015,“基于注意力的神经机器翻译的有效方法” |

一些你应该记住的细节:

  • 为了使这个过程更快,您利用 Keras 的 TimeDistributedLayer,它确保每个时间单位的前馈发生得更快。(只是密密麻麻一层。)

  • 最后合并的编码器隐藏状态作为输入被馈送到第一解码器单元。这个解码器单元的输出被称为第一解码器隐藏状态。

  • 所有分数都通过 Softmax 层传递,以给出注意力权重,然后乘以编码器的隐藏状态,以获得来自每个编码器单元的上下文向量 C t

最后,注意力有很多种:

  • 当地和全球的关注

  • 自我关注

  • 多头注意力

为了完整起见,我将简要地讨论它们,因为每一个都值得写一篇自己的文章,因此,为了有一个总体的理解,我只包括定义。我将在 transformer 架构讨论中详细讨论多头关注。请参考表 4-2 了解不同类型注意力的概述。

表 4-2

不同类型的关注

|

注意力

|

描述

|

|
| — | — | — |
| 当地和全球的关注 | 全局:所有编码器单元都被赋予了重要性。Local :上下文向量生成只考虑输入的一部分。该输入以位置 p t 为中心,宽度为 p t -2L 到 p t +2L,其中 L 是窗口长度。 | 灵感来源于徐等,2015,“展示、参与、讲述:视觉注意下的神经图像字幕生成” |
| 自我关注 | 工作原理类似于上面编码器-解码器架构中所解释的注意事项;我们只是用输入序列本身替换目标序列。 | 程等,2016,“长短期记忆-机器阅读的网络” |
| 多头注意力 | 多头注意力是实现自我注意力的一种方式,但是有多个键。稍后将详细介绍。 | 瓦斯瓦尼等人,2017,“注意力是你所需要的一切” |

转变 NLP 空间:变压器架构

Transformer 模型可以说是为 NLP 迁移学习任务带来了 ImageNet 运动。到目前为止,这需要大量数据集来捕获上下文、大量计算资源,甚至更多时间。但是,一旦 transformer 架构出现,它就可以更好地捕捉上下文,因为可以并行化,所以训练时间更短,并且还可以为许多任务设置 SOTA。图 4-5 显示了 Vaswani 等人题为“你只需要关注”的论文中的变压器架构

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4-5

变压器模型

对于门外汉来说,该模型一开始可能会令人望而生畏,但如果在筒仓中理解不同的概念,则非常容易理解。

要理解变形金刚,你需要理解

  • 编码器-解码器框架

  • 多头注意力

  • 位置编码

  • 剩余连接

编码器和解码器模块与上面的关注主题一起讨论,残差连接只是为了确保来自目标损失的残差可以容易地帮助准确地改变权重,因为有时由于非线性,梯度不会产生期望的效果。

位置编码

变压器能够并行处理更大的数据和更多的参数,实现更快的训练。但是怎么可能呢?通过移除所有有状态的细胞,如 RNN、GRU 或 LSTM,这是可能的。

但是,我们如何确保句子的句法语法没有被打乱,并且句子的单词有一定的顺序感呢?我们通过使用一个密集的向量来编码一个单词在序列中的位置。

思考这个问题的一个非常简单的方法是将每个单词标记为正整数(无界)。但是如果我们得到一个很长的句子或者不同长度的句子呢?在这两种情况下,拥有一个无界的数字表示是行不通的。

好的,那么有界表示可以工作吗?让我们对[a 到 b]之间的所有内容进行排序,其中 a 代表第一个单词,b 代表最后一个单词,其他内容位于两者之间。因为它是一个 10 个单词的句子的模型,你必须增加索引),对于一个 20 个单词的句子,增量是)。因此,增量没有相同的意义。作者提出了图 4-6 中位置编码矢量的公式。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4-6

位置编码

理解这一点的一个好方法,不需要太多的数学知识,就是

  • 位置编码器是一个 d 维向量。这个向量被添加到单词的单词向量表示中,因此 dpe = dmodel。

  • 它是一个大小为(n,d)的矩阵,其中 n 是序列中的单词数,d 是单词嵌入的维数。

  • 它是每个位置的唯一向量。

  • sin 和 cos 函数的组合允许模型很好地学习相对位置。因为任何偏移 k,PEpos+k 可以表示为 PEpos 的线性函数。

  • 由于残留块的存在,该位置信息也保留在较深层中。

多头注意力

多头关注是 transformer 架构的主要创新。我们来详细了解一下。本文使用一个通用的框架来定义注意。

它引入了三个术语:

  1. 钥匙(K)

  2. 查询(Q)

  3. 价值(伏特)

单词的每次嵌入都应该有这三个向量。它们是通过矩阵乘法得到的。这从嵌入向量中捕获了特定的信息子空间。

一个抽象的理解是这样的:你试图通过使用一个查询来识别某些键、值对。您试图确定其关注分数的单词是查询。

由于天气不好,交通堵塞。

假设你的查询是流量。查询向量捕获单词 traffic 的一些语义,可能是它的 pos 标签或与旅行/通勤相关的标签,等等。类似地,对于键和值向量,也捕捉到了一些细微差别。

现在,您到达单词天气,类似地,您捕获 K、Q 和 v。如果交通的查询与天气的关键字具有高相似性,则天气的值对单词**交通的自我关注向量贡献很大。**见图 4-7 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4-7

自我关注。图片改编自《注意力是你需要的全部》

在多头注意力中,有多个这样的矩阵乘法,可以让你每次捕捉到不同的子空间。它们都是并行完成的。参见图 4-8 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4-8

多头自我关注。图片改编自《注意力是你需要的全部》

以上两项是变压器模型中的主要创新。毫无疑问,它能够很好地捕捉句子语义。以下是一些值得一提的其他细节:

  • 解码器模型包含掩蔽的多头注意力模型。它屏蔽掉查询词之后的所有词。价值向量被屏蔽,然后转移到自我关注向量。

  • 来自被掩蔽的注意块的自我注意向量充当其上方的多头注意块的值向量。

  • 使用跳跃连接(灵感来自 ResNet,由何等人在“图像识别的深度残差学习”中介绍)来防止信号丢失。

  • 有多个编码器-解码器模块堆叠在一起,图 4-5 显示了最后一对。Softmax 仅被添加到最后一个解码器块。

Note

来自最后一个编码器的输出被传递到所有的解码器单元,而不仅仅是最后一个。

BERT:来自变压器的双向编码器表示

BERT 为将 NLP 的 ImageNet 运动带入现实奠定了基础。现在我们有了一个 BERT 模型动物园,这基本上意味着几乎每种应用程序都有一个 BERT 模型。

从架构的角度来看,BERT 只不过是堆叠的变压器(只有编码器模块)。但它在处理输入数据和训练方面带来了一些新的创新。在深入研究代码之前,让我们简单地讨论一下它们。

投入

BERT 作者分享了一些输入文本的创新方法。我已经讨论了长度中的位置嵌入,所以让我们快速跳到令牌和段嵌入。参见图 4-9 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4-9

BERT 输入表示。图片改编自《BERT:深度双向转换者语言理解预训练》

令牌嵌入

令牌嵌入只是以数字形式表示每个令牌的一种方式。在 BERT 中,这是一个 768 维的向量。这里有趣的是工件记号化技术。它帮助 BERT 维持了一个相当大的图书馆,有 30,522 个,但没有在未收录的单词上妥协。

我们举个例子来理解一下。假设最初你有一个只有五个单词的字典,它们在语料库中的数量是已知的:

  1. 教堂,5

  2. 孩子,3

  3. 返回,8

  4. 赚,10

  5. 提升,5

结尾的代表单词边界。单词块算法检查文本中的每个字符,并试图找到频率最高的字符对。

假设系统遇到一个像 Churn 这样的不在词汇表中的单词。对于这个单词,BERT 会做如下处理:

  1. c : 5 + 3 = 8

  2. c+h:5+3 = 8

  3. c + h + u: 5,由于总数下降而被拒绝

  4. n : 10

  5. r + n : 8,拒绝,因为它也减少了 n 的计数。

  6. u + r :8,u + r 的计数

因此,创建的令牌是

[ch,ur,n .]

我上面讨论的是 BPE 或二进制编码。正如您所观察到的,它以贪婪的方式根据频率合并单个字符。单词块算法略有不同,在某种程度上,字符合并仍然基于频率,但最终决定是基于出现的可能性(查看哪些单词块更有可能出现)。

片段嵌入

伯特接受两种不同训练任务的训练:

  1. 分类:确定输入句子的类别

  2. 下一个句子预测:预测下一个句子或理想地/连贯地跟随前一个句子的句子(如在训练语料库中存在的)

为了预测下一个句子,BERT 需要一种方法来区分这两个句子,因此在每个句子的末尾引入了一个特殊的标记[SEP]。

因为我已经谈到了位置嵌入,所以我不会在这里再次讨论它。

培养

BERT 模型针对两项任务进行了预训练:

  1. 掩蔽语言建模

  2. 下一句预测

掩蔽语言建模

引入屏蔽语言建模主要是为了允许模型以双向方式学习,并使模型能够捕捉序列中任何随机单词的上下文。

分类层被添加到编码器输出的顶部。这些输出通过时间分布的密集层,将它们转换成词汇的维数,然后计算每个单词的概率。参见图 4-10 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4-10

掩蔽语言建模

  • 为了使模型位置不可知,同时给出足够的上下文,在的每个序列中只有 15% 的单词被随机屏蔽。

  • 如图 4-10 所示,并非所有被屏蔽的单词都被替换为[MASK]标记。而是选择了以下方法:

    • 80%的时间使用了[MASK]标记。

    • 10%的情况下,这些单词被替换成随机单词。

    • 剩下的 10%的时间单词保持不变。

如果你深入思考,你会想到很多关于选择这些百分比的问题。没有进行消融研究来支持这些经验数据;但是,有一些直觉。

  • 使用随机单词会让 BERT 学习错误的嵌入吗?理想情况下不会,因为它在反向传播过程中被正确的标签所纠正。这样做也是为了引入方差。

  • 为什么不保留 100%【面具】令牌?这样做是为了避免微调过程中的任何混乱,如果没有找到[MASK]标记,它将根据任务给出一些随机输出。

下一句预测

根据作者的说法,学习如何将两个句子联系起来可以显著提高问答和自然语言推理等任务的性能。

在这里,他们也提出了某些比率,并使用这些比率为 NSP 创建了一个训练数据:

  • 对于语料库中 50%的句子,下一个句子是与语料库中存在的句子相同的句子。

  • 剩下的 50%,下一句随机抽取。

这给了我们一个二进制分类器来训练。[CLS]令牌用于二进制分类,其最终状态被传递到 FFN 加软件最大层。

我希望您现在对基于 transformer 的模型,尤其是 BERT 有了更深的理解。我认为这应该足以让您将 BERT 应用于该案例,并学习如何对其进行微调。

建模

现在让我们深入研究建模。您已经在上面的“数据”部分准备好了数据。您正在尝试进行多标签分类。你必须以这样的方式准备你的数据。

对于您的任务,您将使用来自高丽大学 DMIS(数据挖掘和信息系统)实验室的 BERT 大模型。您这样做是因为它是为数不多的为 BERT 提供定制词汇表的预训练模型之一。大多数免费的预训练模型保持相同的词汇,在我看来这是一种不好的做法。

第二,你还将利用拥抱脸小组的变形金刚库,它为语言理解(NLU)和自然语言生成(NLG)任务提供通用架构(伯特、GPT-2、罗伯塔、XLM、蒸馏伯特、XLNet)。

但在此之前,让我们先了解一下 BERT 模型的词汇。发布后,您将形成您的数据并进行多标签分类。

伯特深潜

除了更好的性能之外,拥有自定义词汇表的一个好处是能够看到哪些概念被捕获。您将使用一个 UML 数据库来识别词汇表中哪些概念正在被捕获;为此,您将看到子词标记(没有“##”),并选取长度大于 3 的所有标记。

为此,您必须设置 scispacy 库。它建立在 spacy 之上,对于应用 NLP 工作来说是一个非常快速和有用的库。请参见第二章中的安装步骤。

Scispacy 提供了一种链接知识库的方法。概念提取对字符串重叠起作用。它涵盖了大多数公开可用的主要生物医学数据库,如 UMLs、Mesh、RxNorm 等。

此外,您将使用基于生物医学数据的大型空间模型。确保您已经通过下载并链接到 spacy 来设置模型。保留匹配的默认参数,因为这只是一个探索性的练习,您的建模不会直接受到此选择的影响。官方文档在 https://github.com/allenai/scispacy .

词汇实际上包含什么?

在深入训练分类模型或使用微调进一步改进它之前,您应该仔细检查一下您拥有的词汇表。它甚至包括生物医学概念吗?平均令牌长度是多少?(生物医学词汇一般有体面的令牌长度一般> 5 个字符。)

让我们一个一个来看看这些问题。

  1. 找到任何生物医学概念

为了找到生物医学的概念,你将利用一个广泛的 UMLs 知识库。它通过一个简单的界面与科学联系起来。

首次运行时,链接可能需要一些时间,具体取决于您的电脑配置。首先,从导入库和加载相关模型开始。

# Load Hugging-face transformers
from transformers import TFBertModel, BertConfig, BertTokenizerFast
import tensorflow as tf

# For data processing
import pandas as pd
from sklearn.model_selection import train_test_split

# Load pre-trained model tokenizer (vocabulary)
    tokenizer = BertTokenizerFast.from_pretrained('dmis-lab/biobert-large-cased-v1.1')

接下来,让我们找出唯一令牌的总数。

vocab = tokenizer.vocab.keys()
# Total Length
    print("Total Length of Vocabulary words are : ", len(vocab))

词汇单词的总长度为 58996,几乎是谷歌团队分享的第一个 BERT 模型的两倍。猜猜为什么?

嗯,词汇量的大小是基于你能够用词汇表的子词对语料库中的每个词进行编码的清晰程度来决定的。谷歌没有分享代码,所以确切的原因不得而知,但我打赌上述大小足以以优化的方式表示语料库中的不同单词。你可以在 https://github.com/google-research/bert#learning-a-new-wordpiece-vocabulary 从谷歌官方回购了解更多信息。

让我们连接 UMLs 数据库。

import spacy
import scispacy

from scispacy.linking import EntityLinker
    nlp = spacy.load('en_core_sci_lg')
    linker = EntityLinker(resolve_abbreviations=False, name="umls") # keeping default thresholds for match percentage.
nlp.add_pipe(linker)

# subword vs whole word selection based on length
    target_vocab = [word[2:] for word in vocab if "##" in word and (len(word[2:]) > 3)] + [word[2:] for word in vocab if "##" not in word and (len(word) > 3)]

umls_concept_extracted = [[umls_ent for entity in doc.ents for umls_ent in entity._.umls_ents] for doc in nlp.pipe(target_vocab)]

    umls_concept_cui = [linker.kb.cui_to_entity[concepts[0][0]] for concepts in umls_concept_extracted if len(concepts) > 0]
# Capturing all the information shared from the UMLS DB in a dataframe
umls_concept_df = pd.DataFrame(umls_concept_cui)

UMLs 为它的每个 TXXX 标识符提供一个类名。TXXX 是每个 CUI 编号的父项代码,是 UMLs KB 使用的唯一概念标识符。接下来,让我们将 TXXX ids 映射到人类可读的标签。

# To obtain this file please login to https://www.nlm.nih.gov/research/umls/index.html
# Shared in Github Repo of the book :)
    type2namemap = pd.read_csv("SRDEF", sep ="|", header = None)
    type2namemap = type2namemap.iloc[:,:3]
    type2namemap.columns = ["ClassType","TypeID","TypeName"]
    typenamemap = {row["TypeID"]:row["TypeName"] for i,row in type2namemap.iterrows()}

为每个类型 ID 创建计数。

concept_df = pd.Series([typenamemap[typeid] for types in umls_concept_df.types for typeid in types]).value_counts().reset_index()
    concept_df.columns = ["concept","count"]

让我们想象一下这 20 个最重要的概念。见图 4-11 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4-11

生物医学概念在伯特词汇中的分布

哇,这些词汇实际上包含了各种生物医学概念,如疾病、身体部位、有机化学物质(化合物)和药理物质(用于治疗病理障碍)。看起来你有适合你任务的模型。所有这些概念在 EHR 笔记中也很常见。

接下来,让我们看看您在数据集中观察到的子词和实际标记的标记长度。

    subword_len = [len(x.replace("##","")) for x in vocab]
token_len = [len(x) for x in vocab]

import seaborn as sns
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker

    with sns.plotting_context(font_scale=2):
        fig, axes = plt.subplots(1,2, figsize=(10, 6))
        sns.countplot(subword_len, palette="Set2", ax=axes[0])
    sns.despine()
        axes[0].set_title("Subword length distribution")
        axes[0].set_xlabel("Length in characters")
        axes[0].set_ylabel("Frequency")

        sns.countplot(token_len, palette="Set2", ax=axes[1])
    sns.despine()
        axes[1].set_title("Token length distribution")
        axes[1].set_xlabel("Length in characters")
        axes[1].set_ylabel("Frequency")

在图 4-12 中,您确实看到了分布在【5-8】之间的平均值,这是一个很好的指标,表明您正在使用一个正确的预训练模型。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4-12

词汇标记的长度分布

如果你想仔细阅读词汇表中的不同单词,你可以访问下面的链接:

https://huggingface.co/dmis-lab/biobert-large-cased-v1.1/blob/main/vocab.txt

培养

BERT 可用于多种方式的微调:

  • **微调:**您在 BERT 模型的最后一个预训练层的顶部添加另一组层,然后用特定于任务的数据集训练整个模型,尽管在此过程中您必须确保预训练模型的权重不被破坏,因此您将它们冻结一些时期,然后在另一组时期恢复到 BERT 层的完全反向传播。这也叫热身。

  • **从最后一组层中提取权重:**提取的上下文嵌入被用作下游任务的输入。它们是固定向量,因此不可训练。原始论文中讨论了四种不同类型的方法(表 7)。

    • 12 层的加权和。权衡可以是经验性的。

    • 使用最后一个隐藏层。

    • 提取倒数第二个隐藏层(倒数第二)。

    • 连接最后四个隐藏层。

  • **单词嵌入:**从 BERT 的编码器层获取单词嵌入。包装器存在于拥抱脸的变形库中。

微调被认为是更好地控制模型性能的最佳方法,所以您将采用这种方法。

由于您将训练多标签分类,因此让我们为其准备最终数据集。你正在做一个实际的决定,不要保留只有三个或更少标记的短句。

# Making icd9_code unique at SUBJECT ID and HADM_ID level by clubbing different ICD9_CODE
    icd9_code = icd9_code.groupby(["SUBJECT_ID","HADM_ID"])["ICD9_CODE"].apply(list).reset_index()

    full_data = pd.merge(noteevents, icd9_code, how="left", on = ["SUBJECT_ID","HADM_ID"])

# Removing any SUBJECT_ID and HADM_ID pair not having the top 15 ICD9 Codes
    full_data = full_data.dropna(subset = ["ICD9_CODE"]).reset_index(drop = True)

# Make sure we have text of considerable length
    full_data.CLEAN_TEXT = [" ".join([y for y in x.split("|") if len(y.split()) > 3]) for x in full_data.CLEAN_TEXT]

您还将使用full_data变量创建训练和验证集。此外,您的目标将是一个独热矩阵,每个样本有一个其所属的 ICD-9 代码的标签,其余的标签为零。

# Binarizing the multi- labels
from sklearn.preprocessing import MultiLabelBinarizer
from sklearn.model_selection import train_test_split

mlb = MultiLabelBinarizer()
mlb_fit = mlb.fit(full_data.ICD9_CODE.tolist())

    train_X,val_X,train_y,val_y = train_test_split(full_data[["SUBJECT_ID"," ","CLEAN_TEXT"]],full_data.ICD9_CODE.values, test_size=0.2, random_state=42)

你终于准备好加载拥抱脸变压器库,并从 DMIS 实验室获得伯特模型。

# Load Huggingface transformers
from transformers import TFBertModel, BertConfig, BertTokenizerFast
import tensorflow as tf
import numpy as np

# For data processing
import pandas as pd
from sklearn.model_selection import train_test_split

# Load pre-trained model tokenizer (vocabulary)
    tokenizer = BertTokenizerFast.from_pretrained('dmis-lab/biobert-large-cased-v1.1')

# Import BERT Model
from transformers import BertModel, BertConfig, TFBertModel
    bert = TFBertModel.from_pretrained("./dmis-lab/biobert-large-cased-v1.1",
                                   from_pt = True)

DMIS 团队共享的模型是 pytorch 模型,因此不能直接用于您的任务。您将使用 transformers 库中提供的包装函数将 pytorch 模型转换为 TensorFlow BERT 模型。

您必须确保传递了参数from_pt = True,这表示您正试图从 Python 预训练文件创建 TFBertModel。

接下来,决定您将要使用的模型参数。

    EPOCHS = 5
    BATCH_SIZE = 32
    MAX_LEN = 510
    LR = 2e-5
    NUM_LABELS = 15 # Since we have 15 classes to predict for

理想情况下,你决定MAX_LEN。您可以绘制语料库中句子长度的直方图,但是由于文本通常很长,您已经根据标记的数量为句子取了最大长度。

目前,学习速度保持不变,没有热身。所使用的设计参数,如激活函数的选择、批量大小等。,只是经验性的设计选择,因此您可以探索和试验不同的设计选择。

就像在第三章中一样,您将创建一个生成器函数,该函数将生成批量维度的输入数据。

    X = (BATCH_SIZE, {'input_ids':[0 to VOCAB LENGTH],'token_type_ids':[1/0],'attention_mask':[1/0]}

BERT 将字典作为输入:

  • 输入 id表示根据 BERT 模型词汇的标记化单词的索引

  • 令牌类型 ID也称为段 ID。因为您正在训练一个序列分类问题,所以所有的令牌类型 id 都是零。

  • 注意力屏蔽是一个 1/0 向量,它告诉我们应该关注哪个单词。一般来说,所有的单词都被认为是重要的,但这可以根据设计决策很容易地改变。

请注意,您还将句子填充到可能的最大标记长度。

    def df_to_dataset(dataframe,
                  dataframe_labels,
                  batch_size = BATCH_SIZE,
                  max_length = MAX_LEN,
                  tokenizer  = tokenizer):
        """
        Loads data into a tf.data.Dataset for finetuning a given model.
        """
    while True:
        for i in range(len(dataframe)):
                if (i+1) % batch_size == 0:
                    multiplier = int((i+1)/batch_size)
                print(multiplier)
                    _df = dataframe.iloc[(multiplier-1)*batch_size:multiplier*batch_size,:]
                input_df_dict = tokenizer(
                    _df.CLEAN_TEXT.tolist(),
                    add_special_tokens=True,
                    max_length=max_length, # TO truncate larger sentences, similar to truncation = True
                    truncation=True,
                    return_token_type_ids=True,
                    return_attention_mask=True,
                        padding='max_length', # right padded
                )
                input_df_dict = {k:np.array(v) for k,v in input_df_dict.items()}
                    yield input_df_dict, mlb_fit.transform(dataframe_labels[(multiplier-1)*batch_size:multiplier*batch_size])

train_gen = df_to_dataset(train_X.reset_index(drop = True),
train_y)
val_gen = df_to_dataset(val_X.reset_index(drop = True),
val_y)

from tensorflow.keras import layers
    def create_final_model(bert_model = bert):

        input_ids = layers.Input(shape=(MAX_LEN,), dtype=tf.int32, name='input_ids')
        token_type_ids = layers.Input((MAX_LEN,), dtype=tf.int32, name='token_type_ids')
        attention_mask = layers.Input((MAX_LEN,), dtype=tf.int32, name='attention_mask')

    # Use pooled_output(hidden states of [CLS]) as sentence level embedding
        cls_output = bert_model({'input_ids': input_ids, 'attention_mask': attention_mask, 'token_type_ids': token_type_ids})[1]
        x = layers.Dense(512, activation='selu')(cls_output)
        x = layers.Dense(256, activation='selu')(x)
        x = layers.Dropout(rate=0.1)(x)
        x = layers.Dense(NUM_LABELS, activation='sigmoid')(x)
        model = tf.keras.models.Model(inputs={'input_ids': input_ids, 'attention_mask': attention_mask, 'token_type_ids': token_type_ids}, outputs=x)
    return model

model = create_final_model(bert_model = bert)

此外,请确保您只学习自定义层,至少对于几个第一时代;然后就可以学习全网了。为此,您将冻结 BERT 层,只训练自定义层。

for layers in bert.layers:
    print(layers.name)
    layers.trainable= False

让我们检查一下模型的外观;参见图 4-13 。特别注意可训练和不可训练参数的数量。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4-13

模型摘要

model.summary()

这里需要注意的一点是,您使用的是 sigmoid 函数,而不是 Softmax 函数,因为您试图识别特定的 ICD 码是否存在,因此 Softmax 足以满足相同的要求。

model.compile(optimizer= tf.keras.optimizers.Adam(learning_rate=LR),
                  loss='binary_crossentropy',
                  metrics=['AUC'])

由于这是一个大模型,可能需要很多时间来训练,因此建立一个 TensorBoard 来跟踪损失和 AUC 会很好。

# You can change the directory name
    LOG_DIR = 'tb_logs'

import os
if not os.path.exists(LOG_DIR):
    os.makedirs(LOG_DIR)

    tensorboard_callback = tf.keras.callbacks.TensorBoard(log_dir=LOG_DIR, histogram_freq=1)

    with tf.device('/device:GPU:0'):
    history = model.fit(train_gen,
                      steps_per_epoch=len(train_X)//BATCH_SIZE,
                      epochs=EPOCHS,
                      validation_data=val_gen,
                        callbacks=[tensorboard_callback])

您可以使用或不使用 GPU 来训练模型,但请确保您使用的硬件启用了 GPU。如果您没有设置,请重新查看第二章的注释。

要了解您是否有可用的 GPU,请运行以下命令:

tf.test.gpu_device_name()

对于 NVIDIA GeForce GTX 1660Ti,这种模型的训练在 CPU 上可能需要很多时间,在 GPU 上可能需要一点时间。一个历元大约需要四个小时,而在 CPU 机器上几乎需要五倍的时间。因此,我不会在这里讨论模型的结果。

这里有一些加强训练的想法:

  1. 在几个时期内,您可以保持 BERT 层冻结,但最终为了在下游任务中获得稍好的性能,您也可以解冻并训练 BERT 层的参数。

  2. 尝试使用一个更精炼的模型。经过提炼的模型是对参数需求较少的模型,在许多下游任务上实现了几乎相同的性能。这使得整体训练非常快。

  3. 另一个修改可以在数据集生成中进行。input_token_dict可以对全部数据进行处理,也可以对每批数据进行子集处理。

结论

好了,带着这些想法,我想结束这一章。在这一章中,你学习了变形金刚,多重注意力概念,以及伯特长度。您应用所有这些学到的概念,通过使用拥抱人脸库来训练多标签分类模型。

你在这一章学到的变形金刚的基础在未来几年将会非常重要,因为有很多论文试图利用变形金刚完成各种任务。它们被用于图像问题、药物预测、图形网络等等。

尽管人们越来越有兴趣在不损失太多性能的情况下更快地从这种模型中做出推断,但罗杰斯等人的论文“当伯特玩彩票时,所有的彩票都中奖”表明,您可以删除伯特的许多组件,它仍然有效。本文根据彩票假设分析了 BERT 修剪,发现即使是“坏”彩票也可以被微调到良好的准确度。它仍然是推进 NLU 边界的一个非常重要的里程碑。我劝你去读读 XLNext,Longformer and Reformer,Roberta 等。它们是其他基于 transformer 或受其启发的架构,在某些任务上比 BERT 表现得更好。您将使用 BERT 模型来开发问答系统。在此之前,继续阅读和学习。

五、使用图卷积网络从收据图像中提取结构化数据

就像任何其他销售工作一样,制药公司的销售代表总是在现场。在外地意味着产生大量报销食品和旅行的收据。跟踪不遵循公司准则的账单变得很困难。在本案例研究中,您将探索如何从收据图像中提取信息,并构建各种信息。

您还将学习如何在模板文档(遵循标准模板或实体集的文档)上使用不同的信息提取技术。您将构建从开箱即用的 OCR 到图形卷积网络(GCR)的信息提取用例。gcr 相对较新,属于图形神经网络类,这是一种正在积极研究和应用的思想。

数据

您将在此案例中使用的数据是 ICDAR 2019 年扫描收据 OCR 和信息提取数据集上的稳健阅读挑战。网站链接为 https://rrc.cvc.uab.es/?ch=13 .,在网站注册后可以很容易地从下载部分获得。您可能会发现博客/文章提到原始数据中的数据问题,因为一些数据被错误地标注,但这已被团队纠正。

您要做的是识别某些实体,即公司、日期、地址和总数。图 5-1 显示了一些带有标签及其值的图像样本。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-1

样本图像及其标签

数据集被分成训练/验证集(trainval)和测试集(test)。trainval 组包括 626 个收据图像,而测试组包含大约 361 个图像。

有两种标记数据可用:

  1. OCR 输出:数据集中的每个图像都用文本边界框(bbox)和每个文本 bbox 的副本进行注释。位置被标注为具有四个顶点的矩形,从顶部开始按顺时针顺序排列。

    1. 你可以简化这种表示。你实际需要的是(x min ,y min )和(x max ,y max ),分别是矩形的右上角和左下角。
  2. 节点标签:数据集中的每个图像都用一个文本文件进行了注释。

现在 OCR 输出级别没有标签,所以您必须找到一种方法将每个文本 bbox 建模为四个标签中的任何一个。

将节点标签映射到 OCR 输出

如果您仔细阅读标签和文字,您可以观察到某些情况,例如:

  1. OCR 文本被分成多行,而标签输出包含相同文本的串联版本。因此,您可以用两种方式进行子串搜索,因为有时标签文本比输出短,尤其是日期标签。

  2. 总额有时用货币报告,有时不用,所以这有点不一致,但应该没问题,因为您将只关注总额标签的数字部分。

让我们从加载数据开始。从比赛网站下载数据,解压,把文件夹名改成ICDAR_SROIE,然后为了更好的组织,把文件夹放在一个Data文件夹里。

您还将在目录中创建一个名为processed的文件夹来存储文本的边界框及其标签,但这并不简单,因为其中有一些细微差别,我将在本章中进一步讨论。

import pandas as pd
import numpy as np

import glob
import os

    PROCESSED_PATH = "./Data/ICDAR_SROIE/processed/"

# Loading ocr and label data
    receipt_train_img = {os.path.split(x)[-1].replace(".jpg",""):x for x in glob.glob("./Data/ICDAR_SROIE/0325updated.task1train(626p)/*.jpg") if not os.path.split(x)[-1].replace(".jpg","").endswith(")")}

    ocr_data = {os.path.split(x)[-1].replace(".txt",""):x for x in glob.glob("./Data/ICDAR_SROIE/0325updated.task1train(626p)/*.txt") if not os.path.split(x)[-1].replace(".txt","").endswith(")")}
    label_data = {os.path.split(x)[-1].replace(".txt",""):x for x in glob.glob("./Data/ICDAR_SROIE/0325updated.task2train(626p)/*.txt") if not os.path.split(x)[-1].replace(".txt","").endswith(")")}

# Checking if all the sets have the same number of labeled data
assert len(receipt_train_img) == len(ocr_data) == len(label_data)

接下来,创建三个函数:

  1. 读取 OCR 输出,只需保持(x min ,y min )和(x max ,y max ),即(x 1 ,y 1 )和(x 3 ,y 3 )。

  2. 将标签数据作为字典读取。

  3. 将 OCR 输出映射到标签。

import json
    def extract_ocr_data_fromtxt(file_path, key, save = False):
        """
        Extract the bounding box coordinates from txt and returns a pandas dataframe
        """
        with open(file_path, 'r') as in_file:
        stripped = (line.strip() for line in in_file)
            lines = [line.split(",")[:2] + line.split(",")[4:6] + [",".join(line.split(",")[8:])] for line in stripped if line]

            df = pd.DataFrame(lines, columns = ['xmin', 'ymin','xmax', 'ymax','text'])
        # Option to save as a csv
        if save:
            if not os.path.exists(PROCESSED_PATH):
                os.mkdir(PROCESSED_PATH)
                df.to_csv(os.path.join(PROCESSED_PATH,key + '.csv'), index =None)
        return df

    def extract_label_data_fromtxt(file_path):
        """
        Read the label json and return as a dictionary
        """
    with open(file_path) as f:
        json_data = json.load(f)
        return json_data

    def map_labels(text,k):
        """
        Maps label to ocr output using certain heuristics and logic
        """
    text_n = None
    k_n = None
    try:
        text_n = float(text)
    except Exception as e:
        pass

    try:
        k_n = float(k)
    except Exception as e:
        pass
    # if both are text then we are doing a substring match
    if (pd.isnull(text_n) and pd.isnull(k_n)):
        if (text in k) or (k in text):
            return True
    # if both are numerical then we just check for complete match
    elif (text_n is not None) and (k_n is not None):
        return text == k
    # special case to handle total, using endswith
    # as sometimes symbols are attached to ocr output
    elif (k_n is not None) and (text_n is None):
        return text.endswith(k)

    return False

注意映射函数map_labels并不是创建标签的完美方式。total 标签可能有很多误报,如图 5-2 所示,total 标签不匹配。但这并不经常发生,因此可以手动纠正或按原样标记。让我们保持标签不变。

最后,创建一个包装器函数,将映射的数据保存在一个单独的文件夹中。

    def mapped_label_ocr(key):
        """
        Wrapper function to yield result of mapping in desired format
        """
    data = extract_ocr_data_fromtxt(ocr_data[key],key)
    label_dict = extract_label_data_fromtxt(label_data[key])

        data['labels'] = ["".join([k for k,v in label_dict.items() if map_labels(text, v)]) for text in data.text]

    if not os.path.exists(PROCESSED_PATH):
        os.mkdir(PROCESSED_PATH)
        data.to_csv(os.path.join(PROCESSED_PATH,key + '.csv'), index =None)

    return data

# save the data
mapped_data = {key: mapped_label_ocr(key) for key in ocr_data.keys()}

让我们快速检查一下您应用的启发式方法是否有效。图 5-2 和 5-3 显示了两个用于比较的例子。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-3

示例 2:启发式标记

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-2

示例 1:启发式标记

这两个例子都表明,由于数据不一致,简单的子串搜索无法使用。因此,你要去模糊路线,并试图模糊搜索文本与一个非常高的地方截止。

为此,您将使用 fuzzywuzzy 包,这是一个非常有效的包,它提供了对各种类型的模糊匹配(Levenstein,phonical,等等)的访问。)以各种方式应用(令牌、字符级等。).

import json
from fuzzywuzzy import fuzz
    def extract_ocr_data_fromtxt(file_path, key, save = False):
        """
        Extract the bounding box coordinates from txt and returns a pandas dataframe
        """
    .....
    def extract_label_data_fromtxt(file_path):
        """
        Read the label json and return as a dictionary
        """
   ....

    def map_labels(text,k):
        """
        Maps label to ocr output using certain heuristics and logic
        """
   .....
    # if both are text then we are doing a fuzzy match
    if (pd.isnull(text_n) and pd.isnull(k_n)):
            if fuzz.token_set_ratio(text,k) > 90:
            return True
    .....

此外,有时公司名称会成为地址的一部分。为此,您需要修改您的包装函数并优先选择地址。

节点特征

为了用 GCN 对这些收据建模,您需要将它们转换成图形。在 OCR 过程中提取的每个单词都可以被视为一个单独的节点。

这些节点可以是以下类型:

  1. 公司

  2. 地址

  3. 日期

  4. 总数

  5. 不明确的

每个节点都有一个与之关联的特征向量,它将告诉我们该节点携带的数据。理想情况下,您可以使用任何高级 LM 模型从文本中提取信息,但是在这种特殊情况下,文本不需要大量的语义上下文,因此使用任何 LM 模型都是多余的。相反,您可以使用简单的文本特征生成流水线。您将生成以下要素:

  • SpecialCharacterCount:特殊字符总数

  • isFloat:如果文本表示浮点数,则该列的值为 1。

  • isDate:看文字是否代表日期。

  • TotalDistinctNumber:文本中有多少个不同的数字。与其他实体相比,地址通常包含许多数字(如门牌号、街道号和 Pin/邮政编码),因此这是一个有用的特性。

  • BigNumLength:最大数的长度。pin/邮政编码的长度将大于门牌号和行号。此外,帐单的总数可能是最高的数字。

  • IsContainsNum:文本是否包含数值实体。

  • POSTagDistribution:查看每段文字的下列位置标签的分布(总计数)。为此,您将使用空间位置标记( https://spacy.io/api/annotation#pos-tagging )

    • SYM:货币符号(票据总值可以有货币符号)

    • NUM:基数

    • CCONJ:连词(地址可以有很多连词)

    • PROPN:专有名词

所以每个节点总共有 10 个特性。

您将为已处理的数据帧维护一个内存中的对象,但我们也将它保存在一个单独的目录中,供以后参考

    PROCESSED_TEXT_PATH = "./Data/ICDAR_SROIE/processed_text_features"
if not os.path.exists(PROCESSED_TEXT_PATH):
    os.mkdir(PROCESSED_TEXT_PATH)

import spacy
import string
import collections
import re
from dateutil.parser import parse
from itertools import groupby

import en_core_web_sm
nlp = en_core_web_sm.load()

    def get_text_features(text):

    # SpecialCharacterCount
    special_chars = string.punctuation
    SpecialCharacterCount = np.sum([v for k, v in collections.Counter(text).items() \
                  if k in special_chars])

    # isFloat
    try:
        float(text)
            isFloat = 1
    except Exception as e:
            isFloat = 0

    # isDate
    try:
        parse(text, fuzzy=True)
            isDate = int(True and len(text) > 5)
    except Exception as e:
            isDate = 0

    # TotalDistinctNumber
        num_list = re.findall(r"(\d+)", text)
    num_list = [float(x) for x in num_list]

    TotalDistinctNumber = len(num_list)

    # BigNumLength
        BigNumLength = np.max(num_list) if TotalDistinctNumber > 0 else 0

    # DoesContainsNum
        DoesContainsNum = 1 if TotalDistinctNumber > 0 else 0

    # POSTagDistribution
    spacy_text = nlp(text)
    pos_list = [token.pos_ for token in spacy_text]

    POSTagDistribution = {}

        for k in ['SYM','NUM','CCONJ','PROPN']:
            POSTagDistribution['POSTagDistribution' + k] = [0]

        POSTagDistribution.update({'POSTagDistribution'+ value:    [len(list(freq))] for value, freq in groupby(sorted(pos_list)) if        value in ['SYM','NUM','CCONJ','PROPN']})

    pos_features = pd.DataFrame.from_dict(POSTagDistribution)
    other_features = pd.DataFrame([[SpecialCharacterCount, isFloat, isDate,
                                  TotalDistinctNumber, BigNumLength, DoesContainsNum]],
                                      columns = ["SpecialCharacterCount","isFloat","isDate", "TotalDistinctNumber","BigNumLength", "DoesContainsNum"])

        df = pd.concat([other_features, pos_features], axis = 1)
    return df

如前所述,您将使用文本值创建 10 个要素。虽然代码是不言自明的,但仍有一些问题需要讨论。

  • 您正在使用 dateutil 包来提取和识别日期值,但是它并不完美,导致了许多误报,所以现在有了另一个条件,即文本的长度应该至少为 5。这消除了被捕获的误报。

  • 就性能而言,itertools 是一个非凡的包,因此您应该始终尝试在您的应用程序中利用它。还有其他方法可以获得列表元素的频率,但这种方法确实很好,也是最优的。

将结果存储在单独的数据帧中。

mapped_data_text_features = {}
for k, v in mapped_data.items():
        _df = pd.concat([get_text_features(x) for x in v.text], axis = 0)
        final_df = pd.concat([v.reset_index(drop = True), _df.reset_index(drop = True)], axis = 1)
        final_df.to_csv(os.path.join(PROCESSED_TEXT_PATH,k+".csv"), index = None)
    mapped_data_text_features[k] = final_df

在你进一步阅读本章之前,还有两件事需要了解。

  1. 在你的数据集中,没有给出单词和节点之间的联系。

  2. 如何决定训练的输入数据?是批量节点还是单个节点矩阵?

分层布局

Lohani 等人在他们题为“使用图形卷积网络的发票读取系统”的论文中讨论了如何为发票系统的节点/单词建模。这些关系是在最近邻概念的基础上形成的。见图 5-4 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-4

基于最近邻概念创建边。图片来源:Lohani 等人题为“使用图卷积网络的发票读取系统”的论文

每个字节点在每个方向上只有一个邻居。这可以推广到本案例研究之外的任何半结构化文档图建模问题。

作者在他们的论文中提出了两个主要步骤来创建这种分层布局。

谱线形成
  1. 根据顶部坐标 对单词进行排序。

  2. **将行组成一组单词,**遵守以下规则:

    如果 Top(Wa) ≤ Bottom(Wb)和 Bottom(Wa) ≥ Top(Wb ),则两个词(Wa 和 Wb)在同一行

  3. 根据左边坐标对每行单词进行排序

这给出了链接形成的方向。你从左上角开始,在右下角结束。这也确保了您只看到一个单词/节点一次。

import itertools
    def get_line_numbers(key):
        """
        Get line number for each word.
        """
    ################ 1 ##################

    df = mapped_data_text_features[key]
        df.sort_values(by=['ymin'], inplace=True)
    df.reset_index(drop=True, inplace=True)

    # To avoid spacing issue, lets reduce ymax by some small value
        df["ymax"] = df["ymax"].apply(lambda x: int(x) - 0.5)

    ################ 2 ##################
    # In order to get line number we start with left most word/phrase/node
    # and then check all non-matching words and store their indices from L->R
    word_idx = []
    for i, row in df.iterrows():
        flattened_word_idx = list(itertools.chain(*word_idx))
        #print(flat_master)
        # check if the word has not already been checked
        if i not in flattened_word_idx:
                top_wa = int(row['ymin'])
                bottom_wa = int(row['ymax'])

            # Store the word
            idx = [i]

            for j, row_dash in df.iterrows():
                if j not in flattened_word_idx:
                # check a different word, double check
                    if not i == j:
                            top_wb = int(row_dash['ymin'])
                            bottom_wb = int(row_dash['ymax'] )
                        # Valid for all the words next to Wax
                        if (top_wa <= bottom_wb) and (bottom_wa >= top_wb):
                            idx.append(j)
                            #print(line)
            word_idx.append(idx)

    # Create line number for each node

        word_df = pd.DataFrame([[j,i+1] for i,x in enumerate(word_idx) for j in x], columns= ["word_index","line_num"])

    # put the line numbers back to the list
        final_df = df.merge(word_df, left_on=df.index, right_on='word_index')
        final_df.drop('word_index', axis=1, inplace=True)

    ################ 3 ##################
        final_df = final_df.sort_values(by=['line_num','xmin'],ascending=True)\
                .groupby('line_num').head(len(final_df))\
            .reset_index(drop=True)
        final_df['word_id'] = list(range(len(final_df)))

    return final_df

因为轴是颠倒的,

  1. 顶部坐标是 Ymin(最左边的坐标)。

  2. 您需要运行两个for循环,将数据帧中的每个单词与其他单词的垂直位置进行比较。

  3. 最终数据帧的输出按其行号排序。

Note

上述策略在大量重叠边界框的情况下可能会失败,但现在不是这种情况,所以我们可以接受。

最后,将结果存储在一个单独的变量中。

mapped_data_text_features_line = {key:get_line_numbers(key) for key,_ in mapped_data_text_features.items()}

接下来,作者讨论了实际链接形成的图形形成。

图形建模算法
  1. 从最上面的一行到最下面的一行,阅读每行的单词。

  2. 对于每个单词,执行以下操作:

    1. 用它检查垂直投影中的单词。

    2. 计算每个人的 RDL 和 RDR。

    3. 选择水平方向上具有最小 RDL 和 RDR 量值的最近邻单词,前提是这些单词在该方向上没有边缘。

      1. 如果两个单词具有相同的 RDL 或 RDR,则选择具有较高顶部坐标的单词。
    4. 类似地重复步骤 2.1 到 2.3,通过进行水平投影,计算 RDT 和 RDB,并在模糊的情况下选择具有较高左坐标的单词,来检索垂直方向上的最近邻单词。

    5. 在一个单词和它的四个最近的邻居(如果有的话)之间画边。

首先,让我们创建一个目录来保存连接节点图。

    GRAPH_IMAGE_PATH = "./Data/ICDAR_SROIE/processed_graph_images"

if not os.path.exists(GRAPH_IMAGE_PATH):
    os.mkdir(GRAPH_IMAGE_PATH)

然后,创建一个包含不同信息的类,即:

  • 连接列表:包含连接节点信息的嵌套列表

  • G : Networkx 图形对象。Networkx 是用于处理网络对象的 Python 库。

  • 已处理数据帧:包含节点连接的数据帧。

    class NetworkData():
        def __init__(self, final_connections, G, df):
        self.final_connections = final_connections
        self.G = G
        self.df = df
        def get_connection_list():
        return self.final_connections
        def get_networkx_graph():
        return self.G
        def get_processed_data():
        return self.df

Note

在这里,您可以使用 getter 函数,也可以只引用类对象。

import networkx as nx
from sklearn.preprocessing import MinMaxScaler
    def graph_modelling(key, save_graph =False):

    # Horizontal edge formation

    df = mapped_data_text_features_line[key]
        df_grouped = df.groupby('line_num')

    # for directed graph
    left_connections = {}
    right_connections = {}

    for _,group in df_grouped:
            wa = group['word_id'].tolist()
        #2
        # In case of a single word in a line this will be an empty dictionary
            _right_dict = {wa[i]:{'right':wa[i+1]} for i in range(len(wa)-1) }
            _left_dict = {wa[i+1]:{'left':wa[i]} for i in range(len(wa)-1) }

        #add the indices in the dataframes
            for i in range(len(wa)-1):
                df.loc[df['word_id'] == wa[i], 'right'] = int(wa[i+1])
                df.loc[df['word_id'] == wa[i+1], 'left'] = int(wa[i])

        left_connections.update(_left_dict)
        right_connections.update(_right_dict)

    # Vertical edge formation

    bottom_connections = {}
    top_connections = {}

    for i, row in df.iterrows():
        if i not in bottom_connections.keys():
            for j, row_dash in df.iterrows():

                # since our dataframe is sorted by line number and we are looking for vertical connections
                # we will make sure that we are only searching for a word/phrase next in row.
                if j not in bottom_connections.values() and i < j:
                        if row_dash['line_num'] > row['line_num']:
                        bottom_connections[i] = j

                        top_connections[j] = i

                        #add it to the dataframe
                            df.loc[df['word_id'] == i , 'bottom'] = j
                            df.loc[df['word_id'] == j, 'top'] = i

                        # break once the condition is met
                        break

    # Merging Neighbours from all 4 directions
    final_connections = {}

    # Taking all the keys that have a connection in either horizontal or vertical direction
    # Note : Since these are undirected graphs we can take either of (right, left) OR (top, bottom)
    for word_ids in (right_connections.keys() | bottom_connections.keys()):
            if word_ids in right_connections: final_connections.setdefault(word_ids, []).append(right_connections[word_ids]['right'])
        if word_ids in bottom_connections: final_connections.setdefault(word_ids, []).append(bottom_connections[word_ids])

    # Create a networkx graph for ingestion into stellar graph model

    G = nx.from_dict_of_lists(final_connections)

    # Adding node features
    scaler = MinMaxScaler()
        scaled_features = scaler.fit_transform(df[['SpecialCharacterCount', 'isFloat', 'isDate', 'TotalDistinctNumber',
           'BigNumLength', 'DoesContainsNum', 'POSTagDistributionSYM',
           'POSTagDistributionNUM', 'POSTagDistributionCCONJ',
           'POSTagDistributionPROPN', 'line_num']])
    node_feature_map = {y:x for x,y in zip(scaled_features, df.word_id)}

    for node_id, node_data in G.nodes(data=True):
            node_data["feature"] = node_feature_map[node_id]

    if save_graph:
        # There are multiple layouts but KKL is most suitable for non-centric layout
        layout = nx.kamada_kawai_layout(G)

        # Plotting the Graphs
            plt.figure(figsize=(10,5))
        # Get current axes
        ax = plt.gca()
            ax.set_title(f'Graph form of {key}')
        nx.draw(G, layout, with_labels=True)
            plt.savefig(os.path.join(GRAPH_IMAGE_PATH, key +".jpg"), format="JPG")
        plt.close()

    networkobject = NetworkData(final_connections, G, df)
    return networkobject

代码非常直观。这些是代码中发生的高级事情:

  1. 水平连接

    1. 它们只能在同一行中的单词之间形成,因此您可以根据行号对处理的数据进行分组。

    2. 为了更清楚起见,您维护了右连接和左连接,但是对于无向图,右连接字典就足够了。

  2. 垂直连接

    1. 它们永远不能在属于同一行的单词之间形成。

    2. 使用了两个for循环,因为您必须沿着不同的线路遍历。

    3. 同样,方向是不相关的,但保持清晰。

  3. 右侧和底部字典都用于为 networkx 图创建邻接表。

  4. 最后,缩放和归一化结点要素。您还将行号作为特征之一,因为它是带有地址/公司编号等的临时文档。在顶部出现,总在底部出现。

调用上面的代码,你得到的结果如图 5-5 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-5

不同账单的网络布局示例

mapped_net_obj = {key: graph_modelling(key, save_graph=True) for key,_ in mapped_data_text_features_line.items()}

输入数据流水线

在你正在使用的星图库中,你不能训练不同的网络,但是可以使用所有网络的联合。这将导致一个具有大邻接矩阵的大图。参见图 5-6 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-6

多重图的并与训练过程。来源:github . com/tkipf/gcn

您将利用 Networkx 中的内置功能。

    U = nx.union_all([obj.G for k,obj in mapped_net_obj.items()], rename=[k+"-" for k in mapped_net_obj.keys()])

现在,既然您终于有了想要的数据,是时候详细了解一下图和图卷积网络了。

什么是图表,我们为什么需要它们?

在计算机科学理论中,我们将图定义为一种数据结构,它由一组有限的顶点(也称为节点)和一组连接这些节点的边组成。根据图是有向的还是无向的,图的边可以是有序的或无序的。

)

)

)

除了边的方向,不同类型的图之间还有其他区别:

  • 一个图可以加权也可以不加权。在加权图中,每条边都有一个权重。

  • 如果一个无向图 G 的每一对不同的顶点之间都有一条路,则称 G 为连通

  • 简单的图没有自循环,这意味着没有边连接顶点和它自己。

所以可以有多种术语和方法来区分图形。现在问题来了,为什么我们会关心机器学习中的图呢?

本书的大多数读者通常熟悉四种类型的数据,即

  1. 文本

  2. 结构化/表格化

  3. 声音的

  4. 形象

所有这些数据都可以由众所周知的神经网络架构来表示,但有一类特殊的数据不能,它被称为非欧几里德数据集。与上述 1D 或 2D 数据集相比,此类数据集可以更精确地表示更复杂的项目和概念。

让我们明白这一点。

比方说你要分类一句话:

  1. 约翰是个好人。

在案例 1 中,您只有 pos 标签,您可以很好地在 GRU/LSTM/RNN 单元格中对其建模,以分类捕获单词之间的线性和非层次连接。

但是,在第二种情况下,您还会得到关于它们之间的依赖关系的信息。你打算如何为他们建模?参见图 5-7 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-7

建模连接的数据,恰当的例子

这就是图表的用武之地。它们可以帮助您自然而有效地对这种层次结构进行建模,比其他数据集(如社交网络数据、化学分子数据、树/本体和流形)更有效,这些数据集通过层次结构、互连性和多维度保存丰富的信息。在这些情况下,图更适合。

图形有助于建模这样的非欧几里德数据库。它还允许我们表示节点的内在特征,同时还提供关于关系和结构的信息,并且非常容易表示为用于学习的神经网络。

大多数神经网络可以归类为称为多部图的东西,这基本上是可以分成不同节点集的图。这些节点集不与同一集的节点共享边。参见图 5-8 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-8

作为多部图的神经网络

在计算机系统中,图形是用一种叫做邻接矩阵的东西来表示的。邻接矩阵由连接的实体之间的边权重组成。它显示了图的三个最重要的属性:

  • 关系

  • 关系强度(边缘权重)

  • 关系的方向

有向图的邻接矩阵不会沿着对角线对称,因为有向图的边只朝一个方向。对于无向图,邻接矩阵总是对称的。

此外,度数显示了图形的复杂程度。一个顶点的度数表示与之相连的顶点总数。在无向图中,它是连接的组件的简单总和,而对于有向图,根据关系的方向,度数被进一步分为入站和出站度数。参见图 5-9 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-9

具有邻接矩阵的无向图

捕捉图形信息的另一个矩阵是拉普拉斯矩阵。

)

度矩阵中的每个值减去邻接矩阵中相应的值。拉普拉斯矩阵基本上有助于确定图形函数有多平滑。换句话说,当从一个顶点移动到下一个顶点时,值的变化不应该是突然的。对于密集连接的集群来说更是如此。然而,对于孤立的节点,平滑度会降低,各种任务的性能也会降低,这可以使用图形来完成,因此图形连接得越多,它包含的信息就越多。

图形卷积网络

图的卷积

图形卷积网络的工作原理是将卷积应用于图形网络。但这意味着什么呢?让我们看看。

你理解卷积传统上,给定一个输入图像表示,你试图学习一个核矩阵,这有助于你从相邻像素聚集信息。参见图 5-10 中的图示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-10

图像的卷积运算

这里发生的主要事情是,您可以从相邻像素聚集信息。这是为各种任务建模图形数据时借用的概念,例如

  1. 节点分类:预测节点的类型。

  2. 链路预测:预测任意两个节点之间是否正在形成新的连接/链路

  3. 社区检测:识别在图中是否有任何确定的集群形成,很大程度上类似于密集链接的集群,但在统计意义上更大。(想象一下 PageRank。)

  4. 网络相似度:如果该图或其子网与另一个图或其子网相似。

卷积处理图形数据的方式有一些细微的差别。

  1. 图像具有刚性(有很强的方向感,以至于将一个像素值从中心像素的左侧移动到右侧会改变意义)和规则(像素在几何上等距)连接结构。但是图表肯定不会。

  2. 图形学习应该不管输入数据的大小而工作。

就像像素是用来表示图像一样,在图中也有称为节点特征和边特征的东西。

节点特征在语义上标识节点是关于什么的,而边特征可以帮助标识两个节点之间共享的不同关系。

我将要谈论的网络是 Kipf 和 Wellling 在 2017 年题为“使用图形卷积网络的半监督分类”的论文中提出的 GCN 网络。此网络不考虑边缘功能,但您不会将它们用于您的应用程序。大多数复杂网络都需要边要素。正如在分子化学中,双键比单键强得多,所以两者不能以同样的方式处理;它们必须以不同的方式使用。但是,在您的案例中,边代表发票中不同文本实体之间的连接,因此具有相同的含义,因此您可以取消可以模拟边要素的网络。

但对于好奇的人来说,这里有两篇论文是由 Kipf 和 Welling 分享的对 GCN 建筑的改进:

  • 吉尔默等人于 2017 年在 ICML 创作的《MPNN》

  • 2018 年 ICLR veli kovi 等人的“图形注意力网络”

周、崔、张等人在“图神经网络:方法与应用综述”中的论文也给出了很好的综述

了解 GCNs

图 5-11 解释了卷积如何在图数据上工作,给定一个无向图 G = (V,e)具有节点 v i ∊ V,边(v i ,v j ) ∊ E 和一个大小为 NXN 的邻接矩阵(a ),其中 n 表示节点的数量,特征矩阵(h)的大小为 NXK,其中 k 是特征向量的维数。要从每个节点的邻居中找到特征值,您需要将矩阵 A 和 h 相乘。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-11

图的卷积运算

正如您在更新的节点功能矩阵中看到的,有两点可以改进:

  1. 您可以防止由于节点连接程度的差异而导致的规模问题。某些节点是高度连接的,而一些不是,因此自然地,与稀疏连接的节点相比,高度连接的节点将具有更高的特征值。

  2. 每个节点已经完全忘记了自己的特性,全部从标签中学习,所以你需要确保当前的信息没有完全丢失。

首先,您要确保每个节点也能够保留来自自身的信息。为此,您通过添加一个连接到它自身来更新图,这基本上意味着邻接矩阵现在沿着对角线都是 1。

由于比例可以通过用节点的度数进行归一化来校正,所以您将这些值乘以 D -1 。由于 D 是一个对角矩阵,D -1 只是往复所有对角元素。注意,这个 D 矩阵是在上面创建的自循环之后更新的矩阵。

Kipf 和 Welling 在他们提出的想法中指出,与高度连接的层相比,度数较低的节点将对其邻居施加更多的影响。基本上,向所有节点传递信息的节点不会提供任何关于节点的“独特”信息。为此,作者建议将 D -1 AH 的结式矩阵与 D -1 相乘。因为你要规格化两次,所以你要确保除以)。这样,在计算第 I 个节点的聚合要素表示时,不仅要考虑第 I 个节点的度,还要考虑第 j 个节点的度。这也被称为光谱规则。

在这个想法中需要注意的一点是,Kipf 等人提出这个想法时要记住,edge 在这里没有任何作用。如果即使是高度连通的节点的连接也有不同的边特征,那么上述假设并不总是成立。

最后,更新后的节点特征矩阵如下所示:

H 更新 = f ( ADH )

一个完整的方程大概是这样的:

)

Relu 或任何其他非线性激活可以应用于此。这里 W 是大小为(KxK’)的可训练权重矩阵,其中 K’是下一层的特征向量的维数。这基本上有助于通过随深度减小尺寸来解决过拟合问题。

GCNs 中的层堆叠

图形的所有邻居都以这种方式更新。一旦所有的节点都更新了它们的直接邻居,就有了第一层的输出。

第二层也从二级连接中获取信息,这基本上意味着,由于在第一步中每个节点已经从其子节点中建模了信息,如果在下一层中再次运行相同的步骤,子节点的子节点的这些特征将被添加到父节点中。基本上,网络越深,本地的邻域就越大。参见图 5-12 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-12

GCN 中的图层。图片来源 helper。ipam。加州大学洛杉矶分校。edu/publications/glw S4/glw S4 _ 15546。pdf

培养

对于像您这样的节点分类问题,培训主要包括以下步骤。

  1. 通过 GCN 层执行前向传播。

  2. 按行应用 sigmoid 函数(即,针对 GCN 中最后一层的每个节点)。

  3. 计算已知节点标签上的交叉熵损失。

  4. 反向传播损失并更新每层中的权重矩阵 W。

注意,存在最终权重矩阵,其将每个节点的最终隐藏状态表示与节点分类任务预期的类的数量进行映射。所以,如果你把类的数量称为 C,这个权矩阵的形状是(K ',C)。假设节点的最后特征表示具有 K’的维度,权重矩阵的总数= L+1,其中 L 是 GCN 层数。

建模

虽然在 TensorFlow 中构建自己的 GCN 层并不困难,但有一些库通过提供预构建的 API,使 Keras 和 TF 2.0 更容易进行图形深度学习。一个这样的库是 StellarGraph。它的官方 GitHub 上有超过 17k 颗星,并且有一个活跃的社区。StellarGraph 可以从各种数据源(networkx graphs、pandas,甚至 numpy 数组)获取数据。

因为您已经准备好了所有图的并集,所以让我们直接从 networkx 加载您的数据。

    G_sg = sg.from_networkx(U, node_features="feature")
print(G_sg.info())

####################### Output ################
StellarGraph: Undirected multigraph
     Nodes: 33626, Edges: 46820

 Node types:
      default: [33626]
        Features: float32 vector, length 11
    Edge types: default-default->default

 Edge types:
        default-default->default: [46820]
            Weights: all 1 (default)
        Features: none

如你所见,总共有 33626 个节点和 46820 条边。这仍然是一个小图,但它对训练目的非常有用。

训练测试分割和目标编码

接下来,确保在节点 id 和目标之间有一对一的映射。为此,您将从已处理的数据中创建此数据,并将所有空标签替换为“others”

    labelled_data = pd.DataFrame([[k+"-"+str(node_idx), label]
                 for k,obj in mapped_net_obj.items()\
                for node_idx,label in zip(obj.df.word_id,obj.df.labels)],
                                 columns = ["node_id","node_target"])

    labelled_data = labelled_data.replace(r'^\s*$', "others", regex=True)

目标类的分布如下所示。

    |    | index   |   node_target |
    |---:|:--------|--------------:|
    |  0 | others  |         28861 |
    |  1 | address |          1692 |
    |  2 | total   |          1562 |
    |  3 | date    |           764 |
    |  4 | company |           747 |

最有代表性的是other类,这是意料之中的。在节点预测中存在一些类别不平衡。我敦促你尝试纠正这种不平衡,然后重新训练模型。

最后,在创建模型之前,让我们也创建您的训练和验证数据。

您还将对多类输出进行二值化,并为多类分类问题设置模型。

    train,val = model_selection.train_test_split(labelled_data, random_state = 42,train_size = 0.8, stratify = labelled_data.node_target)

# Encoding the targets
target_encoding = preprocessing.LabelBinarizer()
train_targets = target_encoding.fit_transform(train.node_target)
val_targets = target_encoding.fit_transform(val.node_target)

在 StellarGraph 中创建培训流程

接下来,您将使用一个内置的generator函数来生成恒星网络图中的节点批次。

generator = FullBatchNodeGenerator(G_sg)

一旦创建了生成器对象,您就可以调用flow函数并传递目标标签和节点来获得一个可以用作 Keras 数据生成器的对象。

train_flow = generator.flow(train.node_id, train_targets)
val_flow = generator.flow(val.node_id, val_targets)

训练和模型性能图

你形成了一个非常基本的 Keras 模型。您添加了两个大小为 8 和 4 的 GCN 图层。这两层也暗示着你要去的是每个节点的二级邻居。对于每个激活(节点嵌入),使用 SELU 激活函数来防止渐变消失问题。

你还引入了一个辍学,以防止过拟合。

因为您的输入和输出是使用generator对象创建的,您将从 GCN 层获得输入和输出张量来了解输入和输出。

最后,输出被送入一个密集层,其形状等于目标标签的数量。基本上,每个节点嵌入乘以最终权重矩阵,然后应用激活来查看哪个类最有可能用于该节点。参见图 5-13 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-13

GCN 模式总结

# Model Formation
# two layers of GCN
    gcn = GCN(layer_sizes=[8, 4], activations=["selu", "selu"], generator=generator, dropout=0.5)
# expose in and out to create keras model
x_inp, x_out = gcn.in_out_tensors()

# usual output layer
    predictions = layers.Dense(units=train_targets.shape[1],     activation="softmax")(x_out)

# define model
model = Model(inputs=x_inp, outputs=predictions)
# compile model
model.compile(
        optimizer=optimizers.Adam(lr=0.01),
    loss=losses.categorical_crossentropy,
        metrics=["AUC"])

正如您所看到的,您可以引入更多的参数,并创建一个更加有效的模型。但是现在模型的性能对于你的任务来说还过得去。

现在拟合模型并检查结果。

from tensorflow.keras.callbacks import EarlyStopping
    es_callback = EarlyStopping(monitor="val_auc", patience=10, restore_best_weights=True)

history = model.fit(
    train_flow,
        epochs=10,
    validation_data=val_flow,
        verbose=2,
    callbacks=[es_callback])

    Epoch 1/10
    1/1 - 1s - loss: 1.7024 - auc: 0.4687 - val_loss: 1.5375 - val_auc: 0.7021
    Epoch 2/10
    1/1 - 0s - loss: 1.5910 - auc: 0.5962 - val_loss: 1.4360 - val_auc: 0.8740
    Epoch 3/10
    1/1 - 0s - loss: 1.4832 - auc: 0.7261 - val_loss: 1.3445 - val_auc: 0.9170
    Epoch 4/10
    1/1 - 0s - loss: 1.3891 - auc: 0.8178 - val_loss: 1.2588 - val_auc: 0.9189
    Epoch 5/10
    1/1 - 0s - loss: 1.2993 - auc: 0.8753 - val_loss: 1.1768 - val_auc: 0.9175
    Epoch 6/10
    1/1 - 0s - loss: 1.2219 - auc: 0.8958 - val_loss: 1.0977 - val_auc: 0.9160
    Epoch 7/10
    1/1 - 0s - loss: 1.1405 - auc: 0.9068 - val_loss: 1.0210 - val_auc: 0.9146
    Epoch 8/10
    1/1 - 0s - loss: 1.0638 - auc: 0.9120 - val_loss: 0.9469 - val_auc: 0.9134
    Epoch 9/10
    1/1 - 0s - loss: 0.9890 - auc: 0.9131 - val_loss: 0.8767 - val_auc: 0.9129
    Epoch 10/10
    1/1 - 0s - loss: 0.9191 - auc: 0.9140 - val_loss: 0.8121 - val_auc: 0.9120

更高历元的训练在第 18 个历元达到早期停止标准,并产生如图 5-14 所示的训练和验证曲线。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-14

训练和验证性能曲线

看起来不存在过拟合的情况,但是您肯定可以使用更多的参数并尝试提高性能。可以进行以下更改:

  1. 包括该节点的更多特征。

  2. 尝试除最小-最大缩放器之外的不同归一化技术。

  3. 更密集的预测模型有助于更好地捕捉细微差别。

    1. 请注意,从 GCN 层获得输入和输出后,可以像构建任何正常的 Keras 模型一样构建模型。
  4. 处理阶级不平衡。

结论

我希望你对被介绍到这种新的神经网络感到兴奋。它为处理真实世界的数据打开了许多大门。不要把自己局限在这里讨论的 GCN 模型中。外面有一个巨大的知识海洋!

你学到了一项一流的技术,但 GCN 也有不足之处:

  • 它不考虑节点边。

  • 它对同构图形(具有单一类型节点/边的图形)非常有用。

  • 节点局部性在其分类中仍然起着很好的作用。尝试通过从特征集中移除行号参数并重新建模来进行一些消融建模。

我们生活的世界是紧密相连的,随着我们在本世纪的前进,这种联系将会更加紧密。用如何对这种数据建模的知识武装自己将是一项受人尊敬的技能,并且肯定会有助于推进你的职业和兴趣。

六、处理医疗保健中低训练数据的可用性

训练数据的可用性是机器学习应用中的一个关键瓶颈。通过在医疗保健等专业领域工作,这一点得到了进一步增强,在这些领域中,人们需要非常熟练地理解数据,然后标记或标注数据,以供机器学习使用。除了寻找技能管家之外,组织还需要在时间和成本方面进行大量投资。

你已经学会了一种处理有限信息可用性的方法,那就是迁移学习。与迁移学习不同,迁移学习是一种处理低训练数据的算法方法,在本章中,您将使用数据优先的方法,尝试理解和建模数据,以便创建训练标签。

您将了解处理低训练数据的不同方法以及应用这些方法的挑战。最后,您将通过一个实践案例来探索如何使用通气管为生物医学关系抽取增加训练数据。

介绍

创建具有高质量训练标签的数据集需要投入大量的时间和金钱,有时甚至需要高度专业化领域的领域专家。因此,我们必须找到更聪明的方法,以这样或那样的方式利用我们未标记数据的数据模式,帮助我们在看不见的数据上创建训练标签。

半监督学习

半监督学习涉及使用小的金标数据集和未标记数据。半监督学习有四个关键步骤:

  1. 你使用少量的金标数据来训练一个选择的模型,很像标准的监督学习。

  2. 然后,使用未标记的数据来预测使用训练好的模型标签的输出。由于该模型仅在少数样本上训练,因此很难说预测是高度准确的,因此来自这种模型的标签输出被称为伪标签。

  3. 然后,收集黄金标签数据和大量伪标签数据,并创建一个新的训练集。

  4. 您使用这个新集合重新训练您的模型。

  5. 重复这个过程,直到性能指标图表(跨时段)变平。

在第五章中,你处理了节点分类问题,你必须预测公司名称、地址、日期和账单的总成本。可用的训练数据较少,但您能够在训练标签上以合理的准确度进行预测,因为模型不仅学习节点特征,还学习其边连接,因此强大的图形神经网络可以很好地学习这个小数据集。

尽管任何模型都可以用于在一个小的金标数据集加上伪标签上进行训练,但是有两个主要的模型策略已经被广泛利用。

甘斯

发电商敌对网络(GANs)包括两个互为对手的网络,它们相互竞争直到达到理想的平衡状态。这两个网络是发生器和鉴别器。见图 6-1 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-1

发电机对抗网络

生成器:学习生成真实数据

鉴别器:学习鉴别发电机的假数据和真数据

训练 GAN 的关键步骤包括

  • 来自真实数据和虚假数据的样本被用来单独训练鉴别器。在这里,虚假数据是由噪声分布产生的。

  • 那么鉴别器的权重被冻结,并且生成器被训练。

  • 或者,这些网络被训练,彼此竞争,直到它们达到平衡状态(梯度流正常化)。

训练两个网络所涉及的损失函数是基于鉴别器网络的真实与虚假预测。在使用 GANs 的半监督学习中,鉴别器网络不仅输出真实或虚假的分布,还输出所有相关标签的分布。

如果输入被归类为任何类别标签,则被归类为真实,如图 6-2 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-2

半监督 GAN 架构

鉴别器现在有一个双重目标,首先区分真实图像和虚假图像(也称为非监督任务),其次将真实图像分类到它们各自的类别(监督任务)。

对于每个迭代,您执行以下操作:

  • 训练受监督的鉴别器。取一批训练标签,训练多类网络。

  • 训练无监督鉴别器。取一批未标记数据和一批伪样本,训练二进制分类器,反向传播二进制损失。

  • 训练发电机(就像简单的 GAN 一样)。

阅读 Odena 题为“使用生成对抗网络的半监督学习”的论文,进一步深入了解半监督学习在 GANs 中的使用。

自编码器

我在第三章介绍了自编码器,在那里您使用它们来编码您的训练特征,以获得一个低维、密集的表示,以便它可以用于聚类。见图 6-3 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-3

Vanilla 自编自编自编自编自编自编自编自编自编自编自编自编自编自编自编自编自编自编自编自编自编自编自编自编自编自编

这个想法还是一样的,但是这次不仅仅是优化重建损失,还将使用低维密集向量来预测输出。见图 6-4 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-4

半监督学习的自编码器

现在,你们中的许多人可能会想,这些重建损失有时并不那么低,因此在瓶颈层中可能会得到次优的表示。嗯,不完全是这样。

您不需要捕获输入的所有语义来预测标签。您可以使用捕捉部分含义的制图表达,以便将损失 1 降至最低。

尽管如此,当一般表示(损失 2)也有助于预测类别标签时,实现了最佳结果。

人们已经先行一步,尝试了不同的方法来最小化损失 1 和损失 2。你可以阅读的一些文件是

  • Valpola 等人的“利用梯形网络的半监督学习”

  • “探索用于生物医学关系抽取的半监督变分自编码器”

迁移学习

你在第四章中详细探讨了迁移学习及其在自然语言任务中的工作原理。迁移学习的工作原理是使用相似领域的大量标记数据来训练神经网络,这样它就可以很好地学习较低级别的特征,然后您可以使用该架构,使用您拥有的少量标记数据来微调手头的任务。

这是一种非常强大的技术,但是它有一些限制:

  • 您的任务的输入数据可能与这种预训练网络的训练集有很大不同。

  • 预训练的任务和新的任务有很大的不同,例如分类和跨度提取。

  • 过拟合和不必要的使用大型模型:有时你的任务不需要使用复杂的数百万个参数,所以在这些情况下迁移学习可能是多余的。

迁移学习也可以在完全无监督的环境中使用,在这种环境中不需要大量的训练标签。这也叫自我监督。例如,当您训练一个好的语言模型时,您尝试执行以下操作:

  1. 掩蔽语言建模

  2. 下一句预测

这两种技术都不需要带标签的数据集,但却给出了一个能够完成各种任务的真正好的网络。

弱监督学习

弱监督学习是处理有限数据的另一种方式。这里的想法是利用当前数据中的模式,使用嘈杂的、启发式的和有限的来源来标记数据。

与上面讨论的技术一样,它有效地缓解了需要大量训练数据才能完成 ML 任务的问题。阅读斯坦福人工智能实验室团队的论文“浮潜:弱监督下的快速训练数据创建”,该论文探索了弱监督的工作原理。见图 6-5 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-5

知识来源多样,监管不力。资料来源:ai.googleblog.com

在这一章中,你将会用到弱学习的概念。你还将探索由斯坦福人工智能实验室开发的潜水图书馆。

探索浮潜

sculpt 是一个编程库,便于创建、建模和管理训练数据集,而无需手动标记。其工作流程是围绕数据编程设计的,由三个阶段组成:

  1. 写标签功能/弱监管

    这包括使用手工设计的功能、利用外部数据库的远程监督功能等。这些标记函数没有很好的回忆,但相当精确。如果选择次精度函数,其召回率通常会更高。因此,您的标注函数集应该是两种类型的混合。

    通气管中的标签功能是用@labeling_function装饰器创建的。装饰器可以应用于任何返回单个数据点标签的 Python 函数。

    每个 LF 函数输出三个值(二进制类):

)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-6

使用浮潜概率的模型训练。来源:ai . Stanford . edu/blog/weak-supervision/

  1. 合并低频输出

    基于标注函数的质量以及它们的一致性和不一致性,snuck 的生成模型组合标注函数以输出标注。例如,如果两个标注函数在其输出中具有高度相关性,则生成模型会尝试避免此类函数的重复计算。这也说明了为什么生成模型比最大计数要好。通气管还提供了大量的分析参数,可以显示 LF 的性能。你将在本章中探索它们。

  2. 模特培训

    浮潜的输出使用概率标签,然后可以用来训练任何判别模型。这个判别模型填补了低召回率的空白。见图 6-6 。

在本章中,您将探索标记功能以及如何深入应用它们,但还有一些其他方式可以让通气管提高整个标记过程的性能(图 6-7 )。浮潜团队引入了另外两个概念:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-7

不同的编程接口。资料来源:Snorkel.org

  1. 转换函数

  2. 切片功能(SFs)

就像您倾向于数据扩充来增加您的数据集一样,类似地,在通气管中,您可以使用策略将 TF 写入每个训练数据点(确定如何将变换应用于每个点或一些点,等等)。)来生成扩充的训练集。一些常见的方法可以是用同义词替换单词,或者用其他实体替换命名的实体。与标记函数类似,您使用一个transformation_function装饰器,它包装一个函数,该函数接受一个数据点并返回该数据点的转换版本。

通常,在您的训练数据中,您会发现某些子部分或切片比其他子部分或切片更重要,例如接受重症监护的患者被用于药物性能测试,因此,不仅全局性能,而且此类局部切片的失败次数也更少。

通气管提供了一种在这种切片上测量性能的方法。SFs 输出二进制掩码,指示数据点是否在切片中。切片中的那些被监控。任何模型都可以利用 SFs 来学习切片专家表示,这些表示与注意机制相结合,以做出切片感知预测。

数据探索

介绍

您将使用疾病和治疗实体之间的数据捕获关系。它最初是由 Barbara Rosario 和 Marti A. Hearst 在计算语言学协会第 42 届年会(ACL 2004)上发表的题为“生物科学文本中语义关系的分类”的论文中分享的研究提供的,巴塞罗那,2004 年 7 月( https://biotext.berkeley.edu/dis_treat_data.html ))。

该文本随机取自 Medline 2001,这是一个书目数据库,包含超过 2600 万篇生命科学期刊文章的参考文献,集中于生物医学。

关于数据的一些要点:

  1. 数据集涵盖了治疗和疾病之间的多种关系,例如

    • 治愈:治疗治愈疾病,无论其是否被临床证实。

    • 只有疾病:句子中没有提到治疗。

    • 仅治疗:句子中未提及疾病。

    • 预防:治疗预防或抑制疾病的发生。

    • 副作用:疾病是治疗的结果。

    • 含糊:关系语义不清。

    • 不治愈:治疗无效。

    • 复杂:同一实体参与多个相互关联的关系,或者可能存在多对多的关系。

  2. <label>表示它后面的单词是实体的第一个,</label>表示它前面的单词是实体的最后一个。

  3. 有未标记的数据共享用于测试。

您将从上述链接下载带有角色和关系文件的句子,并按如下所示放置文件:

Data
├── sentences_with_roles_and_relations.txt
├── labeled_titles.txt
├── labeled_abstracts.txt

从文本文件中加载数据。你不会和所有的关系一起工作;你只需要关注治疗、预防和副作用之间的关系。其余的被丢弃。

import re
import pandas as pd
import numpy as np
import os

    f = open('./Data/sentences_with_roles_and_relations.txt', encoding = "ISO-8859-1")
f_data = []
for line in f.readlines():
        line = line[:-1] # Remove linebreak
        f_data.append(line.split('||'))
f.close()

rows = []
for l in f_data:
        if l[1] not in ['NONE', 'TREATONLY', 'DISONLY', 'TO_SEE', 'VAGUE', 'TREAT_NO_FOR_DIS']:
            sent = ' '.join(l[0].split())
            dis_re = re.compile('<DIS.*>(.*)</DIS.*>')
            disease = dis_re.search(sent).group(1)
            treat_re = re.compile('<TREAT.*>(.*)</TREAT.*>')
            treat = treat_re.search(sent).group(1)

            sent = re.sub(r'<.*?> ', '', sent).strip()
        # Handles sentences ending with <*> structure
            sent = re.sub(r'<.*?>', '', sent)

            rows.append([sent, l[1], treat.strip(), disease.strip()])

    biotext_df = pd.DataFrame(data=rows, columns=['sentence', 'relation', 'term1', 'term2'])

上面的代码利用了已经包含关系标签的文件,但是您也可以使用文件夹中存在的其他文件,但是需要做一些预处理,以便根据您的目的利用它。

biotext_df.relation.value_counts()

输出

    TREAT_FOR_DIS    830
    PREVENT           63
    SIDE_EFF          30

你可以看到在关系中有许多不平衡,其中大部分被“为治疗而治疗”或“治愈”关系占据。这可以在标签建模期间通过传递包含每个类的比例的类不平衡数组来处理。

标签功能

你所拥有的是来自生物医学期刊的关于治疗、疾病及其关系的标记数据。实际上,您可以为类似的信息提取任务创建三种主要类型的标注函数。

  1. 句法信息:句法信息帮助您捕捉单词之间的语法依赖,并帮助您发现关系类的常见模式。

  2. n:使用外部本体,如 UML,来捕获除治疗和疾病之外的生物医学实体。

  3. Regex :有一些特定的模式可以精确地指示关系类型。例如,像防止防止减少减少这样的词可以很容易地指示防止关系类。

正则表达式

开始创建标签函数的一个快速方法是扫描属于您想要预测的类别的一串文本。

您可以从查看文本的不同 n 元语法的计数图开始。为此,您将使用 sklearn 模块,具体来说就是sklearn.feature_extraction.text.CountVectorizer

    sklearn.feature_extraction.text.CountVectorizer: "Convert a collection of text documents to a matrix of token counts"

但是在直接运行Countvectorizer之前,为了使练习更加有效,您应该执行一些预处理步骤:

  1. 将单词规范化到它们的词条,这样语义相同的单词不会被不同地计数,例如“provide”和“provide”

  2. 删除所有数字提及。

  3. 删除常见的英语停用词。

  4. 降低文本。

您将使用 nltk 包中的 WordNet Lemmatizer 对单个事物进行 lemmatize。使用 WordNet Lemmatizer 的一个重要方面是,您需要为单词提供一个合适的 pos 标签。如果没有做到这一点,可能会导致突然的或没有引理化。

我们用一个例子来理解这个。

首先导入相关的包和类。

from nltk import pos_tag
from nltk.stem import WordNetLemmatizer

lemmatizer = WordNetLemmatizer()

如果提供一个没有任何 pos 标签上下文的单词,这个单词不会被词条化。

    lemmatizer.lemmatize("sitting")

输出

坐着的

如果您提供了 pos 标签的上下文,那么就可以进行词汇化。

    lemmatizer.lemmatize("sitting", pos = "v")

输出

使就座

WordNet 有五种类型的 pos 标签:

  • 形容词

  • 形容词卫星

  • 副词

  • 名词

  • 动词

你们大多数人都听说过形容词、副词、名词和动词,但形容词卫星可能是一个新名词。形容词附属体是一类专门用于特定语境的形容词。例如,只能有“干旱气候;不能有“皮肤干燥”。然而,用于创建 pos 标签的 PennTreeBank 不区分卫星形容词和普通形容词,因此您会将它们都视为形容词。

有了上面的信息,让我们来设计你的预处理函数。对于词汇化,您将维护一个 pos 标签到其用于 WordNet 词汇化器的标签的标签映射。

    mapping_pos_label = {"JJ":'a',
     "RB":'r',
   "NN": 'n',
     "VB":'v'}

接下来,定义一个函数,如果单词的 pos 标签是形容词(JJ*)、副词(RB*)、名词(NN*)或动词(VB*),则返回 WordNet pos label。

    def get_pos_label(w, postag, mapping_pos_label):
    for k, v in mapping_pos_label.items():
        if postag.startswith(k):
            return v
        return "n"

注意,在上面的函数中,如果正则表达式没有找到匹配项,则返回一个名词标记,因为默认情况下,WordNet Lemmatizer 使用名词作为 pos 标记。

您已经拥有了创建预处理函数所需的一切。

import re
    def preprocess_text(text):
    text = text.lower()
        text = " ".join([lemmatizer.lemmatize(w,
                                  pos= get_pos_label(w,
                                                     pos_w,
                                                     mapping_pos_label))\
                     for w, pos_w in pos_tag(text.split()) \
                         if w not in list(set(stopwords.words('english')))])
        text = re.sub(r'\d+', '', text)
    return text

您可以在使用CountVectorizer之前使用这个预处理函数,或者在CountVectorizer函数中传递它。既然后者看起来更整洁,那就用它吧。

cv = CountVectorizer(preprocessor = preprocess_text,
                   ngram_range = (1,3),
                   min_df = 0.01)

除了preprocessor,你还看到另外两个参数。它们的值是根据经验选择的。请随意试验。结果如图 6-8 所示。

  • 告诉你应该考虑计算的短语长度。

  • 如果是浮动的,你可以假设至少一定比例的样本应该提到词汇。如果是整数,假设至少有那么多行应该提到词汇。

    count_mat = cv.fit_transform(biotext_df[biotext_df.relation.isin(["TREAT_FOR_DIS"])].sentence)
count_df = pd.DataFrame(count_mat.todense(), columns=cv.get_feature_names())

count_df = count_df.sum().reset_index()
    count_df.columns = ["word","val"]
    count_df = count_df.sort_values('val', ascending = False)

import plotly.express as px
    fig = px.pie(count_df.head(20), values='val', names='word', title="Top Words for 'TREAT_FOR_DIS' Text")
fig.show()

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-8

TREAT_FOR_DIS 类别中最常用的单词/短语

类似地,对SIDE_EFFPREVENT类重复该过程。见图 6-9 和 6-10 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-10

SIDE_EFF 类别中最常用的单词/短语

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-9

预防类别中最常用的单词/短语

上面的三张图对语料库中的关键词提供了一些非常有用的见解,可以帮助您形成一些基于正则表达式的 LFs。

    treatment_keywords = ['treatment', 'therapy','effective','treat', "reduce"]

请注意,对于治疗关键词,您会看到许多肿瘤相关的术语,如肺癌、乳腺癌等。但我会避免把它们当作标签,因为它们可能只是因为你的语料库有限。您应该尝试创建更健壮的函数来控制精度。

同样的,

    prevent_keywords = ["protect", "prevent", "inhibit", "block", "control", 'effect']
    side_effect_keywords = ["risk","follow", "associate", "toxic"]

句法的

有些词不会很频繁出现,但仍然有助于将疾病与治疗方法及其各种关系联系起来。

为了找到这样的单词,你将利用文本的句法结构。具体来说,您将处理句子的依存解析树。这是一种计算语言学家技术,用于分析句子的语法结构,建立“中心”词并建立这些词之间的关系。更多信息,请参考 https://nlp.stanford.edu/software/nndep.html

您将使用 networkx 库将依赖关系树解析成一个图,并寻找疾病和治疗路径之间出现的单词模式。

通常可以有多条路径连接两条路径,但是您最感兴趣的是最短的依赖路径。这是优选的,因为它仅包含在任何两个实体之间建立关系的必要信息。

例如,考虑来自 PREVENT 类的以下语句:

改良乳罩在预防哺乳期妇女乳腺炎中的应用。

这里

修饰|胸罩治疗乳腺炎疾病

依赖图看起来有点像图 6-11 。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-11

依赖图

Note

引入“|”而不是“,”是有原因的。稍后将详细介绍。

现在,如果您想要遍历从修改的 bra 到乳腺炎的依赖关系,那么在这两个实体之间有多个单词和依赖关系跳转。然而,SDP 相当简单。

SDP 是

改良|胸罩-预防-乳腺炎

其他一些例子有

  1. 结论:这些数据首次证明了慢性压力可以抑制针对肺炎细菌|疫苗的 IgG 抗体反应的稳定性,为痴呆症护理相关的健康风险提供了额外的证据。

    细菌性|疫苗稳定性肺炎

  2. 解磷定对有机磷化合物诱导的肌肉|纤维|坏死的保护作用。

    解磷定作用肌肉|纤维|坏死

您可以看到 SDP 是如何完美地捕捉相关信息,仅基于句子结构将两个实体联系起来,因此您将使用它来识别一些新的关系或单词。

为此,您将使用之前在第四章中使用的 scispacy 包来分析 BERT 模型的词汇。您还将加载 networkx 库来查找最短的依赖路径。

import spacy
import scispacy
import networkx as nx

from scispacy.linking import EntityLinker
    nlp = spacy.load('en_core_sci_lg')

在深入研究主要代码之前,您应该了解一些事情。

  1. 您使用 scispacy 进行依存解析,而不是 spacy 通用文本解析器,原因很简单,scispacy 的依存解析是在 GENIA 1.0 corpus 和 OntoNotes 5.0 上训练的,这提高了生物医学文本解析器的准确性和健壮性。

  2. Spacy 不在空白上标记,而您的大多数标记(由人类注释者或其他人)都是基于空白的。这可能会导致目标令牌的 pos 标签出现一些错位,因为它可能会根据空间逻辑被令牌化为更小的成分。为了解决这个问题,你将

    1. 编写一个重新合并逻辑来合并被拆分的实体(疾病或治疗)。一般来说,带括号的单词表现出不规则的行为。
  3. 你会注意到在上面的例子中,在疾病和治疗阶段使用了“|”字符来代替空格。这是因为您希望在 SDP 计算的依赖关系树中将这些短语用作单个实体,而不是单独的实体。

有关 scispacy 的更多信息,请参考 Neuman 等人的“ScispaCy:生物医学自然语言处理的快速和健壮模型”。

您从编写重组逻辑开始。为此,您可以使用 spacy 的Doc类的合并功能。它就地合并不是空格的标记。实际上,Doc对象中可用的标记变成了空格分隔。

    def remerge_sent(sent):
        i = 0
        while i < len(sent)-1:
        tok = sent[i]
        if not tok.whitespace_:
                ntok = sent[i+1]
            # in-place operation.
            sent.merge(tok.idx, ntok.idx+len(ntok))
            i += 1
    return sent

接下来初始化一个空列表。

    sdp_list = {'PREVENT': [],
               'SIDE_EFF': [],
               'TREAT_FOR_DIS': []}

在主代码中,您采取以下主要步骤:

  1. 首先运行两个for循环,一个用于疾病和治疗的不同关系类型,另一个用于课堂上的不同句子。

  2. 使用 networkx 库初始化一个空图。

    1. 对于每个令牌,通过维护一个单独的列表,并使用add_edges_from函数将它们添加到 Networkx Graph 对象中,来添加与其所有子令牌的关系。

    2. 您还可以使用add_nodes_from函数添加一个节点及其属性。

  3. 您还维护了一个包含不同信息的 Python 字典(meta_info ),您可以利用它进行分析。

for KEY in sdp_list.keys():
    for i,row in biotext_df[biotext_df.relation.isin([KEY])].iterrows():
        # Entities to find SDP between
            entity1 = row["term1"].replace(" ","|").replace("`","")
            entity2 = row["term2"].replace(" ","|").replace("`","")

        # Adjusting for Space
            new_sentence = row["sentence"].replace(row["term1"], entity1)
            new_sentence = new_sentence.replace(row["term2"], entity2)

        # Spacy Pipeline
        doc = nlp(new_sentence)
        doc = remerge_sent(doc)

            entity1_idx = [token.i for token in doc if token.text in [entity1]][0]
            entity2_idx = [token.i for token in doc if token.text in [entity2]][0]

        # Load Networkx Graph
        G = nx.Graph()

        # Load spacy's dependency tree into a networkx graph
        edges = []
        for token in doc:
            for child in token.children:
                    G.add_nodes_from([(token.i, {"pos": token.pos_,
                                                 "text": token.text}),
                                      (child.i, {"pos": child.pos_,
                                                 "text": child.text})])
                edges.append((token.i,
                              child.i))

        # Addding Edges
        G.add_edges_from(edges)

        meta_info = {}
            meta_info["entity1"] = entity1
            meta_info["entity2"] = entity2
            meta_info["entity1_idx"] = entity1_idx
            meta_info["entity2_idx"] = entity2_idx
            meta_info["graph_object"] = G

        shortest_path_list = nx.all_shortest_paths(G, source = entity1_idx, target = entity2_idx)

            meta_info["word_list"] = [(G.node[n]['text'], G.node[n]['pos']) \
                                  for shortest_path in shortest_path_list \
                                  for i,n in enumerate(shortest_path) \
                                      if i>0 and i<len(shortest_path)-1]

        sdp_list[KEY].append(meta_info)

既然你有了树关系的 SDP 列表,让我们分析一下在句子的依存路径中你得到了哪些单词/短语。

与前面采用的策略类似,您将使用 WordNet 词汇排序器对您的单词进行词汇排序。

    mapping_pos_label_spacy = {"ADJ":'a',
     "ADV":'r',
     "NOUN": 'n',
     "VERB":'v'}

    lemmatized_list = [[lemmatizer.lemmatize(word[0].lower(),
                                             get_pos_label(word[0],
                                                           word[1],
                                                       mapping_pos_label_spacy)) \
                        for word in val['word_list']] \
                       for val in sdp_list["TREAT_FOR_DIS"] \
                       if len(val['word_list']) > 0]

接下来,创建一个名为get_top_words,的函数,其中

  • 从词汇化的单词中提取单个单词列表。

  • 创造 1-3 克代币。

  • 找到频率并排序。

    def get_top_words(lemmatized_list, n):
        """
        Show Top 'n' words
        """
        count_df = pd.Series([" ".join(word_phrase) \
                          for word_list in lemmatized_list \
                              for i in range(1,4) \
                          for word_phrase in nltk.ngrams(word_list, i)]).value_counts().reset_index()
        count_df.columns = ["word","counts"]

        count_df = count_df[count_df.counts > 1]
    for i,row in count_df.head(n).iterrows():
            print(row["word"] ,"---->", row["counts"])

这样,您可以获得这三个类的以下值。

  1. 为疾病治疗

  2. 预防

    patient ----> 189
    treatment ----> 134
    treat ----> 59
    use ----> 43
    effective ----> 36
    effect ----> 31
    therapy ----> 23
    treat patient ----> 20
    trial ----> 19
    management ----> 16
    undergo ----> 16
    study ----> 15
   perform ----> 13
    show ----> 13
    rate ----> 13
    effectiveness ----> 13
    improve ----> 11
    efficacy ----> 11
    result ----> 11
    receive ----> 11

  1. 侧面 _ 效果
    prevent ----> 9
    prevention ----> 6
    effective ----> 4
    use ----> 4
    reduce ----> 4
    vaccine ----> 3
    patient ----> 3
    effect ----> 3
    study ----> 2
    incidence ----> 2
    effective prevent ----> 2
    risk ----> 2
    stability ----> 2
    trial ----> 2
    safe ----> 2

    associate ----> 5
    rate ----> 4
    risk ----> 4
    case ----> 3
    eye ----> 3
    administration ----> 2
    complication ----> 2
    neurotoxicity ----> 2
    patient ----> 2
    associate risk ----> 2
    develop ----> 2
    had eye ----> 2
    had ----> 2

正如您所观察到的,上面突出显示的单词现在已经“微弱地”添加了新的信息来帮助对关系进行分类。此外,它们中的一些在野生搜索中没有意义,但是在 SDP 上下文中,出现假阳性的机会减少了,例如 TREAT_FOR_DIS 句子中的“patient”。

远程监督

有许多单词或短语带有语义,因此它们可以用基于统计频率的分析来代替。为了识别这样的短语,您将利用 UMLs 本体,它捕获超过 110 个医学概念,如治疗或预防程序、药理物质、保健活动、病理功能等。

您在第四章中学习了 UML,所以这里您将查看代码并分析输出。

首先,确保将 UML 流水线添加到空间中。为此,您只需调用EntityLinker类来添加umls数据库。

        linker = EntityLinker(resolve_abbreviations=False, name="umls")
# keeping default thresholds for match percentage.
nlp.add_pipe(linker)

# UMLs provides a class name to each of its TXXX identifier, TXXX is code for parents for each of the CUI numbers a unique concept
# identifier used by UMLs Kb

# To obtain this file please login to https://www.nlm.nih.gov/research/umls/index.html
# Shared in Github Repo of the book :)
    type2namemap = pd.read_csv("SRDEF", sep ="|", header = None)
    type2namemap = type2namemap.iloc[:,:3]
    type2namemap.columns = ["ClassType","TypeID","TypeName"]
    typenamemap = {row["TypeID"]:row["TypeName"] for i,row in type2namemap.iterrows()}

然后,为每个关系类创建一个概念数据帧,其中包含特定概念出现的频率。与之前只关注频率的设置不同,这里你还将寻找独特性。

    KEY = "TREAT_FOR_DIS"

umls_concept_extracted = [[umls_ent for entity in doc.ents for umls_ent in entity._.umls_ents] for doc in nlp.pipe(biotext_df[biotext_df.relation.isin([KEY])].sentence.tolist())]
    umls_concept_cui = [linker.kb.cui_to_entity[concept[0]] for concepts in umls_concept_extracted for concept in concepts]
# Capturing all the information shared from the UMLS DB in a dataframe
umls_concept_df = pd.DataFrame(umls_concept_cui)
concept_df = pd.Series([typenamemap[typeid] for types in umls_concept_df.types for typeid in types]).value_counts().reset_index()
    concept_df.columns = ["concept","count"]

    umls_concept_df["Name"] = pd.Series([[typenamemap[typeid] for typeid in types] for types in umls_concept_df.types])

基于每个键的concept_df数据框架,表 6-1 显示了可以用来区分关系类型的主要 UML 类型。

表 6-1

每个关系的 UML 类型

|

关系

|

UML 类型

|

理由

|

概念示例

|
| — | — | — | — |
| 为疾病治疗 | 治疗或预防程序 | 疗法和治疗 | 外科手术、化疗/放疗/阿司匹林疗法、治疗方案等。 |
| 为疾病治疗 | 精神产品 | 方法、目标和过程 | 方法、目标和过程 |
| 为疾病治疗 | 定性概念 | 评估质量 | 有效性,典型,简单,完整 |
| 为疾病治疗 | 患者或残疾人群体 | 捕获单词患者及其别名 | 病人,病人,等等。 |
| 为疾病治疗 | 时间概念 | 与提及的时间和持续时间相关 | 年份、术后时期、每周、暂时等。 |
| 为疾病治疗 | 保健活动 | 评估和报告 | 评估和报告 |
| 预防 | 免疫因素 | 识别其活动影响免疫系统功能或在其中发挥作用的活性物质 | 疫苗和联合疗法 |
| 预防 | 想法或概念 | 结论或结果 | 结论 |
| 预防 | 职业活动 | 职业分析和活动 | 经济分析 |
| 侧面 _ 效果 | 迹象或症状 | 显示药物的效果 | 发育期痛 |
| 侧面 _ 效果 | 受伤或中毒 | 显示药物的效果 | 伤口/损伤 |
| 侧面 _ 效果 | 身体部分、器官或器官组成部分 | 显示药物的效果 | 任何身体部位 |
| 侧面 _ 效果 | 病理功能 | 不良反应和影响 | 脑出血,药物不良反应,自然流产 |

流水线

为了展示 scub 的功能,您需要创建一个实验,将您的数据分成两个数据集:

  • 一个名为train_df的无标签训练数据集,潜航器的LabelModel将使用它来学习标签

  • 一个名为val_df的手工标注的开发数据集,您将使用它来确定您的 LFs 是否工作

您将通过分层方式进行采样来维护目标类的分布。

from sklearn.model_selection import train_test_split

train_df, val_df, train_labels, val_labels = train_test_split(
    biotext_df,
        biotext_df['relation'],
        test_size=0.4,
        stratify = biotext_df['relation'],
        random_state = 42
)

如前所述,通气管有三个主要接口

  • 标签功能

  • 转换函数

  • 切片功能

我将在本章中深入讨论标签功能。标记函数确定性地确定数据的类别。这些功能可以在任何级别(文本/段落/元数据)工作,并且可以利用多种信息源(模型/外部数据库/本体)

为了编写标签函数,您需要为您的问题定义标签模式。

)

除了数据中出现的类之外,还必须定义一个 present 标签,因为只有在有足够证据的情况下,这才允许 scub 为某个类投票。如果你从通气管得到了很多的弃权值,那么你将不得不增加 LFs 的覆盖率。

# Define our numeric labels as integers
    ABSTAIN = -1
    TREAT_FOR_DIS = 0
    PREVENT = 1
    SIDE_EFF = 2

    def map_labels(x):
        """Map string labels to integers"""
        if x == 'TREAT_FOR_DIS':
        return TREAT_FOR_DIS
        elif x == 'PREVENT':
        return PREVENT
        elif x == 'SIDE_EFF':
        return SIDE_EFF

val_labels  =  val_labels.apply(map_labels, convert_dtype=True)

写你的 LFs

标注功能的程序界面为snorkel.labeling.LabelingFunction。它们用名称、函数引用、函数需要的任何资源以及在标记函数运行之前要在数据记录上运行的任何预处理程序的列表来实例化。

定义 LF 函数有两种方法:

  1. 使用基类LabelingFunction

  2. 使用装饰器labeling_function

    snorkel.labeling.LabelingFunction(name, f, resources=None, pre=None)

        - "name" = Name of the LF.
        - "f" = Function that implements the LF logic.
        - "resources" = Labeling resources passed into f
        - "pre" = Preprocessors to run on the data

    snorkel.labeling.labeling_function(name=None, resources=None, pre=None)

        - "name" = Name of the LF.
        - "resources" = Labeling resources passed into f
        - "pre" = Preprocessors to run on the data

您将使用 decorator 方法,因为它简单得多。

对于不了解 decorator 的人来说,decorator 基本上是拿一个函数,添加一些功能(也就是装饰它),通过调用它来返回它。

与装饰者一起工作

根据你的分析,你已经为每个关系类列出了下列单词。因此,您只需编写一个标注函数,如果找到了它们各自的单词,该函数将返回关系类,否则将放弃标注。

    treatment_keywords = ['treatment', 'therapy','effective','treat', "reduce"]
    prevent_keywords = ["protect", "prevent", "inhibit", "block", "control", 'effect']
    side_effect_keywords = ["risk","follow", "associate", "toxic"]

@labeling_function()
    def sent_contains_TREAT_FOR_DIS(x):
    text = x.sentence.lower()
    lemmatized_word = [lemmatizer.lemmatize(w,
                                  pos= get_pos_label(w,
                                                     pos_w,
                                                     mapping_pos_label))\
                     for w, pos_w in pos_tag(text.split()) \
                         if w not in list(set(stopwords.words('english')))]
    return TREAT_FOR_DIS if any([ True if key in lemmatized_word else False for key in treatment_keywords]) else ABSTAIN

@labeling_function()
    def sent_contains_SIDE_EFF(x):
    text = x.sentence.lower()
    lemmatized_word = [lemmatizer.lemmatize(w,
                                  pos= get_pos_label(w,
                                                     pos_w,
                                                     mapping_pos_label))\
                     for w, pos_w in pos_tag(text.split()) \
                         if w not in list(set(stopwords.words('english')))]
    return SIDE_EFF if any([ True if key in lemmatized_word else False for key in side_effect_keywords]) else ABSTAIN

@labeling_function()
    def sent_contains_PREVENT(x):
    text = x.sentence.lower()
    lemmatized_word = [lemmatizer.lemmatize(w,
                                  pos= get_pos_label(w,
                                                     pos_w,
                                                     mapping_pos_label))\
                     for w, pos_w in pos_tag(text.split()) \
                         if w not in list(set(stopwords.words('english')))]
    return PREVENT if any([ True if key in lemmatized_word else False for key in prevent_keywords]) else ABSTAIN

是的,就是这么简单。

通气管中的预处理器

但是上面的代码有一个问题。对于每个函数,每次都必须重复词条化和文本 lower 逻辑。你不能预先对你的数据进行预处理,然后在每个函数中不重复逻辑地使用它吗?

好吧,通气管有一个预处理器,映射一个数据点到一个新的数据点。

可以使用预处理器,让你在转换或增强的数据点上写 LFs。

您将@preprocessor(...)装饰器添加到预处理函数中来创建预处理器。预处理器也有额外的功能,比如内存化(即输入/输出缓存,所以它不会为每个使用它的 LF 重新执行)。

from snorkel.preprocess import preprocessor

@preprocessor(memoize = True)
    def get_syntactic_info(x):

    # Entities to find SDP between
        entity1 = x.term1.replace(" ","|").replace("`","")
        entity2 = x.term2.replace(" ","|").replace("`","")

    # Adjusting for Space
    new_sentence = x.sentence.replace(x.term1, entity1)
    new_sentence = new_sentence.replace(x.term2, entity2)

    # Spacy Pipeline
    doc = nlp(new_sentence)
    doc = remerge_sent(doc)

        entity1_idx = [token.i for token in doc if token.text in [entity1]][0]
        entity2_idx = [token.i for token in doc if token.text in [entity2]][0]

    # Load Networkx Graph
    G = nx.Graph()

    # Load spacy's dependency tree into a networkx graph
    edges = []
    for token in doc:
        for child in token.children:
                G.add_nodes_from([(token.i, {"pos": token.pos_,
                                             "text": token.text}),
                                  (child.i, {"pos": child.pos_,
                                             "text": child.text})])
            edges.append((token.i,
                          child.i))

    # Addding Edges

    G.add_edges_from(edges)

    shortest_path_list = nx.all_shortest_paths(G, source = entity1_idx, target = entity2_idx)

        word_list = [(G.node[n]['text'], G.node[n]['pos']) \
                              for shortest_path in shortest_path_list \
                              for i,n in enumerate(shortest_path) \
                                  if i>0 and i<len(shortest_path)-1]

        lemmatized_list = [lemmatizer.lemmatize(word[0].lower(),
                                             get_pos_label(word[0],
                                                           word[1],
                                                       mapping_pos_label_spacy)) \
                        for word in word_list]

    x.sdp_word = lemmatized_list
    return x

类似地,您可以从每个关系类的 SDP 路径中了解重要的单词。因此,您从初始化它们开始。

    treatment_sdp_keywords = ['patient', 'use','trial','management', "study", "show", "improve"]
    prevent_sdp_keywords = ["reduce", "vaccine", "incidence", "stability"]
    side_effect_sdp_keywords = ["rate","case", "administration", "complication", "develop"]

@labeling_function(pre=[get_syntactic_info])
    def sent_sdp_TREAT_FOR_DIS(x):
    return TREAT_FOR_DIS if any([True if key in x.sdp_word else False for key in treatment_sdp_keywords]) else ABSTAIN

@labeling_function(pre=[get_syntactic_info])
    def sent_sdp_SIDE_EFF(x):
    return SIDE_EFF if any([True if key in x.sdp_word else False for key in side_effect_sdp_keywords]) else ABSTAIN

@labeling_function(pre=[get_syntactic_info])
    def sent_sdp_PREVENT(x):
    return PREVENT if any([True if key in x.sdp_word else False for key in prevent_sdp_keywords]) else ABSTAIN

看看现在代码变得多么简单和干净。

最后,你也有基于距离的弱学习者。类似于上面完成的预处理,您使用预处理装饰器来做另一个预处理。

@preprocessor(memoize = True)
    def get_umls_concepts(x):

    umls_concept_extracted = [[umls_ent for entity in doc.ents for umls_ent in entity._.umls_ents] for doc in nlp.pipe([x.sentence])]

    try:
            umls_concept_cui = [linker.kb.cui_to_entity[concept[0]] for concepts in umls_concept_extracted for concept in concepts]
        # Capturing all the information shared from the UMLS DB in a dataframe
        umls_concept_df = pd.DataFrame(umls_concept_cui)
        concept_df = pd.Series([typenamemap[typeid] for types in umls_concept_df.types for typeid in types]).value_counts().reset_index()
            concept_df.columns = ["concept","count"]

            x["umls_concepts"] = concept_df.concept.tolist()
    except Exception as e:
            x["umls_concepts"] = []

    return x

基于表 6-1 ,你也从句子中知道了主导的和重要的 UML 概念。

    treatment_umls_concepts = ['Therapeutic or Preventive Procedure',
                               'Intellectual Product',
                               'Qualitative Concept',
                               'Patient or Disabled Group',
                               "Temporal Concept",
                               "Health Care Activity"]

    prevent_umls_concepts = ["Immunologic Factor",
                             "Idea or Concept",
                             "Finding",
                             "Occupational Activity"]

    side_effect_umls_concepts = ["Sign or Symptom",
                                 "Injury or Poisoning",
                                 "Body Part, Organ, or Organ Component",
                                 "Pathologic Function"]

最后,为这个远程监控设置编写标签函数。

@labeling_function(pre=[get_umls_concepts])
    def sent_umls_TREAT_FOR_DIS(x):
    return TREAT_FOR_DIS if any([True if key in x.umls_concepts else False for key in treatment_umls_concepts]) else ABSTAIN

@labeling_function(pre=[get_umls_concepts])
    def sent_umls_SIDE_EFF(x):
    return SIDE_EFF if any([True if key in x.umls_concepts else False for key in prevent_umls_concepts]) else ABSTAIN

@labeling_function(pre=[get_umls_concepts])
    def sent_umls_PREVENT(x):
    return PREVENT if any([True if key in x.umls_concepts else False for key in side_effect_umls_concepts]) else ABSTAIN

培养

对于训练,你必须将你的弱标签应用到每个句子中。由于您的数据存储在 pandas 数据帧中,您将利用一个名为PandasLFApplier的内置函数。

是一个给出标签矩阵的LFApplier类。这是一个 NumPy 数组 L,每个 LF 一列,每个数据点一行,其中 L[i,j]是第 j 个标注函数为第 I 个数据点输出的标注。您将为训练集创建一个标签矩阵。

lfs = [sent_contains_TREAT_FOR_DIS, sent_contains_SIDE_EFF, sent_contains_PREVENT,
      sent_sdp_TREAT_FOR_DIS, sent_sdp_SIDE_EFF, sent_sdp_PREVENT,
      sent_umls_TREAT_FOR_DIS, sent_umls_SIDE_EFF, sent_umls_PREVENT]

# Instantiate our LF applier with our list of LabelFunctions (just one for now)
applier = PandasLFApplier(lfs=lfs)

# Apply the LFs to the data to generate a list of labels
L_train = applier.apply(df=train_df)
L_dev   = applier.apply(df=val_df)

估价

在一个简单的名为LFAnalysis的函数中,通气管很好地为我们打包了大量的分析。报告了许多汇总统计数据(见图 6-12 ):

  • 极性:该 LF 输出的唯一标签集合(不包括弃权)

  • 覆盖率:LF 标记的数据集的分数

  • 重叠:具有至少两个(非弃权)标签的数据点的分数。

  • 冲突:该 LF 和至少一个其他 LF 标签不一致的数据集部分(非弃权标签)

  • 正确:该 LF 正确标注的数据点数(如果有金色标注)

  • 不正确:该 LF 标注错误的数据点数(如果有金色标注)

  • 经验精度:该 LF 的经验精度(如果有金标)

# Run a label function analysis on the results, to describe their output against the labeled development data
LFAnalysis(L=L_dev, lfs=lfs).lf_summary(val_labels.values)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-12

具有各种指标的 LFAnalysis 输出

一些观察结果:

  • 您会看到 TREAT_FOR_DIS 在覆盖率和准确性指标上表现得非常好。

  • 与其他标签函数相比,PREVENT 的 SDP 标签具有更好的经验准确性。

  • SIDE_EFF 在 UMLs LF 上的表现似乎不是那么好。您可以在整个句子中或者仅仅在 SDP 中检查 UMLs 标签的组合。你将不得不迭代地使这些 LFs 变得更好。

生成最终标签

到目前为止,你已经走过了很多地方。你有

  • 加载并准备数据

  • 将其分为训练集和测试集

  • 浏览数据寻找灵感

  • 创造了 LF

  • 查看了预处理步骤以及如何记忆它们

  • 根据验证数据评估这些 LFs 的性能

您终于可以生成标签了。通气管提供了两种生成最终标签的主要方法。一个是MajorityLabelVoter,它基本上给样本分配了大多数 LFs 给出的标签。

这通常产生低于或在某些情况下等于潜航的噪声意识更强的生成模型的性能,因此作为一个基准。理解这种次性能的一种非常直观的方式是,在MajorityLabel中,所有的 LFs 都被平等对待。然而,正如你看到的 SIDE_EFF,“regex”比基于“umls”的 LFs 更有意义。

from snorkel.labeling.model import MajorityLabelVoter

    majority_model = MajorityLabelVoter(cardinality = 3)
preds_train = majority_model.predict(L=L_train)

正如您所看到的,您需要为MajorityLabelVoter提供一个基数值,它基本上就是非 absent 类的数量。

这有助于建立基线。现在,您可以轻松地转而使用一种更具噪声意识和加权的投票策略。该策略的细节不在本章讨论范围之内,但是对于感兴趣的人,请阅读 Ratner 等人题为“数据编程:快速创建大型训练集”的论文。

from snorkel.labeling.model import LabelModel

label_model = LabelModel(cardinality=3, verbose=True)

在拟合模型之前,您应该了解可供您使用的不同选项。

LabelModel.fit()允许您使用以下超参数:

  • n_epochs:要训练的时期数(其中每个时期是单个优化步骤)

  • lr:基础学习率(也会受到lr_scheduler选择和设置的影响)

  • l2:以 L2 为中心的正规化力量

  • optimizer:使用哪个优化器[“sgd “、” adam “、” adamax”])

  • optimizer_config:优化器的设置

  • lr_scheduler:使用哪个lr_scheduler([“常数”、“线性”、“指数”、“步长”])中的一个

  • lr_scheduler_config:设置LRScheduler

  • prec_init : LF 精度初始化/先验

  • seed:用于初始化随机数生成器的随机种子

  • log_freq:每隔一定时间报告一次损失(步骤)

  • mu_eps:将学习到的条件概率限制为【mu_eps,1-mu_eps】

现在,您将使用默认值来训练模型,但是我强烈建议您进行实验,并了解更多关于这些超参数对优化的影响。

    label_model.fit(L_train=L_train, n_epochs=100, seed=42)

让我们看看生成模型与多数投票基线相比如何。

    majority_acc = majority_model.score(L=L_dev, Y=val_labels, tie_break_policy="random")[
        "accuracy"
    ]
    print(f"{'Majority Vote Accuracy:':<25} {majority_acc * 100:.1f}%")

    label_model_acc = label_model.score(L=L_dev, Y=val_labels, tie_break_policy="random")["accuracy"
    ]
    print(f"{'Label Model Accuracy:':<25} {label_model_acc * 100:.1f}%")

多数票准确率:80.8%

标签模型准确率:87.6%

如你所见,标签模型比多数投票高出 7.5%。这是一次重大的提升。虽然没有什么结论可以说,但您应该始终通过改变超参数来试验性能的敏感性。

在对验证集的性能进行评分时,您会注意到策略的使用,

断绝关系的政策包括

  • abstain:返回弃权票(-1)。

  • true-random:在并列选项中随机选择。

  • random:使用确定性散列在并列选项中随机选择(不同运行中的值保持一致)。

结论

没有从数据中进行弱学习的完美方法。你只需要比随机更好。您的 LFs 可以不同地预测一个数据点的输出。你只需要通过分析数据,编写 LF,然后提炼和调试,不断产生想法。随着数据以更快的准确性和速度增长,组织必须采用这种创新方法来开始标记数据和训练强大的模型。我希望这一章能激发你的好奇心,让你了解更多关于这些方法的知识。如果是的话,那么这就是我们的胜利。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值