1. graphviz docker
1 | FROM ubuntu:18.04 |
2. networkx
1 | from pprint import pprint |
1 | FROM ubuntu:18.04 |
1 | from pprint import pprint |
最近面试了一北京候选者,之前使用电子病例以及CT图像两种模态信息,训练ViLT多模态预测模型,提高肺结节良恶性预测准确率。
正好我对多模态如何对齐也比较感兴趣,以Transformers-Tutorials提供的代码为例,来看下其内部是如何实现的。
数据集我没有从VQA下载,太大了,这里也强烈安利huggingface提供的lmms-lab/VQAv2
dataset。
剩下就改下VQADataset
部分,其他保持不变。
这里记录比较有趣的几个点。
答案:在第二维对齐。
具体来说,text部分使用的是BertTokenizer,max_position为40(所以如果有长文本,这里就坑了),假设batch_size为4,text embedding出来后就是(4, 40, 768)。
image部分使用的也是ViT,所以整体思想和CLIP是一样的,得到patch后flatten,然后concat到一起即可, 即embeddings = torch.cat([text_embeds, image_embeds], dim=1)
。
相比CLIP,不同之处在于支持不同宽高的image,CLIP默认只支持(224, 224)宽高的图像。所以ViLT多了个visual pad操作,另外kernel size也不一样。如果深究的话,可以看看visual_embed部分。
ViltForTokenClassification
?是的,上面既然已经对齐了,那么前面都是text部分,所以做文本相关例如token classification等这部分也是没问题的。
ViltForQuestionAnswering
是QA吗?先入为主来讲,这个A
呢,是不固定的,比如使用generation
来生成。但是这里呢,是有限的!!!。
怎么说呢,人家提供了个类似[CLS]
,然后你可以指定自己的类别,所以他是个分类器。
这里作者使用了finetuned的label来作为演示,可以看看”dandelin/vilt-b32-finetuned-vqa”下config.json中的label2id查看标签数量。
1 |
|
排序
新操作正如作者所说的那样,VQAv2是个多标签分类数据集,什么意思呢,例如下图。
它的question:where is he looking?
它的标注结果annotations:
1 |
|
answer是有多个的,也就是说它的answers是个众包的,有的人认为是down,有的人认为是table。
例如上面标注结果,首先统计answer次数,得到下面结果:
1 | answers_count = {'down': 7, 'at table': 1, 'skateboard': 1, 'table': 1} |
接着对每个answer count使用get_score
进行平滑处理,get_score有三种可能,那每种可能就认为是每种答案的权重。
1 | def get_score(count: int) -> float: |
在ViLT出来后,接了个分类器,这个分类器的标签数量来自自己实际业务指定,比如answers的总数量。假设一共有10个类别,down类别是1,at table是2,依次类推,得到label如下:
1 | label = [1.0, 0.3333333333333333, 0.3333333333333333, 0.3333333333333333, 0, 0, 0, 0, 0, 0] |
接着计算loss如下:
1 | loss = nn.functional.binary_cross_entropy_with_logits(logits, labels) * labels.shape[1] |
这里又乘了个labels总数量,可以简单理解成loss scale。
作者使用topk进行解码:
1 | probs, classes = torch.topk(predicted_classes, 5) |
至此,大致理解了整体流程。
这里是get到一种新的实现方式了哈,那接下来我想对其扩展下,看看还有哪些方式可以做这种。
1、rank loss。排序中常用到。
2、目前大模型中的打分模型(score model),在最开始我没看其实现的时候,我曾深深纠结于这又是什么新的技术,等我看完后,我感觉更应该关注的是“打分”俩字,即强化学习思想中的critic model。本质技术层面没有太大变化,作为打分模型为Actor提供优化方向。
3、DPO。对,你没看错,强化学习算法DPO,本质思想就在于好的要逐渐和坏的拉开差距,使其更符合人类偏好。
看的越多,会发现还是有共通之处的。
分享个模型可解释的库SHAP,其可以对XGBoost等机器学习库进行结果解释。
更多还有类似Paddle中的TrustAI。
SHAP(SHapley Additive exPlanations)是用来解释机器学习模型的工具。它告诉我们每个特征对模型预测结果的贡献有多大。想象一下你和几个朋友一起干活,最后得到了一笔报酬。你想知道每个人应该分多少钱,SHAP就是在做类似的事情,只不过它是在告诉你每个特征对模型预测结果的贡献。
想象一个团队在完成任务后得到一笔奖金,我们想公平地分配这笔奖金。Shapley值就是一种分配方法,告诉我们每个成员(在机器学习中是每个特征)应得的报酬(对预测结果的贡献)。
假设我们在玩一个游戏,每次加入一个新成员(特征),看看他们对团队成绩(模型预测)的贡献有多大。边际贡献就是每次新加入的成员对总成绩的额外贡献。
SHAP计算每个特征的贡献时,会考虑所有可能的加入顺序。这是因为不同的加入顺序可能会导致不同的贡献。举个例子:
对于每个特征,计算它加入不同特征子集时,对模型预测结果的边际贡献。
对于每个特征,计算它在所有可能子集中的平均贡献,这就是这个特征的Shapley值。
假设我们有一个模型在预测一个人是否会购买某产品,模型用到了年龄、收入和是否有孩子三个特征。SHAP会做以下事情:
对于每个特征,计算它在所有顺序中的平均贡献,这个平均值就是该特征的Shapley值。
SHAP通过考虑每个特征在不同组合中的贡献,并取平均值,来告诉我们每个特征对模型预测的影响有多大。这样,我们就能清楚地知道模型是如何做出预测的,每个特征在其中起了多大作用。
演示代码:https://github.com/geasyheart/algo/blob/m2/python/shap_demo.ipynb
DPO全称Direct Preference Optimization,它是RLHF算法的一种,相比PPO算法来讲,它只需要actor和ref model,少了critic和reward model。其核心期望为good loss - bad loss越来越大, 这点和排序模型中的rank loss很相似,但是又不希望和ref model偏差太多。
1 |
|
值函数希望学习一个价值函数,这个值可以用于评估当前决策的分值。策略函数希望学习一个策略函数,拿到其动作的概率分布。
Actor-Critic是在策略函数的基础上,额外引入学习价值函数,来帮助策略函数更好地学习。
下面这个图很好表示了两者关系。
重点看actor-critic算法中update函数log_probs
部分。actor采用策略,critic来进行评价。
这个是RLHF系列中的策略梯度部分,在看了Hands-on-RL和parl两者实现后,感觉整体难度并不是很高,但是当自己从零实现时还是会莫名其妙多一些问题,相比深度学习来讲,还是有蛮多小细节是需要额外注意的。
这里是指learn阶段中的获取最大期望阶段,如下代码所示:
1 | output = self.model(obs_bs) |
在最开始自己实现时,我没有加log进行平滑,发现模型没法收敛(CarPole-v0 reward最大得分为200),一直是8,9徘徊。后来我看了上述实现,发现这里多了个log,这里让我觉得很困惑,因为我觉得这一步是不必要的,原因有以下几个方面:
但是呢,如果不加log这一步,模型就无法收敛。
这里是指在每一次done,产生了一批state、reward、action之后,在进行计算loss时,下一步的reward还要考虑当前步reward的结果,即下一步的reward一定要小于当前步的reward。也就是calc_reward_to_go
函数这里。
这里同样也会觉得很困惑,因为如果希望期望最大,那就reward * prob使其概率最大即可。
如果看Hands-on-RL他的实现,不会感到任何困惑,因为他是用for来做的。但是呢,parl的实现在考虑了reward * prob使其概率最大
这一步之后,又添加了calc_reward_to_go
函数,从而本来是个离散的东西,强生生的给变成了一个连续状态的事情,关键呢,怎么看都不像是连续的,因为当前步在计算loss时也并没有跟上一步扯上直接的关系。
这一步还好,从理解角度来讲,我会更倾向Hands-on-RL的实现,容易理解。
关于第一点,我的感觉是步子不能迈太大,宁可慢慢收敛,也要比无法收敛更强,例如model部分我尝试改成如下:
1 | class PolicyNet(torch.nn.Module): |
即forward部分不用softmax,剩下代码保持不变,也同样处于无法收敛状态,似乎来看,softmax+log才是这个算法成功的关键。
这里让我想到一个事情,在深度学习Layer参数初始化的过程,比如:
1 | a = nn.Linear(2222, 11) |
我们会是这么写,基本不会关心a这个linear的参数是如何初始化的。是因为框架内部已经考虑了kaiming、xavier、uniform等各种初始化技术。如果不加这些参数初始化技术,模型基本也很难收敛。
所以如果应用的话,可以采用现成的实现,如果研究的话,其中一些细节可以慢慢调整。
1 | import random |
下面记录下DQN算法以及一些细节,注意哦,本博客更多目的在于当下记录,并非完整严谨的哦,也或许有理解错误。
关于DQN,看了下网上的介绍以及从Q-Learning到DQN解决state和action无法枚举完的情况。另外也强烈推荐下面链接:
看下面这个DQN网络,你觉得有问题么?
1 | class DQN(nn.Module): |
啊啊啊,这里为什么要加softmax呢,是不是习惯了深度学习那套分类思想,再好好想想这里,是不是不应该加softmax。
仔细观察下面代码,eps是在做sample时起作用的,也就是select_action
这里,可以看到,在每个epoch结束时,更新新的eps值,从最开始的1,到最后的eps_end=0.1,它是一个比较平滑的曲线。
从select_action
可以观察到,action早期处于随机采样的状态,随着epoch的增加,action的决策更多过度到model决策。
那这里是否是必须的呢?
不,这里并不是必须的,因为完全可以在train之前加一个warmup步骤,让model一定程度上学会state到action这个变化。再到后面,就是正常训练流程,不需要sample这个过程了。
那这里更多起到什么作用?
我觉得有个点可以很好理解这里,即从完全小白到慢慢学习直至认知理解的过程。添加warmup,即先提前产生一批训练样本,而eps这里,即随着过程慢慢学习,不过相比warmup,可以更优采取权重随机采样方案,即torch.multinomial(torch.softmax(self(obs), dim=-1), 1).item()
,它表现出来的特点是权重高的多次采样出现的频率也会更高,那么随着模型的优化,采样更优的可能性也会提升。
不,这里也并不是必须的。观察整个过程,policy_net要比target_net新一个epoch,而且他俩实际上是在干同一个事情,那么可以将target_net指向policy_net,可以发现epoch的增加,reward的值也是正常提升的。
reply memory可以说记录了整个state和action等的过程,当然有一个maxlen来限制其大小,过早的数据就不要了。
不过需要指出的是,改动了上面这些点,虽然也可以收敛,但是可能会收敛变慢。
这里借鉴了知乎网友实现DQN。
pygame==2.1.0
1 |
|
大模型最近很是火啊,媒体铺天盖地的宣传,候选者简历中写LLM微调等等。本文希望以huggingface trl/RLHF notebooks讲到的几个例子作为入口,介绍下RLHF在整个训练工作中的位置以及起到的作用,方便理解与后续应用。
在huggingface trl/RLHF notebooks这个文件夹下,一共有三个例子:
同时也按照上述这三个文件顺序进行分析。
目的:这个文件实现的是如何利用RLHF学会生成正向评论。
数据集默认有两个字段,text
和label
,即用户对一部电影的评论和这条评论的情感倾向(正向、负向)。
这里对text
字段随机截断长度为n后面的文本,例如:text=这个电影我觉得很棒
。 截取后变成query=这个电影
。
这里采用GPT2作为训练model,ref model和model是一样的,可先理解成model是用来训练的,ref model是用来参考的。
ref model是RLHF训练过程中不可缺少的一部分,也跟在generation model后面添加ValueHead层是一个道理,关于强化学习更细力度,本文先忽略。
这里采用distilbert-imdb模型来作为打分模型,这个模型的作用是输入一条评论,它会给出positive、negative的打分。
即让model基于query生成指max_new_tokens的文本,然后让reward model来打分,以positive score为目标,不断优化model,使其能够基于用户给定的文本开头来生成正向评论。
这里的max_new_tokens也比较有意思,它可以有两层的不同解释:
关于后者,我觉得会是一个比较有意思的点。在RLHF中,有针对每一步给一个score,还有走完后针对整条路径给一个score。那这里的max_new_tokens是不是就可以理解成是中间的状态~
既不会因为每一步都打分造成训练效率低下也不会因为对整条路径打分导致某些点决策失误所带来的更大偏差,尽量缓解这种情况。
结束。
目的:通过添加prompt来控制生成评论的情感。
这里的prompt有三类:positive、negative、neutral。由于neutral是reward model本身能力所不具备的,看到这里也可以跳过。
其构造示例如下:
1 | query="[positive]这个电影很" |
那么预期目标是好
。
如果是
1 | query="[negative]这个电影很" |
那么预期目标是不好
,差
之类的情感。
剩下流程和上面文件一致,此处忽略。
目的:RLHF的目标是超越原有天花板,那这种是选取ref model的best of n来和RLHF训练后的做个比对。
整体下来,reward model占据很重要的作用,决定了RLHF的效果,需要注意。
更多看下原代码,整体流程不是很复杂。又水水水了一篇。
以ChnSentiCorp作为情感分类数据集。
train_score.py
1 |
|
训练后F1能达到95%,所以打分模型至此结束。
1 | import pandas as pd |
1 | from transformers import AutoModelForCausalLM, AutoTokenizer |
使用RLHF训练后的生成结果示例如下:
1 | 这个电影,见证点深的艺术价值。<|endoftext|> |
前两个例子还好,比较容易生成理想的正面评论,第三个和第四个例子前面说到了天气很差
和屏幕很差
,都是偏负面的评论,但是后面生成的文本还是能生成正向的回答,说明经过强化学习的确有产生预期效果。
关于文本流畅问题,有个问题是Wenzhong-GPT2本身产生通顺句子的能力就比较弱,但经过多轮训练,也能产生除生成正向评论外的效果,这点也是很nice.
阿里出了个qwen1.8B,对于资源有所要求的场景或者需要支持长文本的场景,应该是目前国内在这个量级内最优的选择了吧。接下来以此来打通微调、部署各个流程,算是一次记录。
首先按照要求和快速使用来跑起来,安装flash-attn,先跑下推理,正常,接下来就进入微调阶段。
按照微调流程,这里采用LoRA进行微调,但是需要注意的是,虽然官方给出了显存占用及训练速度,但是我在1080Ti上得到的显存占用还是要更高一些,大家可以将这个指标理解成为运行起来至少需要的显存,在进行训练时,还是会有一些增高。
训练的话采用finetune_lora_single_gpu.sh
默认配置,幸亏我没有采用train,而是使用了dev数据集,7500条数据,8个多小时,,不过整个loss还是蛮正常的,没有出现issue里出现的各种问题。。。
我这里保存到了outout_qwen,下面为调用LoRA微调后的模型。
1 | path = 'Qwen-1_8B-Chat' |
注意的是,如果到此就打算部署的话,也要将adapter_config.json
中的base_model_name_or_path
正确引用,不过官方也给了合并代码,可以将LoRA和qwen合并到一起。
1 | from peft import AutoPeftModelForCausalLM |
如果需要使用llama.cpp,这里还是建议进行上述步骤合并的,使用llama.cpp也不是很复杂,可按照官方README安装cmake编译安装,后续也不是很复杂,官方提供了非常清晰的使用说明。
1 | mkdir build |
不过需要注意的是,用不用量化或者说要不要做转换,本身上还是要根据转换后的效果和效率来决定的,由于目前我们直接跑在GPU上,后续有机会单独针对llama.cpp尝试深入一下~
Lora,是微软出的一种在低资源场景下进行微调大模型的实现方式,在transformers里有peft这个包进行调用,它通过固定预训练模型权重并只训练新增lora层来实现微调,目前其在比如Baichuan2、ChatGLM上都有相关资料,更多介绍可自行搜索了解。
其简单理解实现方式为,比如qkv的linear为768*768(更大模型可能会更大),那lora通过新增两个linear(lora_A和lora_B),引入一个超参r
来降低训练参数量,其伪代码如下:
1 |
|
比如当r设置为1时,是否可以明显感受到训练参数量的变化。
注意:Lora本质只有在微调阶段生效,其加速了微调,但是对于推理阶段不会加速,而且因引入了新的计算会导致推理速度略有下降。
注意,此篇文章通过介绍LoraLinear的实现,来说明Lora的工作原理。
其实现在这里。
LoraLinear继承自nn.Linear,所以是在Linear的基础上进行改动。
Linear的weight不进行训练,这也符合Lora的原理说明。
1 | self.weight.stop_gradient=True |
forward部分,代码如下:
1 | def forward(self, input: paddle.Tensor): |
我们看如果pretrained model和lora不进行merge的时候,其推理是pretrained model先计算,拿到result,然后和lora计算结果进行相加获取最终结果。
对于Parameter Efficient fine-tune,Lora是其中一个实现方式,其他还有比如p-tuning等。但是通过Lora,我们大致理解了从原来直接对pretrained model fine-tune到现在的引入新的参数(比如)来使得我们能用低资源来使用LLM的能力。
缺失模块。
1、请确保node版本大于6.2
2、在博客根目录(注意不是yilia根目录)执行以下命令:
npm i hexo-generator-json-content --save
3、在根目录_config.yml里添加配置:
jsonContent: meta: false pages: false posts: title: true date: true path: true text: false raw: false content: false slug: false updated: false comments: false link: false permalink: false excerpt: false categories: false tags: true