Sora技术解析与实战-01

前言

关于Part02的部分,这里分为两次来进行学习,每一次是两小结一共有四小结,所以文章会有些长,大家可以慢慢看,视频链接以及代码仓库会放到下面。

项目地址: ‌⁡⁤‌⁤‌‍⁡⁡‍⁡⁢‍⁡⁣‍⁡‌⁢⁤‍‬‍‌‌‍⁤⁡‬‌⁡⁢⁤⁡‬Sora原理与技术实战 - 飞书云文档 (feishu.cn)

第一小节:Stable diffusion技术解析,基于diffusion的视频生成技术介绍+实战

视频链接: Sora原理与技术实战:文生图片技术路径、原理与SD实战

代码链接: SD推理最佳实践

体验地址: scepter低代码体验

参考资料: How does Stable Diffusion work? (stable-diffusion-art.com)

第二小节:Transformers技术解析+实战(LLM)

视频链接: Transformers技术解析+实战(LLM)

代码链接: chapter2_2.md

论文地址: [1706.03762] Attention Is All You Need (arxiv.org)

推荐视频: Transformer论文逐段精读

一、Stable diffusion技术解析学习+SD推理实践+scepter体验

1.1 Stable diffusion技术解析学习

首先呢,在Sora出来的那一段时间的时候,我在一个学习群里面看到有大佬发了一个Stable diffusion 3的链接,其实当时我并不知到这个是什么东西,我就好奇的点进去看了一下,发现他是一种文本生成图片(text-to-image)的一种模型,在官网可以看到它给了一个例子,给定一个prompt(Epic anime artwork of a wizard atop a mountain at night casting a cosmic spell into the dark sky that says "Stable Diffusion 3" made out of colorful energy )得到了下面这样的图片,看起来还是挺惊艳的,我也是申请了一下他的候补名单。

image-20240302222451938

当时课程出来的时候我就知道为什么要讲Stable diffusion了,因为它跟Sora使用的都是同一个模型也就是Diffusion model。它们都属于AIGC(AI generated content)生成式的AI,那么下面就让我们一起学习一下Stable diffusion的技术原理吧。

关于Diffusion model 其实在part01的时候已经简单介绍过了,那么Stable Diffusion是属于一类称为扩散模型(diffusion model)的深度学习模型,它是生成模型。Sora跟Stable Diffusion相似的地方就在这里它们都存在一个前向和逆向扩散的过程,为了逆转扩散需要教神经网络模型来预测增加的噪声,这是一个U-Net模型,他的训练过程是这样的

  1. 选择一张训练图像,比如一张猫的照片。

  2. 生成随机噪声图像。

  3. 通过将此嘈杂图像添加到一定数量的步骤来破坏训练图像。

  4. 教噪声预测器告诉我们添加了多少噪声。这是通过调整其权重并向其显示正确答案来完成的。

img

在每个步骤中按顺序添加噪声。噪声预测器估计每个步骤的总噪声总和。

那么经过训练后,就会有一个噪声预测器,能够估计添加到图像中的噪声。那么在有了噪声预测器以后呢,我们要生成图片需要噪声预测器告诉我们噪声,然后我们从原始图片中减去噪声,经过几轮以后就会得到我们想要的图片。

img

反向扩散的工作原理是从图像中连续减去预测的噪声。

那么什么是Stable Diffusion model呢? 从字面意思来看是稳定的扩散模型,刚才的扩散模型不稳定吗,那肯定啊不然也不会有Stable Diffusion对吧,哈哈哈😀,开个玩笑。因为图像空间是巨大的,想想看:一个 512×512 的图像有三个颜色通道(红色、绿色和蓝色)是一个 786,432 维的空间!像谷歌的 Imagen 和 Open AI 的 DALL-E 这样的扩散模型都在像素空间中。他们使用了一些技巧来使模型更快,但仍然不够。这就引出来了Latent diffusion model (潜在扩散模型)这种模型就是为了解决这种速度问题。首先将图像压缩到潜在空间中,潜在空间小了 48 倍,因此它的好处是处理更少的数字,这就是为什么它要快得多。这里思考一下就是Sora既然能生成那么高质量的视频和图像没有类似这种快速的模型支持,可想一下他生成要有多慢。

