导读
学习Transformer,快来跟着作者动手写一个。
作为工程同学,学习Transformer中,不动手写一个,总感觉理解不扎实。纸上得来终觉浅,绝知此事要躬行,抽空多debug几遍!
注:不涉及算法解释,仅是从工程代码实现去加强理解。
一、准备知识
以预测二手房价格的模型,先来理解机器学习中的核心概念。
1.1、手撸版
1、准备训练数据
# 样本数
num_examples = 1000
# 特征数:房屋大小、新旧
num_input = 2
# 线性回归模型,2个W参数值:2,-3.4
true_w = [2, -3.4]
# 1个b参数值
true_b = 4.2
# 随机生成样本:特征值
features = torch.randn(num_examples, num_input, dtype=torch.float32)
# print(features[0])
# 随机生成样本:y值
labels = true_w[0] * features[:, 0] + true_w[1] * features[:, 1] + true_b
# print(labels[0])
# 随机生成一批数据,给y值再随机化下
_temp = torch.tensor(np.random.normal(0, 0.01, size=labels.size()), dtype=torch.float32)
labels = labels + _temp
# print(labels[0])
2、定义模型
# 模型参数 w:w1、w2,b:b,设了初始值
w = torch.tensor(np.random.normal(0, 0.01, (num_input, 1)), dtype=torch.float32)
b = torch.zeros(1, dtype=torch.float32)
# 设置要求计算梯度w.requires_grad_(requires_grad=True)
b.requires_grad_(requires_grad=True)
# 线性回归模型:定义 torch.mm(X, w) + b,mm是矩阵乘法(m:multiply缩写)
def linreg(X, w, b):
return torch.mm(X, w) + b
# 定义损失函数:y_pred=预测值、y=真实值,最简单的 平方误差
def squared_loss(y_pred, y):
return (y_pred - y.view(y_pred.size())) ** 2 / 2
# 定义梯度下降算法:sgd
# params是参数(data 是参数值、grad 是梯度值),lr是学习率,batch是数量
# 为什么要除batch?因为这个梯度值,是在batch个数据上累加算的
def sgd(params, lr, batch):
for param in params:
# 注意这里更改param时用的param.data
param.data -= lr * param.grad / batch
3、训练模型
辅助函数:by batch & 随机,读取样本数据
# 定义随机读取数据 by batch
def data_iter(batch, features, labels):
nums = len(features)
# 生成 0-1000 数组
indices = list(range(nums))
# 打乱下标的读取顺序
random.shuffle(indices)
# 生成:start=0,stop=1000,step=batch=10
for i in range(0, nums, batch):
# 截取一段下标,size=batch
t_ind = indices[i: min(i + batch, num_examples)] # 最后一次可能不足一个batch
# 转换为torch要求格式:tensor
j = torch.LongTensor(t_ind)
# 按照下标从向量中找
# yield:生成一个迭代器返回,有点类似闭包,每调用一次返回一次,且下次调用时会从上次暂停的位置继续
yield features.index_select(0, j), labels.index_select(0, j)
1.2、pytorch版
1、准备训练数据
同上
2、定义模型
3、训练模型
batch 读取数据
在用torch框架,进一步要理解:
a、nn.Module 和 forward() 函数:表示神经网络中的一个层,覆写 init 和 forward 方法(提问:实现2层网络怎么做?)
b、debug 观察下数据变化,和 手撸版 对比着去理解
二、手写Transformer
上面这是一个"逻辑"示意图。印象中,第1次看时,以为 “inputs & outputs” 是并行计算、但上面又有依赖,很糊涂。
这不是一个技术架构图,上图有很多概念,attention、multi-head等。先尝试转换为面向对象的类图,如下:
2.1、例子:翻译
# 定义词典
source_vocab = {'E': 0, '我': 1, '吃': 2, '肉': 3}
target_vocab = {'E': 0, 'I': 1, 'eat': 2, 'meat': 3, 'S': 4}
# 样本数据
encoder_input = torch.LongTensor([[1, 2, 3, 0]]).to(device) # 我 吃 肉 E, E代表结束词
decoder_input = torch.LongTensor([[4, 1, 2, 3]]).to(device) # S I eat meat, S代表开始词, 并右移一位,用于并行训练
target = torch.LongTensor([[1, 2, 3, 0]]).to(device) # I eat meat E, 翻译目标
2.2、定义模型
按照上面类图,我们来一点点实现
1、Attention
2、MultiHeadAttention
通用变量定义
d_model = 6 # embedding size
# d_model = 3 # embedding size
d_ff = 12 # feedforward nerual network dimension
d_k = d_v = 3 # dimension of k(same as q) and v
n_heads = 2 # number of heads in multihead attention# n_heads = 1
# number of heads in multihead attention【注:为debug更简单,可以先改为1个head】
p_drop = 0.1 # propability of dropout
device = "cpu"
注1:按惯性会想,会有多个head、串行循环计算,不是,多个head是一个张量输入
注2:FF 全连接、残差连接、归一化,35、38 行业代码,pytorch框架带来的简化
class MultiHeadAttention(nn.Module):
def __init__(self):
super(MultiHeadAttention, self).__init__()
self.n_heads = n_heads
self.W_Q = nn.Linear(d_model, d_k * n_heads, bias=False)
self.W_K = nn.Linear(d_model, d_k * n_heads, bias=False)
self.W_V = nn.Linear(d_model, d_v * n_heads, bias=False)
self.fc = nn.Linear(d_v * n_heads, d_model, bias=False) # ff 全连接
self.layer_norm = nn.LayerNorm(d_model) # normal 归一化
def forward(self, input_Q, input_K, input_V, attn_mask):
# input_Q:1*4*6,每批1句 * 每句4个词 * 每词6长度编码
# residual 先临时保存下:原始值,后面做残差连接加法
residual, batch = input_Q, input_Q.size(0)
# 乘上 W 矩阵。注:W 就是要训练的参数
# 注意:维度从2维变成3维,增加 head 维度,也是一次性并行计算
Q = self.W_Q(input_Q) # 乘以 W(6*6) 变为 1*4*6
Q = Q.view(batch, -1, n_heads, d_k).transpose(1, 2) # 切开为2个Head 变为 1*2*4*3 1批 2个Head 4词 3编码
K = self.W_K(input_K).view(batch, -1, n_heads, d_k).transpose(1, 2)
V = self.W_V(input_V).view(batch, -1, n_heads, d_v).transpose(1, 2)
# 1*2*4*4,2个Head的4*4,最后一列为true
# 因为最后一列是 E 结束符
attn_mask = attn_mask.unsqueeze(1).repeat(1, n_heads, 1, 1)
# 返回1*2*4*3,2个头,4*3为带上关注关系的4词
prob = ScaledDotProductAttention()(Q, K, V, attn_mask)
# 把2头重新拼接起来,变为 1*4*6
prob = prob.transpose(1, 2).contiguous()
prob = prob.view(batch, -1, n_heads * d_v).contiguous()
# 全连接层:对多头注意力的输出进行线性变换,从而更好地提取信息
output = self.fc(prob)
# 残差连接 & 归一化
res = self.layer_norm(residual + output) # return 1*4*6
return res
3、Encoder
在 attention 概念中,有很关键的 “遮盖” 概念,先不细究,你debug一遍会更理解
def get_attn_pad_mask(seq_q, seq_k): # 本质是结尾E做注意力遮盖,返回 1*4*4,最后一列为True
batch, len_q = seq_q.size() # 1, 4
batch, len_k = seq_k.size() # 1, 4
pad_attn_mask = seq_k.data.eq(0).unsqueeze(1) # 为0则为true,变为f,f,f,true,意思是把0这个结尾标志为true
return pad_attn_mask.expand(batch, len_q, len_k) # 扩展为1*4*4,最后一列为true,表示抹掉结尾对应的注意力
def get_attn_subsequent_mask(seq): # decoder的自我顺序注意力遮盖,右上三角形区为true的遮盖
attn_shape = [seq.size(0), seq.size(1), seq.size(1)]
subsequent_mask = np.triu(np.ones(attn_shape), k=1)
subsequent_mask = torch.from_numpy(subsequent_mask)
return subsequent_mask
4、Decoder
class Decoder(nn.Module):
def __init__(self):
super(Decoder, self).__init__()
self.target_embedding = nn.Embedding(len(target_vocab), d_model)
self.attention = MultiHeadAttention()
# 三入参形状分别为 1*4, 1*4, 1*4*6,前两者未被embedding,注意后面这个是 encoder_output
def forward(self, decoder_input, encoder_input, encoder_output):
# 编码为1*4*6
decoder_embedded = self.target_embedding(decoder_input)
# 1*4*4 全为false,表示没有结尾词
decoder_self_attn_mask = get_attn_pad_mask(decoder_input, decoder_input)
# 1*4*4 右上三角区为1,其余为0
decoder_subsequent_mask = get_attn_subsequent_mask(decoder_input)
# 1*4*4 右上三角区为true,其余为false
decoder_self_mask = torch.gt(decoder_self_attn_mask + decoder_subsequent_mask, 0)
# 1*4*6 带上注意力的4词矩阵【注:decoder里面,第1个attention】
decoder_output = self.attention(decoder_embedded, decoder_embedded, decoder_embedded, decoder_self_mask)
# 1*4*4 最后一列为true,表示E结尾词
decoder_encoder_attn_mask = get_attn_pad_mask(decoder_input, encoder_input)
# 输入均为 1*4*6,Q表示"S I eat meat"、K表示"我吃肉E"、V表示 "我吃肉E"
#【注:decoder里面,第2个attention】
decoder_output = self.attention(decoder_output, encoder_output, encoder_output, decoder_encoder_attn_mask)
return decoder_output
5、Transformer
class Transformer(nn.Module):
def __init__(self):
super(Transformer, self).__init__()
self.encoder = Encoder()
self.decoder = Decoder()
self.fc = nn.Linear(d_model, len(target_vocab), bias=False)
def forward(self, encoder_input, decoder_input):
# 入 1*4,出 1*4*6,作用:"我吃肉E",并带上三词间的关注力信息
encoder_output = self.encoder(encoder_input)
# 入 1*4, 1*4, 1*4*6=encoder_output
decoder_output = self.decoder(decoder_input, encoder_input, encoder_output) # 预测出4个词,每个词对应到词典中5个词的概率,如下
# tensor([[[ 0.0755, -0.2646, 0.1279, -0.3735, -0.2351],[-1.2789, 0.6237, -0.6452, 1.1632, 0.6479]]]
decoder_logits = self.fc(decoder_output)
res = decoder_logits.view(-1, decoder_logits.size(-1))
return res
2.3、训练模型
2.4、使用模型
# 预测目标是5个单词
target_len = len(target_vocab)
# 1*4*6 输入"我吃肉E",先算【自注意力】
encoder_output = model.encoder(encoder_input)
# 1*5 全是0,表示EEEEE
decoder_input = torch.zeros(1, target_len).type_as(encoder_input.data)
# 表示S开始字符
next_symbol = 4
# 5个单词逐个预测【注意:是一个个追加词,不断往后预测的】
for i in range(target_len):
# 譬如i=0第一轮,decoder输入为SEEEE,第二轮为S I EEE,把预测 I 给拼上去,继续循环
decoder_input[0][i] = next_symbol
# decoder 输出
decoder_output = model.decoder(decoder_input, encoder_input, encoder_output)
# 负责将解码器的输出映射到目标词汇表,每个元素表示对应目标词汇的分数
# 取出最大的五个词的下标,譬如[1, 3, 3, 3, 3] 表示 i,meat,meat,meat,meat
logits = model.fc(decoder_output).squeeze(0)
prob = logits.max(dim=1, keepdim=False)[1]
next_symbol = prob.data[i].item() # 只取当前i
for k, v in target_vocab.items():
if v == next_symbol:
print('第', i, '轮:', k)
break
if next_symbol == 0: # 遇到结尾了,那就完成翻译
break
如何学习大模型 AI ?
由于新岗位的生产效率,要优于被取代岗位的生产效率,所以实际上整个社会的生产效率是提升的。
但是具体到个人,只能说是:
“最先掌握AI的人,将会比较晚掌握AI的人有竞争优势”。
这句话,放在计算机、互联网、移动互联网的开局时期,都是一样的道理。
我在一线互联网企业工作十余年里,指导过不少同行后辈。帮助很多人得到了学习和成长。
我意识到有很多经验和知识值得分享给大家,也可以通过我们的能力和经验解答大家在人工智能学习中的很多困惑,所以在工作繁忙的情况下还是坚持各种整理和分享。但苦于知识传播途径有限,很多互联网行业朋友无法获得正确的资料得到学习提升,故此将并将重要的AI大模型资料包括AI大模型入门学习思维导图、精品AI大模型学习书籍手册、视频教程、实战学习等录播视频免费分享出来。
第一阶段(10天):初阶应用
该阶段让大家对大模型 AI有一个最前沿的认识,对大模型 AI 的理解超过 95% 的人,可以在相关讨论时发表高级、不跟风、又接地气的见解,别人只会和 AI 聊天,而你能调教 AI,并能用代码将大模型和业务衔接。
- 大模型 AI 能干什么?
- 大模型是怎样获得「智能」的?
- 用好 AI 的核心心法
- 大模型应用业务架构
- 大模型应用技术架构
- 代码示例:向 GPT-3.5 灌入新知识
- 提示工程的意义和核心思想
- Prompt 典型构成
- 指令调优方法论
- 思维链和思维树
- Prompt 攻击和防范
- …
第二阶段(30天):高阶应用
该阶段我们正式进入大模型 AI 进阶实战学习,学会构造私有知识库,扩展 AI 的能力。快速开发一个完整的基于 agent 对话机器人。掌握功能最强的大模型开发框架,抓住最新的技术进展,适合 Python 和 JavaScript 程序员。
- 为什么要做 RAG
- 搭建一个简单的 ChatPDF
- 检索的基础概念
- 什么是向量表示(Embeddings)
- 向量数据库与向量检索
- 基于向量检索的 RAG
- 搭建 RAG 系统的扩展知识
- 混合检索与 RAG-Fusion 简介
- 向量模型本地部署
- …
第三阶段(30天):模型训练
恭喜你,如果学到这里,你基本可以找到一份大模型 AI相关的工作,自己也能训练 GPT 了!通过微调,训练自己的垂直大模型,能独立训练开源多模态大模型,掌握更多技术方案。
到此为止,大概2个月的时间。你已经成为了一名“AI小子”。那么你还想往下探索吗?
- 为什么要做 RAG
- 什么是模型
- 什么是模型训练
- 求解器 & 损失函数简介
- 小实验2:手写一个简单的神经网络并训练它
- 什么是训练/预训练/微调/轻量化微调
- Transformer结构简介
- 轻量化微调
- 实验数据集的构建
- …
第四阶段(20天):商业闭环
对全球大模型从性能、吞吐量、成本等方面有一定的认知,可以在云端和本地等多种环境下部署大模型,找到适合自己的项目/创业方向,做一名被 AI 武装的产品经理。
- 硬件选型
- 带你了解全球大模型
- 使用国产大模型服务
- 搭建 OpenAI 代理
- 热身:基于阿里云 PAI 部署 Stable Diffusion
- 在本地计算机运行大模型
- 大模型的私有化部署
- 基于 vLLM 部署大模型
- 案例:如何优雅地在阿里云私有部署开源大模型
- 部署一套开源 LLM 项目
- 内容安全
- 互联网信息服务算法备案
- …
学习是一个过程,只要学习就会有挑战。天道酬勤,你越努力,就会成为越优秀的自己。
如果你能在15天内完成所有的任务,那你堪称天才。然而,如果你能完成 60-70% 的内容,你就已经开始具备成为一名大模型 AI 的正确特征了。