提克破事水


  • Home

  • About

  • Tags

  • Categories

  • Archives

  • Search

论文浅读-任务型对话系统中的自然语言生成研究进展综述

Posted on 2023-08-21 | In 论文浅读 |

WangLab at MEDIQA-Chat 2023: Clinical Note Generation from Doctor-Patient Conversations using Large Language Models

Abstract

本文描述了我们对Mediqa-Chat 2023共享任务的提交,该任务用于从医患对话中自动生成临床记录。 我们报告了两种方法的结果:第一种方法在共享任务数据上微调预先训练的语言模型(PLM);第二种方法在大语言模型(LLM)中使用少量上下文学习(ICL)。 两者都实现了通过自动度量(例如Rouge,BertScore)来衡量的高性能,并分别在共享任务的所有提交中排名第二和第一。 专家的人类审查表明,通过基于ICL的方法和GPT-4生成的笔记与人类书写的笔记一样经常被首选,这使得它成为从医患对话中自动生成笔记的一条有希望的途径。

1 Introduction

日益增长的临床文档负担已经成为医疗保健中的一个关键问题,增加了临床医生的工作不满和倦怠率,并对患者体验产生了负面影响。
通过医患对话生成电话记录,MEDIQA-Chat dialgue2note共享任务被提出。我们探索了两种方法; 第一种,微调一个预先训练的语言模型(PLM,§3.1);第二种,使用少量的上下文学习(ICL,§3.2)。 这两个都实现了很高的性能,由自动自然语言生成度量(§4)来衡量,并分别在共享任务的所有提交中排名第二和第一。 在一项由三位专家医生进行的人类评估中,通过基于ICL的方法和GPT-4生成的笔记被首选,其频率与人类书写的笔记一样(§4.3)。

2 Shared Task and Dataset

MEDIQA-Chat 2023中包含对话总结和根据医生和病人之间的对话生成病历

2.1 Task definition

病历中包含一个或多个部分,比如如“主诉”和“家族史”,评估过程中,各部分被分为四个类别之一:Subjective、 Objective Exam、 Objective Results和Assessment and Plan,下图为医患对话和病历的的示例。

2.2 Dataset

该数据集包括67个训练和20个验证示例,以医生和病人相遇的转录对话和由此产生的临床医生写的笔记为特色。 每个示例都标有“数据集源”,表明用于生成笔记的对话转录系统

3 Approach

我们对共享任务采用两种高性能方法。首先,我们在所提供的训练集(§3.1)上微调一个预先训练的语言模型(PLM)。 在第二个方面,我们将上下文学习(ICL)与一个大型语言模型(LLM,§3.2)结合起来。

3.1 Fine-tuning pre-trained language models

作为第一种方法,我们在训练集上微调PLM遵循规范的、序列到序列的训练过程(图1a;有关详细信息,请参见附录C)。 给定输入对话的长度(图3),我们选择使用LongFormerEncoder-Decoder(LED,Beltagy et al.2020),其最大输入大小为16,384个令牌。 我们从PubMed总结数据集(Cohan et al.,2018)上调优的LEDLARGE检查点开始微调,该检查点在初步实验中表现最好。3该模型是在一个NVIDIA A100-40GB的GPU上使用HuggingFace Transformers(Wolf et al.,2020)微调的。 对验证集中的超参数进行了轻微调优。

3.2 In-context learning with LLMs

作为第二种方法,我们使用ICL尝试子任务B。我们选择GPT-4(OpenAI,2023)作为LLM,并设计了一个简单的提示符,其中包括自然语言指令和上下文示例(图4)。 我们将提示符大小限制在6192个令牌–允许2000个输出令牌,因为模型的最大令牌大小为8192–并使用了尽可能多的上下文示例,最多为3个。 我们将温度参数设置为0.2,并将OpenAI API的所有其他超参数设置为默认值。
自然语言指令
Prompt:Write a clinical note reflecting this doctor-patient dialogue. Use the example notes below to decide the structure of the clinical note. Do not make up information.
In-context example selection
每个上下文中的示例都是来自训练集的注释。 为了选择注释,我们首先嵌入每个训练示例的对话和输入对话。 然后根据与输入对话的余弦相似度对训练对话进行排序; 结果的top-k训练示例的注释被选择为上下文中的示例(参见图1,b)。 对话是使用支持自然语言教学的文本编码器讲师(Su et al.,2022a)嵌入的。6最后,我们限制上下文中的示例与输入对话属于相同的“数据集源”(参见§2.2),假设这可能会提高性能。

3.2 Evaluation

模型使用验证集中的官方评估脚本8进行评估(因为没有提供测试注释)。通过ROUGE (Lin, 2004)、BERTScore (Zhang et al., 2020)和BLEURT (Sellam et al., 2020)对生成的笔记与提供的地面真实笔记进行评估。我们报告的性能作为算术平均值的ROUGE-1 F1, BERTScore F1和BLEURT-20