在Stable Diffusion中同样使用了VAE这种变分自编码器技术,那么在训练过程中它不会生成噪声图像,而是在潜在空间(潜在噪声)中生成随机张量。它不是用噪声破坏图像,而是用潜在噪声破坏潜在空间中图像的表示。这样做的原因是它要快得多,因为潜在空间更小。那为什么VAE可以在不丢失信息的情况下将图像压缩到更小的潜在空间中呢?因为自然图像不是随机的,它们具有很高的规律性:脸部遵循眼睛、鼻子、脸颊和嘴巴之间的特定空间关系。换句话说,图像的高维性是人为的。自然图像可以很容易地压缩到更小的潜在空间中,而不会丢失任何信息。这在机器学习中被称为流形假设(manifold hypothesis)

那么在潜在空间中的反向扩散是怎么工作的呢?

首先,生成随机潜在空间矩阵,噪声预测器估计潜在矩阵的噪声,然后从潜在矩阵中减去估计的噪声,重复步骤 2 和 3,直至特定的采样步骤,VAE的解码器将潜在矩阵转换为最终图像。

下面我们来学习一下Stable Diffusion 的是如何处理文本提示的,我们都知道文本提示的处理是决定你生成图片质量的一个很重要的部分。

Tokenizer 首先将提示中的每个单词转换为称为标记的数字。然后,每个标记都转换为称为嵌入的 768 值向量。然后,文本转换器对嵌入进行处理,并准备好供噪声预测器使用。

In Stable Diffusion, text prompt is tokenized and converted to embedding. It is then processed by the text transformer and consumed by the noise predictor.

下面我们分解着来学习一下这个过程

img

文本提示首先由 CLIP 分词器进行标记化

CLIP 是由 Open AI 开发的深度学习模型,用于生成任何图像的文本描述。Stable Diffusion v1 使用 CLIP 的分词器。

因为计算机只能识别数值,所以要讲单词转化为数值。分词器只能对它在训练期间看到的单词进行分词。例如,CLIP 模型中有“dream”和“beach”,但没有“dreambeach”。Tokenizer 将“dreambeach”这个词分解为两个代币“dream”和“beach”。所以一个词并不总是意味着一个令牌!Stable Diffusion仅限于在提示符中使用 75 个token。

img

为什么要进行嵌入?这是因为有些词是相互密切相关的。我们希望利用这些信息。例如,man、gentleman 和 guy 的嵌入几乎相同,因为它们可以互换使用。莫奈、马奈和德加都以印象派风格作画,但方式不同。这些名称具有接近但不相同的嵌入。

img

在嵌入噪声预测器之前,嵌入需要由文本转换器进一步处理。transformer就像一个用于调节的通用适配器。在这种情况下,它的输入是文本嵌入向量,但它也可以是其他东西,如类标签、图像和深度图。transformer不仅进一步处理数据,而且还提供了一种包含不同调节模式的机制。

文本转换器的输出被整个 U-Net 的噪声预测器多次使用。U-Net通过交叉注意力机制(cross-attention mechanism)消耗它。这就是提示与图像相遇的地方。

其实文本提示并不是调节稳定扩散模型的唯一方法,文本提示和深度图像都用于调节图像深度模型。ControlNet通过检测到的轮廓、人体姿势等来调节噪声预测器,并实现对图像生成的出色控制。

ControlNet 是一个神经网络,它通过添加额外条件来控制 Stable Diffusion 中的图像生成。

这里就跟成晨老师提到的基于任务姿势来生产图像比较相似了。

输入是一张姿势图片(或者使用真人图片提取姿势)作为AI绘画的参考图,输入prompt后,之后AI就可以依据此生成一副相同姿势的图片;

imageimage

那么关于Stable Diffusion的技术学习就到这里,下面就是实践部分。

1.2 SD推理实践

代码地址: Generative Models by Stability AI (github.com)

论文地址: 2307.01952.pdf (arxiv.org)

权重地址: stabilityai (Stability AI) (huggingface.co)

这里实践使用的是Stable Diffusion的SDXL(Stable Diffusion XL)模型,上面放的是相关文档和代码的地址有兴趣可以去看看。

废话不多说让我们开始实践吧

首先我们打开魔搭社区,注册一个账号然后创建一个Notebook环境使用CPU环境

这里别听我的开个GPU环境,你往下看就知道了

image-20240303000125085

然后启动我们的Notebook将我们的代码上传上去

sora-tutorial/docs/chapter2/SD推理最佳实践.ipynb at main · datawhalechina/sora-tutorial (github.com)

上传以后安装一下环境

! pip install diffusers -U

嗯...... 就是开始的时候,直接使用SDXL这个运行速度是真的慢,我大概等了两个小时左右,所以在这两个小时里面我写了前面的一部分,那么我们就先简单分析一下代码吧

from modelscope.utils.constant import Tasks
from modelscope.pipelines import pipeline
import cv2
​
pipe = pipeline(task=Tasks.text_to_image_synthesis, 
                model='AI-ModelScope/stable-diffusion-xl-base-1.0',
                use_safetensors=True,
                model_revision='v1.0.0')
​
prompt = "Beautiful and cute girl, 16 years old, denim jacket, gradient background, soft colors, soft lighting, cinematic edge lighting, light and dark contrast, anime, art station Seraflur, blind box, super detail, 8k"
output = pipe({'text': prompt})
cv2.imwrite('SDXL.png', output['output_imgs'][0])

导入库我们就不说了,其实这段代码很好理解

这里task使用“文本到图像合成”(text_to_image_synthesis)模型是。stable-diffusion-xl-base-1.0,使用安全张量(safetensors)来处理数据。

然后提示词是"Beautiful and cute girl, 16 years old, denim jacket, gradient background, soft colors, soft lighting, cinematic edge lighting, light and dark contrast, anime, art station Seraflur, blind box, super detail, 8k"

然后将这段prompt作为输入传入pipe,最后生成图片。

经过两小时的等待终于生成好了我们来看看效果吧

image-20240303001358451

可以看到生成的质量还是不错的,就是速度忒慢了,后来我终于知道为什么我运行的这么慢了,我允许第二个代码的时候报了这样一个错误,NND我脑子瓦特了,我用的CPU跑的😭,我真是一个呆子

image-20240303002604393

然后我们看一下剩下的几个通过加速推理生成的图片吧

这是使用SDXL-turbo模型生成的图片就是头发那一块有点小问题

SDXLturbo

这是SDXL+LCM经过四步生成的图片

image-20240303005310152

将设置成num_inference_steps=2,这个是两步生成的图片,还挺好看嘿嘿😀,是我的理想型,哈哈哈,就是细节有点小瑕疵

image-20240303005459330

到stable-cascade模型生成图片的时候又出错了,报了一个cannot import name 'StableCascadeDecoderPipeline' from 'diffusers',我就怀疑是不是包的版本不对,然后我上网搜了一下重新下了一个版本

pip install git+https://github.com/kashif/diffusers.git@wuerstchen-v3 -U

但是!!!又出现了另一个错误

Cannot load /mnt/workspace/.cache/modelscope/AI-ModelScope/stable-cascade/decoder because embedding.1.weight expected shape tensor(..., device='meta', size=(320, 64, 1, 1)), but got torch.Size([320, 16, 1, 1]). If you want to instead overwrite randomly initialized weights, please make sure to pass both `low_cpu_mem_usage=False` and `ignore_mismatched_sizes=True`. For more information, see also: https://github.com/huggingface/diffusers/issues/1619#issuecomment-1345604389 as an example.

我在网上搜的使用pip install --force-reinstall --no-deps git+https://github.com/huggingface/diffusers.git@a3dc21385b7386beb3dab3a9845962ede6765887这个命令安装一个指定版本但是我试了没用,后来我打开config.json文件看了一下,在这个仓库里面removed kwargs from unet's forward · kashif/diffusers@cbd0775 (github.com)他修改了

-            self.prior.config.c_in,
+            self.prior.config.in_channels,

但是config.json里面的版本是0.26.0dev的版本而且c_in已经改为in_channels了,所以我就将in_channels又改回来了,然后就不会报错了,也就是说config文件与版本对不上,哎又废了我几个小时的实践,让我们看一下它生成图片的效果吧

image-20240303150426620

这是image_1,下面是image_2,效果还是不错的

image-20240303150523290

下面是推理4得到的图片

image-20240303135032450

微调lora叠加推理得到的,这是第一次得到的,吓我一跳,这啥玩意儿,一看就是图像处理不到位