正则表达式随手记

Posted on 2023-07-21 | In 常用工具 |

基础知识

案例1

从类似于“中医师分会第一届委员会名单”的文本中,获取到届数“一”。类似的任务当中其实一直有个疑问,就是获取“第一届”和“一”的正则表达式的区别是什么?

1
text = "中医师分会第一届委员会名单"

正则表达式对于我来说就只有一种”(.*?)”一种使用方法,所以我提笔即写:

1
2
match = re.search("第(.*?)届",text)
print(match.group()) # 第一届

印象中,曾经遇到过想要类似于想要“第一届”但是只能获取到“一”,后来好像是这么做的:

1
2
match = re.search("(第.*?届)",text)
print(match.group()) # 第一届

可是这次不知道为什么两种pattern输出的结果是相同的。那如果只想获取到“一”呢?问了下Perplex.ai是这么说的:

1
2
3
4
match = re.search(r'(?<=第)[一二三四五六七八九十]+(?=届)', text)
if match:
_res = match.group()
print(_res)

案例2

反爬虫原理与绕过实战笔记

Posted on 2023-07-19 | In 常用工具 |

静态网页与动态网页

  • 传统的静态网页指没有数据库和不可交互的纯HTML页面,不修改代码则显示内容不会改变。
  • 传统的动态网页指在不改变页面HTML代码的情况下,能够根据不同用户或者不同操作而显示不同内容的网页。
    在爬虫领域中,静态网页与动态网页的定义与传统定义是完全不同的。
  • 静态网页指网页主题内容的渲染工作在服务器端完成,并通过响应正文返回的网页。
  • 动态网页指的是主体内容或者全部内容都需要客户端执行JavaScript代码来计算或渲染的网页。

爬虫并非所见即所得。在得到的资源中,最重要的就是响应正文。但是由于Python、Java和PHP等变成语言没有JavaScript解释器和渲染引擎,所以使用变成语言编写的爬虫程序无法渲染页面,它们智能爬取响应正文中的内容。有一些工具已经集成了渲染页面索需要的组件,并且开放API允许变成语言操作页面以获取渲染后的页面代码。

  • Splash:异步的JavaScript渲染服务
  • Selenium: 自动化测试框架
  • Puppeteer:一个通过DevTools协议控制Chrome的Node.js库

反爬虫的概念与定义

书内约定,限制爬虫程序访问服务器资源和获取数据的行为成为反爬虫。限制手段包括但不限于请求限制、拒绝响应、客户端身份验证、文本混淆和使用动态渲染技术。可以分成主动反爬和被动反爬。

  • 主动型反爬虫:开发者有意识地使用技术手段区分正常用户和爬虫,并限制爬虫对网站的访问行为,如验证请求头信息、限制访问频率、使用验证码等。
  • 被动型反爬虫:为了提升用户体验或节省资源,用一些技术间接提高爬虫访问难度的行为,比如数据分段加载、点击切换标签页、鼠标悬停预览数据等。
    还可以从特点上对反爬虫进行更细致的划分,如信息校验型反爬虫、动态渲染型反爬虫、文本混淆型反爬虫、特征识别型反爬虫等。需要注意的是,同一种限制现象可以被柜内到不同的反爬虫类型中,比如通过JS生成随机字符串并将字符串放在请求头中发送给服务器,服务器校验客户端身份的这种限制手段既可以说是信息校验型反爬虫,也可以说是动态渲染反爬虫。

反爬手段

信息校验型反爬虫

信息校验中的“信息”指的是客户端发起网络请求时的请求头和请求正文,而“校验”指的是服务器端通过对信息的正确性、完整性或唯一性进行验证或判断,从而区分正常用户和爬虫程序的行为。
在Web应用中,用户每次请求都会先经过服务器,然后转发到对应的后端程序。后端面临众多请求时要如何识别哪些是用户请求,哪些是爬虫请求?校验请求头和请求正文就可以区分正常用户和爬虫程序。

User-Agent反爬虫

这是一种较为初级的反爬虫手段。
浏览器是一种用于检索并展示万维网信息资源的应用程序,使用浏览器在各个网页之间跳转其实就是访问不同的信息资源,服务器会根据客户端传递的请求信息以及身份信息返回客户端所希望接收的内容。User-Agent就是请求头域之一,服务器能够从User-Agent对应的值中识别客户端使用的操作系统、CPU类型、浏览器、浏览器引擎、操作系统语言等。之所以选择User-Agent头域作为校验对象,是因为很多变成语言和软件有默认的标识。比如使用Python中的Requests库向服务器发送HTTP请求时,服务器读取的User-Agent值为:

python-requests/2.21.0
使用Java和PHP等语言编写的库也有类似的默认标识。在nginx中可以设定黑名单,用以屏蔽爬虫程序的请求。nginx的配置生效后,只要请求头中的User-Agent头域值包含黑名单中的关键词,那么这次请求就无法通过校验。

1
2
3
4
5
6
7
# selenium 4
from selenium.webdriver.chrome.options import Options
user_agent = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36'
options = Options()
options.add_argument(f"--User-Agent={user_agent}")
driver = webdriver.Chrome(options=options)
driver.get(url)

Cookie反爬虫

Cookie不近可以用于Web服务器的用户信息存储或状态保持,还能够用于反爬虫。大部分的爬虫程序在默认情况下只请求HTML文本资源,这意味着它们不会主动完成浏览器保存Cookie的操作,这次的反爬虫正式利用了这个特点。浏览器会自动检查响应头中是否存在Set-Cookie头域。如果存在,则将值保存在本地,而且往后的每次请求都会自动携带对应的Cookie值,这时候只要服务器端对请用头中的Cookie值进行校验即可。服务器会校验每个请求头中的Cookie值是否符合规则,对于不符合的请求将重新定向到首页。

  • 若携带正确Cookie,那么浏览器、Postman和爬虫程序发出的请求都可以正常响应。
  • 若未携带正确Cookie,那么浏览器、Postman和爬虫程序发出的请求都被重定向
  • 只有首页页面的响应头中有Set-Cookie头域,内容页的响应头中没有
    User-Agent和Cookie都是请求头的默认头域,在值的设定方面有一定的局限性,但是与JavaScript结合后,就会变得很灵活。相对服务器软件来说,后端程序的校验更为灵活且准确,但使用后端程序进行校验所需的步骤较多,在实际应用时可以根据需求选择合适的校验方式。

签名验证反爬虫

签名是根据数据源进行计算或加密的过程,签名的结果是一个具有唯一性和一致性的字符串。签名结果的特性使得它成为验证数据来源和数据完整性的条件,可以有效避免服务器端将伪造数据或被篡改的数据当成正常数据处理。
签名验证是防止恶意连接和数据被篡改的有效方式之一,也是目前后端API最常用的防护方式之一。与Cookie、User-Agent、Host和Referer等请求头域不同,用于签名验证的信息通常被放在请求正文中发送到服务器端。
(那跟我应该就没啥关系了,另外也看不懂了。。涉及到好多前端、后端、JavaScript的知识。。。)

动态渲染反爬虫

Selenium可破/doge

异步渲染服务Splash

如果只需要在一台计算机上运行爬虫程序,那么使用Selenium套件或者Puppeteer就可以满足渲染需求了。但如果是分布式爬虫呢?假如我们需要在30台服务器上启动爬虫程序,那么要在每一台服务器上安装一个Selenium套件嘛?
Splash是一个异步的JavaScript渲染服务,它是带有HTTP API的轻量级Web浏览器。SPlash能够并行地处理多个页面请求,在页面上下文中执行自定义的JavaScript以及浏览器中的点击、下滑等操作。有了Splash之后情况就变得不一样了,我么可以将Splash服务部署到云服务器上并配置负载均衡。这样做的好处是渲染服务压力大的时候,可以动态的增加Splash渲染服务。多个爬虫程序公用Splash服务还可以节省硬件资源。

(坑)

渲染工具知识扩展

但是Selenium有一个非常明显的问题就是:慢。开启webdriver,访问页面等待元素加载,模拟点击等等耗时过长。有没有更快的方法呢?
比如,页面上的内容是由JavaScript代码计算得出时,可以获得源数据后模仿JS逻辑自己在本地运算。

相比于直接使用浏览器的Selenium和Puppeteer来说,Splash在页面渲染、对新特性的支持等方面是比较差的。这个差距表现在对DOM节点的渲染和HTML Element时间操作上,例如点击事件触发后内容无法渲染到指定的位置。但是在浏览器中经常使用到的操作比如点击、滑动、拖拽、文本输入、复制、页面前进、后退、截图、文件下载等,Splash也同样能做到。

文本混淆反爬虫

文本混淆可以有效地避免爬虫获取Web应用中重要的文字数据,使用文本混淆限制爬虫虎丘文字数据的方式称为文本混淆反爬虫。反爬虫的前提是不能影响永固正常浏览网页和阅读文字内容,直接混淆文本很容易被看出来,所以开发者通常是利用CSS的特性来实现混淆。常见的文本混淆手段有图片伪装、文字映射和自定义字体等。

图片伪装反爬虫

图片伪装指的是将带有文字的图片与正常文字混合在一起,以达到“鱼目混珠”的效果,这种混淆方式不会影响用户阅读,但是可以让爬虫程序无法获得“所见”的文字内容。
(如何解决?上OCR!)

CSS偏移反爬虫

CSS偏移反爬虫指的是利用CSS央视将乱序的文字排版为人类正常阅读顺序的行为。这个概念不是很好理解,我们可以通过对比两段文字来加深对这个概念的理解。