image-20240303140202921

后面我又运行了一次,嗯~,这次还差不多

image-20240303140251306

SD+controlnet得到的,这里需要准备一张图片将他重命名为canny.jpg,我准备是Mikey的图片

image-20240303141043955

让我们看一下经过controlnet以后的图片吧,你如果看轮廓的话还是有原图片的痕迹的就比如左下角的小辫子,小辫子是Draken的

image-20240303141207618

我们尝试将prompt修改成pink butterfly试一下,我倒是没看见蝴蝶的影子但是很好看

image-20240303142406117

1.3 scepter体验

SCEPTER 是一个开源代码存储库,专门用于生成训练、微调和推理,包含一系列下游任务,例如图像生成、传输、编辑。它整合了流行的社区驱动实现以及阿里巴巴集团同益实验室的专有方法,为AIGC领域的研究人员和从业者提供了全面的工具包。这个多功能库旨在促进创新并加速快速发展的生成模型领域的开发。

我也是简单的体验了一下功能还是挺多的,大家可以去看看哦,链接在最上面

image-20240303151943683

二、Transformers技术解析+实战(LLM)

2.1 Transformers技术解析学习

如果要想详细了解Transformer的技术的话还是要去读原论文的,链接我放在上面了,还有就是我这里强力推荐李沐老师的那个Transformer论文精读的那个视频,李沐老师讲的非常好,如果一边听不懂可以多听几遍。为什么要讲Transformer呢是为了后面的代码实践有个基础的了解不然后面的代码是看不懂的,那么废话不多说让我们开始吧

Transformer中最核心的东西就跟他论文名字一样,Attention Is All You Need(你只需要注意),那么我们首先要学习一下什么是Attention机制

2.1.1 Attention机制

我们来看一下这个图

首先我们将一段话分成一个一个的token,然后通过Encoder编码器将每一个token转化为特征向量,然后将每一个向量通过前馈神经网络连到一起,然后通过注意力机制匹配权重最高的那个特征向量,最后通过Decoder解码器生成最后的文本,之里面还涉及到Embedding(嵌入)操作,当然这只是简单的讲述了一下这个过程,那么注意力机制是如何匹配相似的权重呢?

FNN一般指前馈神经网络(Feedforward Neural Network),是一种人工神经网络,其中节点之间的连接不形成循环

前馈神经网络是设计的第一种也是最简单的人工神经网络。在该网络中,信息仅在一个方向上从输入节点向前移动,通过隐藏节点(如果有)并到达输出节点。网络中没有循环或环路。前馈神经网络由三部分组成:输入层(第0层),输出层(最后一层),中间部分称为隐藏层,隐藏层可以是一层,也可以是多层 。

首先Attention的任务是获取局部关注的信息。Attention的引入让我们知道输入数据中,哪些地方更值得关注。其中就涉及到query、key、value也就是QKV,Q、K、V都源于输入特征本身,是根据输入特征产生的向量(它们其实都是一个东西复制过来的,它们的长度都是一样的)。V是表示输入特征的向量,Q、K是计算Attention权重的特征向量。那么在注意力机制中我们首先要计算QK的相似度 W(Q,K) ,然后 W(Q,K) 通过Softmax层进行得到一组权重,最后根据这组权重与对应的Value的乘积求和得到Attention下的Value值。

在计算权重时常用的方式是Dot-product attention(点积注意力机制),这里我简单画了一个图(后面还会用到)方便大家理解

image-20240304182604083

Dot-product attention即点积型注意力机制

该机制是利用点积作为兼容性函数来计算注意力权重的方法。在多个tokens构成的矩阵中,通过矩阵乘法得到n*n维的score矩阵(这里可以参考后面的公式),分别代表不同token之间的兼容性、相似性或者说匹配性。最后通过softmax操作对矩阵的同一行(dim=1)的值进行归一化,得到最终的注意力权重。

2.1.2 模型框架

那么在简单介绍完注意力机制以后呢我们就要来学习Transformer了,首先我们来了解一下他的模型架构(大部分专业术语来自于论文翻译过来的内容)

大多数竞争性神经序列转导模型都具有编码器-解码器结构 [5,2,35]。在这里,编码器将符号表示的输入序列 (x_1, ..., x_n) 映射到连续表示序列 z = (z_1, ..., z_n)