HTML文本中的文字:我的学号是1308205,我在北京大学读书
浏览器显示的问题:我的学号是1380205,我在北京大学读书
爬虫看到的学号是1308205,但用户在浏览器中看到的却是1380205.如果不细心观察,爬虫工程师很容易被爬取结果糊弄。这种混淆方法和图片伪装一样,是不会影响用户阅读的。
这种情况可能需要观察页面上的CSS样式,找出其中的规律,将爬虫看到的数据转换成展示给用户的数据。

SVG映射反爬虫

字体反爬虫

文本混淆反爬虫通用解决方法

特征识别反爬虫

webdriver识别

浏览器特征

爬虫特征

隐藏链接反爬虫

App反爬虫

App抓包

APK文件反编译

代码混淆反爬虫

App应用加固知识扩展

了解应用程序自动化测试工具

验证码

字符验证码

计算型验证码

滑动验证码

滑动拼图验证码

滑动拼图验证吧在滑动验证码的基础上增加了随机滑动距离,用户需要使用滑动的方式完成拼图,才能通过校验。

文字点选验证码

鼠标轨迹的检测和原理

验证码产品赏析

综合知识

编码与加密

JavaScript代码混淆

前端禁止事件

法律法规

ChatDoctor A Medical Chat Model Fine-tuned on LLaMA Model using Medical Domain Knowledge

Posted on 2023-05-28 | In 论文浅读 |

ChatDoctor A Medical Chat Model Fine-tuned on LLaMA Model using Medical Domain Knowledge

Abstract

ChatGPT获得巨大成功,可是在医疗领域其回答准确性不足,医疗领域中没有类似能力的llm。因此,采集10万医患对话来微调语言模型,并添加了自主知识检索的功能。提出模型在理解患者需求和提供建议方面的能力有显著提升。

Introduction

现有llm训练过程中未包含医疗领域的数据,导致模型回复医学问题时准确性较差。通过实用医患对话数据微调大预言模型提升模型理解患者需求的能力。还设计了一种基于Wiki的医学领域数据库知识大脑,实时访问权威的医学信息,模型可以根据这些信息回答患者问题。实验证明,微调过的对话模型P\R\F1都要优于CHatGPT。采用LLaMA作为基座,实用Alpaca的52K指令跟踪数据训练通用会话模型。本文贡献有三个:

  1. 设计了一个微调医学llm的框架
  2. 收集、开放了对话数据集
  3. 提出一种能实时访问wiki,能在线分析的ChatDoctor模型

Mehtod

医患对话数据集

收集数据 + 人工过滤 + 语法纠错

外部知识大脑

llm推理过程中很可能给出错误答案,生成的答案往往是不可能控的、随机的,这在医学领域是不可接受的,于是构建一个数据库,其中包括约700种疾病及其相关症状,进一步的医学检测或措施,以及推荐的药物。数据库可以随时更新,无需对模型进行训练,理论上可以根据部门或特定目标建立特定疾病数据库。

基于知识大脑的ChatDoctor

在构建了外部知识大脑之后,我们需要让ChatDoctor自主检索他需要的知识,这通常可以通过构建适当的提示在一个大的语言模型中实现。为了使这一过程自动化,我们设计了关键词挖掘提示符,用于ChatDoctor提取关键字进行相关知识搜索。

A question is provided below. Given the question, extract keywords from the text.
Focus on extracting the keywords that we can use to best lookup answers to the
question.


{Question of patient}


Provide keywords in the following comma-separated format.
Keywords:

模型训练

使用LLaMA作为基座,以Alpaca的训练方式,使用HealthCareMagic-100k的对话对LLaMA模型进行微调,以获得一些基本的对话能力。使用6 * A100 进行3个小时的微调,训练超参数设置为:

Batch Size learning rate epoch max length warm up weight decay
192 2e-5 3 512 0.03 0

实验结果

为了测试基于知识大脑的自主ChatDoctor模型的能力,向模型提出了一些最新的医学问题,如Mpox(猴痘),它于2022年11月28日被世界卫生组织(WHO)新命名。由于这是一个新术语,ChatGPT完全无法回答它,而ChatDoctor可以自主检索Mpox的Wikipedia内容,并给出准确的答案。对于一些一般性的医学问题,如中耳炎,ChatDoctor通过知识检索后提供了非常可靠的答案。Daybue在2023年3月被FDA批准为药物,我们的模型在自主信息检索后也提供了准确的答案。
为了定量评估ChatDoctor的表现,使用iCliniq的问题作为输入,然后使用iCliniq对应的真实医生的回答作为ground truth,也将相同的输入给ChatGPT并记录其回答。我们使用BERT分数分别计算ChatDoctor和ChatGPT的Precision, Recall和F1分数。我们发现经过微调的ChatDoctor模型在Precision, Recall和F1方面都优于ChatGPT。

讨论与结论

ChatDoctor首先使用真实的医患对话数据进行微调,从而让模型更好地理解患者的问题,从而做出更明智的回答,而且还能够自主检索大脑的知识,然后提供答案,进一步提高了模型响应的可信度。在实际应用中,ChatDoctor可以提高医疗诊断的准确性和效率,减少医疗专业人员的工作量,同时增加医疗咨询的机会,特别是对大多数服务不足的医院和第三世界国家的患者。我们相信,我们的聊天医生可以是一个宝贵的援助,改善病人的结果和推进医学研究。
但你不能用,因为这个模型在诊断和医嘱中潜在的语言错误可能会造成严重的后果。而大型语言模型往往会对他们不知道的知识产生许多不正确和有害的陈述(幻觉),这可能会导致滥用。ChatDoctor仅用于学术研究,任何商业用途和临床用途都是严格禁止的。首先,我们没有设计足够的安全措施,现有的模型不能保证医学诊断和建议的完全正确性。其次,我们的模型没有获得医疗保健相关目的[9]许可。第三,ChatDoctor基于LLaMA,拥有非商业许可,所以我们有必要继承这些规则。

argsparse

Posted on 2023-05-21 |

参数配置库-argsparse

argsparse是python的命令行解析的标准模块,内置于python,不需要安装。这个库可以让我们直接在命令行中就可以向程序中传入参数并让程序运行。

引用及使用

声明ArgumentParser对象后,可以使用add_argument方法在parser中添加参数。type用于设置参数的数据类型,default为默认值,help为参数出错时的提示。还可以为参数设置required为True或者False,代表程序执行时必须设定该参数的值,否则会报错。其他的参数设置可以参考命令行选项、参数和子命令解析器

1
2
3
4
5
6
import argparse

parser = argparse.ArgumentParser()
parser.add_argument("--task",type=str,default="nothing",help="执行任务名称")
args = parser.parse_args(args=[])
print(args.task) # nothing

其中”–task”表示task为可选参数,如果没有”–”表示为必选参数,即required,如果运行程序时未指定参数值,即便设置了default也仍然会报错。

使用json生成config文件

训练模型过程中,通常会涉及到多模型的性能比较,不同模型的参数设置往往都是不相同的,使用上述的配置参数方式可能便捷性较差。我们可以将所有的参数分成两部分,一部分是不怎么会调整的参数,直接使用add_argument方法定义,另一部分是模型中可能变动、可能需要调整的参数,将这些参数写成一个json。这样,不同模型调用不同json中的设置,以降低使用不同模型进行试验时调整参数的复杂度。
对于类似输出路径、预训练模型路径等不怎么需要变更的参数使用add_argument方法设定,对于模型学习率等参数,我们记录在json当中。

1
2
3
4
5
6
parser = argparse.ArgumentParser()
parser.add_argument('--config', type=str, default='./config/something.json')
parser.add_argument('--save_path', type=str, default='./outputs')
parser.add_argument('--bert_name', type=str, default=r"E:\MyPython\Pre-train-Model\mc-bert-base")
parser.add_argument('--device', type=str, default="cuda")
args = parser.parse_args(args=[])

我们已经在args中设定了config的路径,使用json读取这个路径就可以得到剩余的参数。我们可以声明一个config类,用来存json当中的每一项参数,然后将两部分参数整合在一起就是所有参数了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Config:
def __init__(self, args):

# 使用json读取参数
with open(args.config, "r", encoding="utf-8") as f:
config = json.load(f)
# 设置需要调整的参数
self.loss_type = config["loss_type"]
self.learning_rate = config["learning_rate"]
self.bert_learning_rate = config["bert_learning_rate"]
self.weight_decay = config["weight_decay"]

# 将两部分参数整合在一起
for k, v in args.__dict__.items():
if v is not None:
self.__dict__[k] = v

def __repr__(self):
return "{}".format(self.__dict__.items())

这样我们就可以在程序中实例化config对象,通过访问对象属性的方式来获取预先设定好的参数。

1
2
config = Config(args)
print(config.learning_rate)

题外话

通常情况下,使用add_augment定义的往往都是单一的参数,实际上在add_augment方法中可以使用nargs关键字来获取多个参数。举个栗子叭:

1
2
3
4
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--files", type=str, default=[r"C:/Users/.ssh/id_rsa",r"C:/Users/.ssh/id_rsa.pub"],nargs='+')
args = parser.parse_args(args=[])

此时我们单独打印args.files可以得到:

1
2
args.files
# ['C:/Users/.ssh/id_rsa', 'C:/Users/.ssh/id_rsa.pub']

可以通过遍历得到每一个传入的路径:

1
2
3
4
for i in args.files:
print(i)
# C:/Users/.ssh/id_rsa
# C:/Users/.ssh/id_rsa.pub

huggingface的参数配置-HFArgumentParser