这里就是进行token化并进行Embedding,通过注意力机制生成Value

给定 z,解码器然后生成一个符号的输出序列 (y_1, ..., y_m) 一次一个元素。在每一步中,模型都是自回归的,在生成下一个步骤时,将先前生成的符号作为附加输入。

image-20240304171724547

Transformer 遵循这一整体架构,对编码器和解码器使用堆叠自注意力层和逐点全连接层,分别如上图的左半部分和右半部分所示。

在我们学习这个模型之前,你可以先把下面的内容看一遍,下面讲了Multi-Head Self-Attention和Scaled Dot-Product Attention是如何实现的以及为啥么要用它们。

首先我们要明确几点,对于Multi-Head Self-Attention

  1. 因为Multi-Head 的参数量是一样的,所以并不是head越多参数越多(这个在后面的文档里面也会解释到)

  2. 当head为1是它并不等价于Self-attention,虽然Multi-Head Self-Attention使用的是Self Attention的公式

  3. MultiHead除了 W^q , W^k , W^v 三个矩阵外,还要多额外定义一个 W^o 。(这也是为什么 MultiHead(Q, K, V ) = Concat(head_1, ..., head_h)W^O 这个公式要合并以后乘上 W^o ,它的作用会在后面解析中讲)

明确完以后我们就可以来学习模型了,在这个学习过程中要时刻有那个模型图,我们先忽略Embedding。

在进入Multi-Head Self-Attention前需要将词按照“词向量维度”这个方向,将Q,K,V拆成了多个头,这里head简单设为2,(为了简单起见,该图忽略了 Softmax 和 d_k )如图:

image-20240304233552062

有了2个head以后就要每个分别进行计算了

image-20240304235758742

不知道大家看懂这个图没有,其实啊每一个QKV的运算都是一个Attention(这里我将每一个head计算写在一个Attention,其实应该分开的),然后运算出来的A_1和A_2作为下一个Attention的输入,这个就叫Self-Attention(自注意力机制)。但是这样拆开来计算的Attention使用Concat进行合并效果并不太好,所以最后需要再采用一个额外的 W^o 矩阵,对Attention再进行一次线性变换,这也就是他为什么加了这样一个额外的矩阵 W^O\in\R^{{hd_v}×d_{model}}

在Transformer中模型架构图上面的Decoder中有一个Masked Multi-Head Self-Attention,那它是什么呢,这个Mask掩码是为了让当前的这个词只能看见前面的词而不让他看见后面的词,因为在解码器中你要预测当前这个词,那你就要比较他们之间的权重,为了不影响前面权重的比较就会把后面的权重设置为0,这就是Mask的作用。

2.1.3 Multi-Head Self-Attention和Scaled Dot-Product Attention

那么在Transformer中提出了Multi-Head Self-AttentionScaled Dot-Product Attention,它们分别是什么呢?

Multi-Head Attention---多头注意力机制 。简单来说就是多个Self- Attention的组合,它的作用类似于CNN中的多核。但是多头Attention的实现不是循环的计算每个头,而是通过transposes and reshapes,用矩阵乘法来完成的。

多头注意力机制允许模型同时关注来自不同位置的不同表示子空间的信息,从而提高了模型的表示能力。此外,由于每个头都可以独立地进行注意力计算,因此多头注意力机制也可以并行计算,提高了模型的计算效率。

Scaled Dot-Product Attention是一种基于矩阵乘法的注意力机制,用于在Transformer等自注意力模型中计算输入序列中每个位置的重要性分数 。在Scaled Dot-Product Attention中,通过将q向量和k向量进行点积运算,并将结果除以注意力头数的平方根来缩放,得到每个查询向量与所有键向量间的注意力权重。这些权重同时乘以值向量,然后求和,从而生成最终的输出向量。通过这种方式,模型能够自适应地捕捉输入序列中不同位置之间的相关性,从而在自然语言处理等任务中取得优秀的性能。

为什么Transformer要提出多头注意力机制呢,因为它们发现与其使用 d_{model} 维度的K、V和Q执行单个注意力函数,不如将Q、K和V线性投影 h 次,分别将不同的学习线性投影到 d_k、d_k 和 d_v 维度。然后,在 Q、K和V 的这些投影版本中,并行执行注意力函数,从而生成 d_v 维度的输出值。这些串联起来并再次投影,从而产生最终值,如图所示。

image-20240304185359886

d_{model}是指Transformer模型中输入和输出向量的维度。在一般的 Transformer 模型中, d_{model} 的值通常是512 或 1024。它决定了模型的复杂度和能力,同时也会影响模型的训练速度和内存占用。

当Transformer的输入数据是时间序列数据时, d_{model}代表着每个时间步的输入信号的维度,它等于词嵌入向量的维度乘以序列长度。它也是在自注意力机制中,用来定义查询、键和值的维度。 d_{model}越大,模型学习到的信息就越多,但也会导致模型参数过多,训练复杂度和计算成本的增加。

多头注意力允许模型共同关注来自不同位置的不同表示子空间的信息。对于单个注意力头,平均可以抑制这一点。

MultiHead(Q, K, V ) = Concat(head_1, ..., head_h)W^O其中

head_i = Attention(QW^Q_i , KW^K_i , VW^V_i )

其中投影是参数矩阵 W^Q_i\in\mathbb{R}^{d_{model}×d_k} W^K_i\in \mathbb{R}^{d_{model}×d_k}W^V_i\in \mathbb{R}^{d_{model}×d_v}W^O\in\R^{{hd_v}×d_{model}} .在这项工作中,使用了 h = 8 个平行的注意力层或头部。对于其中的每一个,使用 d_k = d_v = d_{model}/h = 64。由于每个头部的尺寸减小,总计算成本与全维的单头注意力相似。

它们将 particular attention“ Scaled Dot-Product Attention(缩放点积关注)”(上图)。输入由维度 d_k 的查询和键以及维度 d_v 的值组成。计算所有键的查询点积,将每个键除以 \sqrt d_k,然后应用 softmax 函数来获取值的权重。在实践中,同时计算一组query的注Attetion函数,并打包到矩阵 Q 中。键和值也被打包到矩阵 K 和 V 中。

我们将输出矩阵计算为:

Attention(Q, K, V ) = softmax(\frac{QK^T}{\sqrt d_k})V

两个最常用的注意力函数是加性Attention和点积(乘性)Attention

加性Attention,如(Bahdanau attention):

\boldsymbol{v}_a^{\top}\tanh \left(\boldsymbol{W}_{\mathbf{1}} \boldsymbol{h}_t+\boldsymbol{W}_{\mathbf{2}} \overline{\boldsymbol{h}}_s\right)

乘性Attention,如(Luong attention):