本节内容来源于transformer.HfArgumentParser的使用
HfArgumentParser是Transformer框架中的命令行解析工具,它是ArgumentParser的子类。用于从类对象中创建解析对象。
在python中,我们习惯于将有关联的一些参数放在一个类当中,HfArgumentParser可以将类对象中的实例属性转换成转换为解析参数。需要注意的是这里的类对象必须是通过@dataclass()创建的类对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from transformers import HfArgumentParser
from dataclasses import dataclass,field
from typing import Optional
@dataclass()
class BasicSetting():
# a:str = field(default="bagging")
model_path : str = field(default=r"E:\MyPython\Pre-train-Model\bert-base-chinese")
@dataclass()
class HyperParameters():
bert_learning_rate:float = field(
default=3e-5
)
parser = HfArgumentParser((BasicSetting,HyperParameters))
basic,hyper = parser.parse_args_into_dataclasses()
print(basic.model_path) # E:\MyPython\Pre-train-Model\bert-base-chinese
print(hyper.bert_learning_rate) # 3e-05

dataclass

dataclass是Python3.7 开始引入的一个新功能, dataclass提供了开箱即用的方法来创建自定义数据, 可以直接实例化、打印和比较数据类实例。
dataclass 可以认为是提供了一个简写__init__方法的语法糖。类型注释是必填项 (不限制数据类型时, 添加typing.Any为类型注释), 默认值的传递方式和__init__方法的参数格式一致.

1
2
3
4
5
6
from dataclasses import dataclass
@dataclass
class HyperParameter:
bert_learning_rate: float
learning_rate: float
print(HyperParameter(5e-5,1e-3)) # HyperParameter(bert_learning_rate=5e-05, learning_rate=0.001)

field

当我们尝试使用可变的数据类型, 给dataclass做默认值时, 可能会导致多个实例公用一个数据从而引发bug。dataclass 默认阻止使用可变数据做默认值,此时需要使用field中的default_factory。

1
2
3
4
5
6
7
8
9
10
11
from dataclasses import dataclass
from typing import List
@dataclass
class HyperParameter:
bert_learning_rate: float
learning_rate: float
hp = HyperParameter(5e-5,1e-3)
@dataclass
class HyperParameter:
paramters : List[HyperParameter] = [hp]
# mutable default <class 'list'> for field paramters is not allowed: use default_factory

此时就需要使用field来完成相应实现。

1
2
3
4
5
6
7
8
9
10
11
12
from dataclasses import dataclass,field
from typing import List
@dataclass
class HyperParameter:
bert_learning_rate: float
learning_rate: float
hp = HyperParameter(5e-5,1e-3)
@dataclass
class HyperParameter:
paramters : List[HyperParameter] = field(default_factory=lambda:[hp])
_hp = HyperParameter()
print(_hp) # HyperParameter(paramters=[HyperParameter(bert_learning_rate=5e-05, learning_rate=0.001)])

梯度累积

Posted on 2023-05-07 | In 深度学习 |

ChatGLM的微调方式

由于类Chat语言模型的参数规模过于庞大,难以使用传统的微调方式全量更新其模型权重。因此出现了如Freeze、Lora、P-tuning等相对成本较低、训练速度较快的微调方式。

Freeze

Lora

P-tuning

P-tuning微调的核心思想是将下游任务转换为“完形填空”。以二分类情感分析任务为例,“___满意。这趟北京之旅我感觉很不错”,如果空位处模型输出为“很”,当前样本即为“积极”,如果空位处输出为“不”,则当前样本属于“消极”。以这种方式识别样本的情感极性,理论上可以实现零样本学习,这种训练方式被称之为PET(Pattern-Exploiting Training)。

PET

PET的实现方式并不复杂,首先,将输入的文本增加一个前缀或者后缀,并且Mask掉某些Token,将下游任务转换为完形填空问题,这样的转换被称为Pattern。然后,构建预测Token的候选空间,建立Token到实际类别的映射,这一步骤被称之为Verbalizer。对于上面的示例,候选空间是{很,不},映射关系是{很→积极,不→消极},候选空间与实际类别之间不一定是一对一,也可以是多对一,比如“很满意、挺满意”都可以用来表示积极。这种训练方式与预训练任务一致,可以有效发挥模型在预训练阶段学习到的知识。然而这种人工构建模板的方式也存在一些弊端。首先,人工构建模板难度较大,不同任务之间模板也无法通用。其次,模型表现很大程度上依赖于构建的模板质量。为避免人工构建模板对模型性能的影响,同时增加Pattern的泛用性,出现了自动构建模板的P-Tuning训练方式。

Sigmoid与Softmax

Posted on 2023-04-26 | In 深度学习 |

Sigmoid

Sigmoid与Softmax是分类任务中的常用激活函数,用于将模型的输出值转换到(0,1)之间的概率。通常情况下,Sigmoid用于二分类任务中,Softmax用于多分类任务中。
$$\delta (t) = \frac{1}{1+e^(-t)}$$

Softmax