\operatorname{score}\left(\boldsymbol{h}_{t},\overline{\boldsymbol{h}}_{s}\right)=\left\{\begin{array}{ll}\boldsymbol{h}_{t}^{\top}\overline{\boldsymbol{h}}_{s} & \text { dot } \\\boldsymbol{h}_{t}^{\top} \boldsymbol{W}_{a} \overline{\boldsymbol{h}}_{s} & \text { general } \\\boldsymbol{v}_{a}^{\top} \tanh \left(\boldsymbol{W}_{a}\left[\boldsymbol{h}_{t} ; \overline{\boldsymbol{h}}_{s}\right]\right) & \text { concat }\end{array}\right.

dot-product (multi_plicative) attention与我们的算法相同,只是比例因子为 \frac{1}{\sqrt d_k}。加法注意力使用具有单个隐藏层的前馈网络计算兼容性函数。虽然两者在理论上的复杂性相似,但点积注意力在实践中要快得多,空间效率也高得多,因为它可以使用高度优化的矩阵乘法代码来实现。虽然对于较小的 d_k 值,两种机制的性能相似,但加法注意力优于点积注意力,而没有缩放较大的 d_k 值。对于较大的 d_k 值,点积的幅度会变大,从而将 softmax 函数推入梯度极小的区域 4 。为了抵消这种影响,将点积按 \frac{1}{\sqrt d_k}缩放。

2.2 代码实践-transformer

终于将Transforme技术解析写完了,脑细胞又死了好几亿😭,中间其实还省略了好多没讲,比如Encoder and Decoder还有Embeddings and Softmax啊,如果想继续学习的话可以去看一下论文或者看李沐老师的那个视频。那就让我们开始代码实践吧

实践的代码就是按照Transformer的模型架构进行的,这里就简单讲一下部分代码,这里分为模型定义、数据处理、训练还有推理。

2.2.1 模型定义

首先是selfattention.py

  1. 首先,通过线性层将输入序列转换为查询向量q、键向量k和值向量v。将查询向量q、键向量k和值向量v分别转换为(batch_size, seq_len, num_heads, head_dim)的形状。

        def __init__(self, config):
            super().__init__()
            self.config = config
            
            assert config.hidden_dim % config.num_heads == 0
            
            self.wq = nn.Linear(config.hidden_dim, config.hidden_dim, bias=False)
            self.wk = nn.Linear(config.hidden_dim, config.hidden_dim, bias=False)
            self.wv = nn.Linear(config.hidden_dim, config.hidden_dim, bias=False)
            
            self.att_dropout = nn.Dropout(config.dropout)

  2. 将查询向量q转置(batch_size, num_heads, seq_len, head_dim)

    def forward(self, x):
            batch_size, seq_len, hidden_dim = x.shape
            
            q = self.wq(x)
            k = self.wk(x)
            v = self.wv(x)
            
            q = q.view(batch_size, seq_len, self.config.num_heads, self.config.head_dim)
            k = k.view(batch_size, seq_len, self.config.num_heads, self.config.head_dim)
            v = v.view(batch_size, seq_len, self.config.num_heads, self.config.head_dim)
            
            q = q.transpose(1, 2)
            k = k.transpose(1, 2)
            v = v.transpose(1, 2)

  3. 计算注意力权重att,使用softmax函数将注意力权重归一化。应用注意力权重dropout。计算上下文向量attv,将注意力权重与值向量v相乘,然后将结果转换回(batch_size, seq_len, hidden_dim)的形状。

   # (b, nh, ql, hd) @ (b, nh, hd, kl) => b, nh, ql, kl
        att = torch.matmul(q, k.transpose(2, 3))
        att /= math.sqrt(self.config.head_dim)
        score = F.softmax(att.float(), dim=-1)
        score = self.att_dropout(score)
        
        # (b, nh, ql, kl) @ (b, nh, kl, hd) => b, nh, ql, hd
        attv = torch.matmul(score, v)
        attv = attv.view(batch_size, seq_len, -1)
        return score, attv      

然后定义了一个Config类通过@dataclass装饰器定义了一些数据

  1. vocab_size: 词汇表大小。

  2. hidden_dim: 隐藏层维度。

  3. num_heads: 多头注意力机制中的head数。

  4. head_dim: 每个注意力头的维度。

  5. dropout: dropout概率,用于控制模型训练过程中的dropout比例,以防止过拟合。

  6. num_labels: 标签数,用于确定模型输出的标签数,因为我们是文本分类这里指的是类别数量。

  7. max_seq_len: 最大序列长度,用于确定模型处理的最大序列长度。

  8. num_epochs: 训练轮数。

@dataclass
class Config:
    
    vocab_size: int = 5000
    hidden_dim: int = 512
    num_heads: int = 16
    head_dim: int = 32
    dropout: float = 0.1
    
    num_labels: int = 2
    
    max_seq_len: int = 512
    
    num_epochs: int = 10

然后将这些参数传给model,然后使用torch.randint函数生成一个3行30列的矩阵,其元素在(0,5000))范围内随机分布。然后使用我们刚才自定义的模型对我们的随机矩阵处理得到权重和logits

2.2.2 数据处理

这里数据处理将缺失行的数据都删除掉了然后对label进行了均衡处理

df = df.dropna()

df = pd.concat([df[df.label==1].sample(2500), df[df.label==0]])

为了方便模型学习定义了一个分词器和dataset.py

def collate_batch(batch):
    label_list, text_list = [], []
    for v in batch:
        _label = v["label"]
        _text = v["text"]
        label_list.append(_label)
        text_list.append(_text)
    inputs = tokenizer(text_list)
    labels = torch.LongTensor(label_list)
    return inputs, labels

collate_batch将数据中的文本(text)和标签(label)分别提取出来,然后将文本进行分词(tokenizer)处理,并将标签转换为LongTensor类型。最后,函数返回分词后的文本和标签。它的输入参数是一个名为batch的列表,其中包含了多个字典,每个字典代表一个样本,包含"label"和"text"两个键值对。