很多场景中需要我们找出数组所有元素中值最大的元素,实质上都是求的hardmax。hardmax最大的特点就是只选出其中一个最大的值,即非黑即白。但是往往在实际中这种方式是不合情理的,比如对于文本分类来说,一篇文章或多或少包含着各种主题信息,我们更期望得到文章对于每个可能的文本类别的概率值(置信度),可以简单理解成属于对应类别的可信度。所以此时用到了soft的概念,Softmax的含义就在于不再唯一的确定某一个最大值,而是为每个输出分类的结果都赋予一个概率值,表示属于每个类别的可能性。

优点

Softmax的计算过程中引入了指数函数,目的是为了将输出的数值拉开距离,其效果如以下所示。
$$\delta (t) = \frac{e^t}{\sum^n_1 e^(n)}$$

1
2
3
4
5
6
7
8
import torch
a = torch.tensor([2.,3.,5.])
a = a / torch.sum(a)
print(a) # tensor([0.2000, 0.3000, 0.5000])

b = torch.tensor([2.,3.,5.])
b = torch.softmax(b,dim=-1)
print(b)# tensor([0.0420, 0.1142, 0.8438])

可以发现,经过使用指数形式的Softmax函数能够将差距大的数值距离拉的更大。在深度学习中通常使用反向传播求解梯度进而使用梯度下降进行参数更新的过程,而指数函数在求导的时候比较方便。
此外,将每个类别的输出转化为概率,可以使得每个类别都有一定的概率被预测到,从而解决类别不平衡问题。(猜测:有一些样本较少的类别,可以通过softmax“快速”提高自身被识别出的概率,即使样本数量较少,也能有一个比较好的拟合效果)

缺点

当使用Softmax函数作为输出节点的激活函数的时候,一般使用交叉熵作为损失函数。由于Softmax函数的数值计算过程中,很容易因为输出节点的输出值比较大而发生数值溢出的现象,在计算交叉熵的时候也可能会出现数值溢出的问题。
​在PyTorch中,​可以​使用torch.max函数来获取每个输入值的最大值,​然后将其减去每个输入值,​以避免softmax的数值溢出。

为什么生成模型中需要输入BOS和EOS等特殊标志

Posted on 2023-04-26 | In 深度学习 |

为什么加BOS/EOS

在RNN时代,对于Seq2Seq模型,需要对数据添加开始标志[BOS]和结束标志[EOS],这样做的目的是在解码阶段模型进行自回归计算时,模型可以收到一个结束标志[EOS],同时,在开始解码时,通过输入开始标记[BOS]来确保模型看不到第一个真实的词,这种处理数据的方式被沿用到现在。
|原始文本序列|你是谁?|
|处理后新序列|[BOS]你是谁?[EOS]|

Transformers库提供的预训练模型

以Bart模型为例,源码的forward中有这样的代码:

1
2
3
4
5
if labels in not None:
if decoder_input_ids is None:
decoder_input_ids = shift_tokens_right(
labels,self.config.pad_token_ids,self.config.decoder_start_token_id
)

查看shift_tokens_right的源码,其实现如下:

1
2
3
4
5
6
def shift_tokens_right(input_ids:torch.Tensor,pad_token_id:int,decoder_start_token_id:int):
shifted_input_ids = input_ids.new_zeros(input_ids.shape)
shifted_input_ids[:,1:] = input_ids[:,:-1].clone()
shifted_input_ids[:,0] = decoder_start_token_id
assert pad_token_id is not None
shifted_input_ids.masked_fill_(shifted_input_ids == -100, pad_token_id)

可以发现模型已经将decoder_start_token_id添加到数据当中了。

为什么生成模型中需要输入BOS和EOS等特殊标志

Posted on 2023-04-26 | In 深度学习 |

在使用预训练模型时,我们有时需要使用一些自定义 token 来增强输入,例如使用[ENT_START]和[ENT_END]在文本中标记出实体。由于自定义token并不在预训练模型原来的词表中,因此直接使用tokenizer处理数据会将自定义的特殊标记当作未知字符处理。或者在遇到一些领域中的专业术语时,往往这些术语不存在于词表当中,在tokenize时也会出现问题。这时就需要将这些标记、名词添加到tokenizer中。

添加新token

Huggingface的Transformers库中提供了两种方式来添加新token,分别是:

  • add_tokens()
    在词表的最后添加普通token,返回值为成功添加的token个数;
    函数中包括special_tokens参数,将其设置为true即代表添加的token为special_token
    1
    2
    tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")
    print(tokenizer.add_tokens(["new_tok1", "my_new-tok2"])) # 2
  • add_special_tokens()
    添加包含特殊token的字典,键值从bos_token、eos_token、unk_token、sep_token、pad_token、cls_tpken、mask_token、additional_special_tokens中选择。如果被添加的token不在词表中,则被添加到词表的最后。添加后,可以通过属性来访问这些token。
    1
    2
    3
    4
    5
    tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")
    special_tokens_dict = {"cls_token": "[MY_CLS]"}
    num_added_toks = tokenizer.add_special_tokens(special_tokens_dict)
    print("We have added", num_added_toks, "tokens") # We have added 1 tokens
    assert tokenizer.cls_token == "[MY_CLS]"
    特殊 token 的标准化 (normalization) 过程与普通 token 有一些不同,比如不会被小写。这里我们使用的是不区分大小写的 BERT 模型,因此分词后添加的普通 token [NEW_tok1] 和 [NEW_tok2] 都被处理为了小写,而特殊 token [NEW_tok3] 和 [NEW_tok4] 则维持大写,与 [CLS] 等自带特殊 token 保持一致。