然后将处理好的数据将原始数据集(ds)划分为训练集(train_ds)和测试集(test_ds),其中测试集的大小为原始数据集大小的20%。然后,再次使用train_test_split函数将训练集(train_ds)划分为训练集(train_ds)和验证集(valid_ds),其中验证集的大小为训练集大小(train_ds)的10%。

train_ds, test_ds = train_test_split(ds, test_size=0.2)
train_ds, valid_ds = train_test_split(train_ds, test_size=0.1)
len(train_ds), len(valid_ds), len(test_ds)

然后定义三个加载器分别处理三个数据集

train_dl = DataLoader(train_ds, batch_size=BATCH_SIZE, collate_fn=collate_batch)
valid_dl = DataLoader(valid_ds, batch_size=BATCH_SIZE, collate_fn=collate_batch)
test_dl = DataLoader(test_ds, batch_size=BATCH_SIZE, collate_fn=collate_batch)

2.2.3 训练

训练的时候导入自定义好的trainer.py文件,然后将训练轮数设为10,使用GPU或CPU运行导入配置和模型

NUM_EPOCHS = 10
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

config = Config(5000, 64, 1, 64, 0.1, 2)
model = Model(config)
model.to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-3)
train(model, optimizer, train_dl, valid_dl, config)

test(model, test_dl)

2.2.4 推理

最后从测试数据集中随机选择一个样本,确保文本长度不超过20个字符,使用分词器对文本进行处理,并将处理后的结果赋值给inputs。使用推理函数对处理后的文本进行推理,并将结果赋值给attnprob。然后从注意力权重中提取注意力概率,并将其转换为numpy数组。然后使用分词器对原始文本进行分词,并将结果赋值给tokens。最后返回结果和概率

sample = np.random.choice(test_ds)
while len(sample["text"]) > 20:
    sample = np.random.choice(test_ds)

print(sample)

inp = sample["text"]
inputs = tokenizer(inp)
attn, prob = infer(model, inputs.to(device))
attn_prob = attn[0, 0, :, :].cpu().numpy()
tokens = tokenizer.tokenize(inp)
tokens, prob
{'text': '房间有些陈旧!不过还算舒适,交通也方便。', 'label': 1}
(['房间', '有些', '陈旧', '!', '不过', '还', '算', '舒适', ',', '交通', '也', '方便', '。'], 1)

可以看到这个推理效果还是不错的

image-20240305170246466

2.3 LLM实践-tinyllamas

这里的模型下使用的是最小的llama模型,准备工作就不说了直接进入代码

这里需要先把二进制的模型文件转为numpy数组形式

!python convert_bin_llama_to_np.py stories15M.bin

然后创建分词器和模型文件以及模型参数

args = ModelArgs(288, 6, 6, 6, 32000, None, 256)

token_model_path = "./tokenizer.model.np"
model_path = "./stories15M.model.npz"

tok = Tokenizer(token_model_path)
llama = Llama(model_path, args)

最后设置prompt生成文本

prompt = "Once upon"

ids = tok.encode(prompt)
input_ids = np.array([ids], dtype=np.int32)
token_num = input_ids.shape[1]

print(prompt, end="")
for ids in llama.generate(input_ids, args.max_seq_len, True, 1.0, 0.9, 0):
    output_ids = ids[0].tolist()
    if output_ids[-1] in [tok.eos_id, tok.bos_id]:
        break
    output_text = tok.decode(output_ids)
    print(output_text, end="")

结果

Once upon a time, there wa a little girl named Jane. Jane had a special journal. Every day she drew picture in it and wrote about her friend and her family. 
One day, Jane wa feeling very miserable. She wa so sad that she started to cry. Her mommy and daddy were outside, and Jane thought she would have a ore tummy. 
Her mommy and daddy took her to the doctor. The doctor looked at Jane' journal and said that Jane would be healthy again. Jane wa so happy! 
The doctor gave Jane a high look at her journal and said that it wa time to get it clean. Jane smiled and said that it felt good. 
And from that day on, Jane alway remembered to wah her journal when she wa feeling miserable.

2.4 思考

这里对长琴老师提出的问题进行一下回答,因为文章太长了我就把这些回答放到我的文档里面了

文档链接:人工智能(Artificial Intelligence) (lingkongstudy.com.cn)