调整Embedding矩阵

无论使用那种方式向词表中添加新token后,都需要重置token embedding矩阵的大小,也就是向矩阵中添加新token对应的embedding,这样模型才可以正常工作。

1
2
3
4
5
6
7
8
9
10
from transformers import AutoTokenizer, AutoModel
tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased")
model = AutoModel.from_pretrained("bert-base-uncased")
original_vocab_size = len(tokenizer)
num_added_toks = tokenizer.add_tokens(['[ENT_START]', '[ENT_END]'], special_tokens=True)
new_tokens_start_index = len(tokenizer) - original_vocab_size
model.resize_token_embeddings(len(tokenizer))
print(model.embeddings.word_embeddings.weight.size())
# Randomly generated matrix
print(model.embeddings.word_embeddings.weight[-2:, :])

token embedding初始化为已有token的值

如果有充分的训练语料对模型进行微调或者继续预训练,那么将新添加 token 初始化为随机向量没什么问题。但是如果训练语料较少,甚至是只有很少语料的 few-shot learning 场景下,这种做法就可能存在问题。研究表明,在训练数据不够多的情况下,这些新添加 token 的 embedding 只会在初始值附近小幅波动。换句话说,即使经过训练,它们的值事实上还是随机的。
比较常见的操作是根据新添加token的语义,将其值初始化为词表中已有token的embedding。例如对于上面的例子,我们可以将 [ENT_START] 和 [ENT_END] 的值都初始化为“entity”对应的 embedding。因为 token id 就是 token 在矩阵中的索引,因此我们可以直接通过 weight[token_id] 取出“entity”对应的 embedding。

1
2
3
4
5
token_id = tokenizer.convert_tokens_to_ids('entity')
token_embedding = model.embeddings.word_embeddings.weight[token_id]
with torch.no_grad():
for i in range(new_tokens_start_index,0,-1):
model.transformer.wte.weight[-i,:] = token_embedding.clone().detach().requires_grad_(True)

全0初始化

在很多情况下,我们需要手工初始化这些新 token 的 embedding。对于 Transformers 库来说,可以通过直接对 embedding 矩阵赋值来实现。例如对于上面的例子,我们将这两个新 token 的 embedding 都初始化为全零向量:

1
2
3
4
#初始化 embedding 的过程并不可导,因此这里通过 torch.no_grad() 暂停梯度的计算。
with torch.no_grad():
for i in range(new_tokens_start_index,0,-1):
model.transformer.wte.weight[-i,:] = torch.zeros([1,model.config.hidden_size],requires_grad=True)

参考链接:
1.为什么生成模型中需要输入BOS和EOS等特殊标志

面试一生之敌之编辑距离的实现

Posted on 2023-04-24 | In Leetcode |

什么是编辑距离?

最小编辑距离即从一个字符串到另一个字符串所需要的最小编辑次数,利用编辑距离可以判断两个字符串的相似程度。在这里定义的单字符编辑操作有且仅有三种:

  • 插入(Insertion)
  • 删除(Deletion)
  • 替换(Substitution)

假设存在两个字符串X和Y,长度分别为N和M。

  • dp[i][j]为X[1…i]到Y[1…j]的最小编辑距离;
  • X[1…i]表示字符串X的前i个字符;
  • Y[1…j]表示字符串Y的前j个字符;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def Levenshtein_Distance(str1, str2):
"""
计算字符串 str1 和 str2 的编辑距离
:param str1
:param str2
:return:
"""
n,m = len(str1),len(str2)
#根据dp[i][j]定义初始化dp
matrix = [[ i + j for j in range(m + 1)] for i in range(n + 1)]
for i in range(1, len(str1)+1):
for j in range(1, len(str2)+1):
if(str1[i-1] == str2[j-1]):
d = 0
else:
d = 1
matrix[i][j] = min(matrix[i-1][j]+1, matrix[i][j-1]+1, matrix[i-1][j-1]+d)
return matrix[len(str1)][len(str2)]
<i class="fa fa-angle-left"></i>123…8<i class="fa fa-angle-right"></i>

77 posts
10 categories
14 tags
© 2024 Antique
Powered by Hexo
|
Theme — NexT.Gemini v5.1.4