https://gitee.com/TomCoCo/transformer_test
Transformer的应用太多了:
本文主旨基于介绍Transformer,做一个简单的文本分类任务来说明
llm可视化模型
【官方双语】直观解释注意力机制,Transformer的核心 | 【深度学习第6章】 https://www.bilibili.com/video/BV1TZ421j7Ke)
“3”,“Wall St. Bears Claw Back Into the Black (Reuters)”,“Reuters - Short-sellers, Wall Street’s dwindling\band of ultra-cynics, are seeing green again.”
在7600个数据的验证集,总体acc@1>90%,即判断对分类的概率为90%. (参数量在2000w左右)
总体类似于下图,图像来自于https://bbycroft.net/llm,点击nano-gpt模型,本文主要结构和nano-gpt模型的形状一致
我们简单描述dataset,以及datasetloader的构建,说白了就是把数据封装为标签和文本的过程,以下是batch_size=2的示例
Label: Size([2])
tensor([2, 1])
Texes:tulpe: Size:2(batch_size=2)
(
‘Fosters counts cost of poor wine market Brewing giant Fosters today said that higher annual profits from sales of beer overseas had been eroded by a sharp drop in US wine trade.’,
‘Carter - joined the Jets. (Getty Images) Less than three weeks after being released by the Dallas Cowboys, quarterback Quincy Carter has landed with the New York Jets. Carter arrived in New York on Tuesday and signed a one-year contract.’
)
from torchtext import datasets
# 可以直接使用torchtext构建
train_data = datasets.AG_NEWS(root=train_data_path, split='train')
valid_data = datasets.AG_NEWS(root=train_data_path, split='test')
self.train_data_loader = DataLoader(dataset=train_data, batch_size=self.batch_size, shuffle=True)
self.valid_data_loader = DataLoader(dataset=valid_data, batch_size=self.batch_size, shuffle=True)
self.train_data_size = 120000
self.valid_data_size = 7600
首先,我们想处理任意的文本输入,需要告诉模型,你需要认识哪些词汇(即token),也就是给模型一份(词)字典,
分词的主要过程:
首先将一句话切分成若干的语义单元(token),例如下面这句话的分词方式
我/买了/一个/金色/的/黄铜/材质/的/东方明珠/塔/模型(分词方式1)
我/买了/一个/金色的/黄铜材质的/东方明珠塔/模型(分词方式2)
分词方式: 每一个词,有其本身的含义在,合适的分词是处理的第一步,
例如:方式2就会比方式1,构建出更厚的一本词表,但是相对的,每个词的含义更加明确不会混淆;
极端的,如果针对每一个字,单独切分,构建出更薄的一本词表也是可行的.就会从词典变成字典. 每个字的含义会更加混淆不清楚;
这样带来的影响,就是模型需要通过更多的上下文,去推断这个字的含义,因为单字的含义大多数时候是模糊不清的.
你看新华字典可比现代汉语词典要小的多了
词表大小: 再看这句话,其中包含了英文和中文,如果目标是兼容2种文字,则词表会建立的更大,相当于建立了一本<<现代汉语和英语词典>>
熟悉/markdown/语法/可以/让/文章/排版/更/便捷/美观
确定你的需要处理的文字的范围,以确定词表大小,把所有的词(token)都从0-length编个有序列表,这样一个空的词表就建好了. 相当于建立了一本词典,但是目前只有目录,所有词条的含义尚未定义.这些词条定义,需要在学习中逐渐定义出来
分词器: 中文分词器jieba,英文分词器BertTokenizerFast.本文针对AG_news处理,故而使用BertTokenizerFast
特殊词处理:我构建词表时,最简单的方式就是将训练集的所有句子,统一执行分词,然后去重,就很容易得到一个训练集上出现的所有单词列表,这个是没有问题的. 但是预测推理的过程中,无法保证新的词,一定在词表中出现. 就如现在的很多网络新词,你查<<现代汉语词典>>也是查不到的,所幸,如果训练集足够大,那么词表足够大,缺少的词也不会很多,故而影响不是很大.
为处理AG_NEWS, 由于是全英文的,此处我们不去手动构建词表,使用BertTokenizerFast提供的词表,词表长度:30522; 使用以下方法可以将文本数组,使用 分词->映射->填充->转换格式 的处理流程处理为了tensor格式的下标.
转换示例:
tulpe: Size:2(batch_size=2)
(
‘Fosters counts cost of poor wine market Brewing giant Fosters today said that higher annual profits from sales of beer overseas had been eroded by a sharp drop in US wine trade.’,
‘Carter - joined the Jets. (Getty Images) Less than three weeks after being released by the Dallas Cowboys, quarterback Quincy Carter has landed with the New York Jets. Carter arrived in New York on Tuesday and signed a one-year contract.’
)
->Tensor:Size([2, 512])
tensor([[ 101, 6469, 2015, …, 0, 0, 0],
[ 101, 5708, 1011, …, 0, 0, 0]])
转换代码:
from transformers import BertTokenizerFast
# 分词器和词表,注意这里的词表,仅仅使用了一个分词功能和id映射功能,没有使用到词嵌入的映射哦
tokenizer = BertTokenizerFast.from_pretrained('bert-base-uncased')
# 分词,max_length是上下文长度
encoded_batch = tokenizer.batch_encode_plus(
texts,
max_length=512,
truncation=True,
padding='max_length',
return_tensors='pt'
)
# 转化为了tensor格式的下标
input_ids = encoded_batch['input_ids'].to(self.device)
上下文长度:
vocab_size = self.tokenizer.vocab_size
# 我这里设计深度为300,参数选择可以见附录参数参考
embedding_feature_dim = 300
self.embedding = nn.Embedding(vocab_size, embedding_feature_dim)
->Tensor:Size([2, 512])
tensor([[ 101, 6469, 2015, …, 0, 0, 0],
[ 101, 5708, 1011, …, 0, 0, 0]])->Tensor:Size([2, 512, 300])
tensor([[[-0.0749, -1.5324, 0.0588, …, -1.8509, 0.4253, -1.5777],
[ 0.5036, 1.1159, -1.0334, …, 0.7873, -1.9076, -0.5892],
[ 1.6551, -1.7568, 1.0481, …, -1.8378, -1.4752, -1.7096],
…,
[-1.0518, 0.9318, -0.3377, …, 0.6822, -1.4751, 0.9430],
[-1.0518, 0.9318, -0.3377, …, 0.6822, -1.4751, 0.9430],
[-1.0518, 0.9318, -0.3377, …, 0.6822, -1.4751, 0.9430]],
[[-0.0749, -1.5324, 0.0588, …, -1.8509, 0.4253, -1.5777],
[ 0.7489, -0.8386, 0.9028, …, 3.1699, 0.8188, -0.2222],
[-0.0861, 0.1495, -0.1039, …, -0.1902, -2.5695, 0.0744],
…,
[-1.0518, 0.9318, -0.3377, …, 0.6822, -1.4751, 0.9430],
[-1.0518, 0.9318, -0.3377, …, 0.6822, -1.4751, 0.9430],
[-1.0518, 0.9318, -0.3377, …, 0.6822, -1.4751, 0.9430]]],
grad_fn=)
class PositionalEncoding(nn.Module):
"""
位置编码器
"""
def __init__(self, d_model, max_len=512, device=None):
super(PositionalEncoding, self).__init__()
self.encoding = torch.zeros(max_len, d_model)
self.register_buffer('positional_encoding', self.encoding)
position = torch.arange(0, max_len).unsqueeze(1).float()
div_term = torch.exp(torch.arange(0, d_model, 2).float() * -(math.log(10000.0) / d_model))
self.encoding[:, 0::2] = torch.sin(position * div_term)
self.encoding[:, 1::2] = torch.cos(position * div_term)
self.encoding = self.encoding.unsqueeze(0) # Add batch dimension
self.encoding = self.encoding.to(device)
def forward(self, x):
return x + self.encoding[:, :x.size(1)]
depth
)(transformer): Transformer(
(layers): ModuleList(
(0-5): 6 x ModuleList(
(0): AttentionMulti(
(w_qkv): Linear(in_features=300, out_features=3072, bias=True)
(attention_to_embedding): Linear(in_features=1024, out_features=300, bias=True)
(dropout): Dropout(p=0.0, inplace=False)
)
(1): Sequential(
(0): LayerNorm((300,), eps=1e-05, elementwise_affine=True)
(1): Linear(in_features=300, out_features=1200, bias=True)
(2): GELU(approximate=‘none’)
(3): Dropout(p=0.1, inplace=False)
(4): Linear(in_features=1200, out_features=300, bias=True)
(5): Dropout(p=0.1, inplace=False)
)
)
)
(0): AttentionMulti(
(w_qkv): Linear(in_features=300, out_features=3072, bias=True)
(attention_to_embedding): Linear(in_features=1024, out_features=300, bias=True)
(dropout): Dropout(p=0.0, inplace=False)
)
(1): Sequential(
(0): LayerNorm((300,), eps=1e-05, elementwise_affine=True)
(1): Linear(in_features=300, out_features=1200, bias=True)
(2): GELU(approximate=‘none’)
(3): Dropout(p=0.1, inplace=False)
(4): Linear(in_features=1200, out_features=300, bias=True)
(5): Dropout(p=0.1, inplace=False)
)
先看AttentionMulti,结构貌似非常简单,忽略掉dropout,就是2个全连接层而已,观察2个全连接层
wq = nn.Linear(300, 1024)
wk = nn.Linear(300, 1024)
wv = nn.Linear(300, 1024)
好的,我们先不看这个多层多头的Transformer模块,我们先去理解,这个模块想要做什么
核心任务改变词嵌入张量,以适应上下文的语义. 核心在改变一词,我们需要获取到这个变化量dt,可以看出,变化量的维度必须和词嵌入张量维度一致,不然无法叠加上去
这个模块的功能就是从上下文中提取到合适的语义,然后计算这个变化量
如何计算呢,首先我们需要衡量,一句话中,那一个词会对当前词产生影响,影响因子(QK)有多少,对于塔这个词来说,影响可能是这样的
比如: 我/买了/一个/黄金的/埃菲尔/塔.
对于词 “塔” 来说
上下文中: 一个,黄金的,埃菲尔 都对塔产生了明显语义影响,且影响很大,而: 我,买了 则影响相对较小.
影响源 | 影响目标 | attention |
---|---|---|
我 | 塔 | 0.09 |
买了 | 塔 | 0.01 |
一个 | 塔 | 0.2 |
黄金的 | 塔 | 0.2 |
埃菲尔 | 塔 | 0.5 |
塔 | 塔 | 0 |
上表中的是单个对"塔"单个的影响,而且是归一化的概率值,下面是一句话中的所有词构成的注意力矩阵,logits值
其次,我们需要知道,每一个词对目标词的**影响量(V)**是多少,例如""黄金的"对"塔"的影响,就应该使之具有一种金灿灿,值钱,黄色等等语义的赋予.注意这个影响量也是具有维度的,维度越高,那么可以赋予的语义空间就越大.
所有的词的**影响量(V)**x,影响因子(QK),对塔有影响的数据.
原始数据切分为token,token选择对应的词向量表示(从嵌入矩阵中),此时的向量只编码了基础单词含义,是没有上下文影响存在的
上下文长度,每一个词能够看到的上下文对他的影响距离
上下文长度的token,输入到模型中,和解嵌入矩阵相乘,得到预测单词分布
输入6,深度128,即输入6x128记为X
X流经 注意力模块 还是X ,但是被编码了上下文信息,即数据改变了,即一句话里面的每一个词,含有了其上下文的语义,而不是一个单独的词存在
埃菲尔铁塔模型 -> 拆分为 埃菲尔/铁塔/模型 铁塔(初始值), 埃菲尔对其影响, 模型对其影响
生成Q的矩阵, ,词向量为128x1时,为了生成Q,则
生成Q,即有哪些特征会影响到本词
生成K,即本词具有哪些特征
QK的乘积,即本词和哪些其他的词匹配上了,匹配度是多少,即本词需要的特征与其他词的提供特征对应上了.
比如: 我/买了/一个/黄金的/埃菲尔/塔
上下文中明显: 一个,黄金的,埃菲尔 都对塔产生了语义影响,且影响很大,而: 我,买了 则影响相对较小.
换一个角度来说,
对于词: 塔 来说,他的Q(128维度),第一维是期待获取形状,第二维度材质,第三维度颜色,第四维度数量,第五维度位置等等等等
对于词:黄金的 来说,他K(128维度)第一维描述为重量,第二维度描述为颜色,第三维描述为形容词等等等等(注意,这里是特征名,不是特征值)
对于词:黄金的 来说,他V(128维度),则是他的K的对应的每一个维度的特征值,例如第一维是:很重,第二维是:金色,第三维:是
执行点积,即执行两者的相似度度量,如果相似,则匹配较好,故而这些词对他的影响会更大,
注意力矩阵大小等于上下文长度的平方
具体的计算方案,
构建 W Q K V W_{QKV} WQKV三个全连接层,输入参数即词嵌入矩, 用以计算 Q , K , V Q,K,V Q,K,V
W Q W_Q WQ矩阵 :生成Q矩阵 :词嵌入特征维度->注意力特征维度
W K W_K WK矩阵 :生成K矩阵 :词嵌入特征维度->注意力特征维度
W V W_V WV矩阵 :生成V矩阵 :词嵌入特征维度->注意力特征维度
计算注意力矩阵
A t t e n t i o n ( Q , K ) = s o f t m a x ( Q K T d k ) Attention(Q,K)=softmax(\frac{QK^T} {\sqrt{d_k}}) Attention(Q,K)=softmax(dkQKT)
其中 d k d_k dk为注意力特征维度(此例为128)
除以 d k \sqrt{d_k} dk 可以使点积的结果保持在一个较为稳定的范围内,确保 softmax 函数的输出不至于变得过于极端。
注意力矩阵乘以V
A t t e n t i o n ( Q , K , V ) = s o f t m a x ( Q K T d k ) V Attention(Q,K,V)=softmax(\frac{QK^T} {\sqrt{d_k}})V Attention(Q,K,V)=softmax(dkQKT)V
得出的值即应该添加到原数据上的变化量
注意,此时的维度不一致,需要将变化量结果从: 注意力特征维度->词嵌入特征维度
wq = nn.Linear(300, 128)
wk = nn.Linear(300, 128)
wv = nn.Linear(300, 128)
attention_to_embedding = nn.Linear(128, 300)
......省略若干
# 使用WK,WQ 300->128 , 将嵌入矩阵6x512x300分别全连接为6x512x128,
# 512来自于上下文长度,128来自于kq特征维度(attn_feature_dim).
# 计算KQV结果: 6x512x128 ,
q_tensor = wq(token_tensor)
k_tensor = wk(token_tensor)
v_tensor = wv(token_tensor)
# 点积qk(注意需要转置矩阵),得注意力矩阵(attention matrix),6x512x512. 即计算kq的128维度相关性
# 点积的结果值,和attn_feature_dim相关,维度越高,则点积的结果越大
attention_tensor = torch.matmul(q_tensor, k_tensor.transpose(-1, -2))
# 为平衡点积结果,故而除以根号下attn_feature_dim 6x512x512->6x512x512
attention_tensor = attention_tensor / torch.sqrt(torch.tensor(128, dtype=torch.float32))
# 对结果做softmax得到概率值(6x512x512)
attention_tensor = torch.softmax(attention_tensor, dim=-1)
# 做叉乘,得6x512x128,即在这个注意力网络中,对每一个的词向量的改变值.
v_output = torch.matmul(attention_tensor, v_tensor)
# 注意这个变化量形状(6x512x128)和词向量形状(6x512x300)不一致. 再做一个线性变换,从注意力特征维度转为300词嵌入维度
attention_delta = attention_to_embedding(v_output)
# 将变化量添加到原数据上
token_tensor.add(attention_delta)
以上是单头单层注意力模块的内容,数据的变化如下
6x512x300 + (6x512x300 -> 6x512x128 -> 6x512x300)
MLP看上去貌似更简单,实际也是更简单,其实就是2个全连接顺序执行,先升维度,激活函数,再降维
Sequential(
(0): LayerNorm((300,), eps=1e-05, elementwise_affine=True)
(1): Linear(in_features=300, out_features=1200, bias=True)
(2): GELU(approximate=‘none’)
(3): Dropout(p=0.1, inplace=False)
(4): Linear(in_features=1200, out_features=300, bias=True)
(5): Dropout(p=0.1, inplace=False)
)
首先目的:
1. 经过若干次的transformer模块,句子的合适的语义信息,已经被捕捉到了,保存在6x512x300的tensor中,我们处理的是一个4分类任务,即我们需要一个6x4的可能性tensor
2. 执行分类操作,直接执行 300->4的分类是缺少意义的,是针对一句话的每一个词(512)分别求分类,需要在第1维度作平均,平均一句话的内容,即6x512x300->6x4
3. 这样再执行一次全连接 300->4即可
# 执行分类操作,直接执行 300->4的分类是缺少意义的,是针对一句话的每一个词(512)分别求分类,需要在第1维度作平均,平均一句话的内容
x = x.mean(dim=1)
# 全连接执行分类操作,得分类的logtis值
return self.fc(x)
import math
import time
from datetime import datetime
import torch
from functorch.einops import rearrange
from torch import nn, optim
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter
from torchtext import datasets
from transformers import BertTokenizerFast
class PositionalEncoding(nn.Module):
"""
位置编码器
"""
def __init__(self, d_model, max_len=512, device=None):
super(PositionalEncoding, self).__init__()
self.encoding = torch.zeros(max_len, d_model)
self.register_buffer('positional_encoding', self.encoding)
position = torch.arange(0, max_len).unsqueeze(1).float()
div_term = torch.exp(torch.arange(0, d_model, 2).float() * -(math.log(10000.0) / d_model))
self.encoding[:, 0::2] = torch.sin(position * div_term)
self.encoding[:, 1::2] = torch.cos(position * div_term)
self.encoding = self.encoding.unsqueeze(0) # Add batch dimension
self.encoding = self.encoding.to(device)
def forward(self, x):
return x + self.encoding[:, :x.size(1)]
class AttentionMulti(nn.Module):
def __init__(self, embedding_feature_dim, attention_feature_dim, heads, dropout=0.):
"""
:param embedding_feature_dim: 词嵌入特征维度: 例300
:param attention_feature_dim: 注意力特征维度(每一个头),即dk 例:128
:param heads: 多头注意力的头数 例:8
"""
super(AttentionMulti, self).__init__()
# qkv合一,形状一致,故而深度为attention_feature_dim * heads * 3
self.w_qkv = nn.Linear(embedding_feature_dim, attention_feature_dim * heads * 3)
self.heads = heads
self.dk = attention_feature_dim / heads
self.attention_to_embedding = nn.Linear(attention_feature_dim * heads, embedding_feature_dim)
self.dropout = nn.Dropout(dropout)
def forward(self, x, mask):
# 6x512x300 -> 6x512x(128x8x3)
qkv = self.w_qkv(x)
# 沿着qkv切开 3(tuple)x6x512x(128x8)
qkv = qkv.chunk(3, dim=-1)
# 分离出heads提到前面来 3(tuple)x6x8x512x128
# b (batch_size); h (heads)头数 ;n (sequence length)序列长度(上下文长度);d (dimension) 注意力特征维度
q, k, v = map(lambda t: rearrange(t, 'b n (h d) -> b h n d', h=self.heads), qkv)
# 6x8x512x128 叉乘 6x8x512x128 ->6x8x512x512,得128维组合的相关性
attention_tensor = torch.matmul(q, k.transpose(-1, -2))
attention_tensor = attention_tensor / torch.sqrt(torch.tensor(self.dk, dtype=torch.float32))
# 在计算注意力权重时,将填充部分的权重设置为负无穷大,以使 softmax 计算后变为零。从而使填充不会影响到其他数据
if mask is not None:
# 使用填充掩码调整注意力权重
mask = mask.unsqueeze(1).unsqueeze(2) # Shape: [b, 1, 1, n]
attention_tensor = attention_tensor.masked_fill(mask == 0, float('-inf'))
# 对结果做softmax得到概率值(6x8x512x512)
attention_tensor = attention_tensor.softmax(dim=-1)
attention_tensor = self.dropout(attention_tensor)
# 做叉乘,得6x8x512x128,即在这个注意力网络中,对每一个的词向量的改变值.
out = torch.matmul(attention_tensor, v)
# 将多头获取到的变化合并到最后的改变特征值维度,6x8x512x128 -> 6x512x(128x8)
# 即每个头都为原始的词嵌入提供了128维度的变化量,总计8个头,故而提供128x8,所以合并这些变化量
out = rearrange(out, 'b h n d -> b n (h d)', h=self.heads)
out = self.attention_to_embedding(out)
out = self.dropout(out)
# 输出尺寸和输入一致 6x512x300
return out
class Transformer(nn.Module):
def __init__(self, depth, embedding_feature_dim, attention_feature_dim, heads, mlp_feature_dim, dropout=0.):
"""
:param depth: 模型深度:例如3
:param embedding_feature_dim: 词嵌入特征维度: 例300
:param attention_feature_dim: 注意力特征维度(每一个头),即dk 例:128
:param heads: 多头注意力的头数 例:8
:param mlp_feature_dim: mlp的特征维度:例1200
"""
super(Transformer, self).__init__()
self.layers = nn.ModuleList([])
for _ in range(depth):
# 多头注意力
attention = AttentionMulti(embedding_feature_dim, attention_feature_dim, heads)
# 每个MLP一个ln,2个全连接,一个激活函数构成
mlp = nn.Sequential(
nn.LayerNorm(embedding_feature_dim),
nn.Linear(embedding_feature_dim, mlp_feature_dim),
nn.GELU(),
nn.Dropout(dropout),
nn.Linear(mlp_feature_dim, embedding_feature_dim),
nn.Dropout(dropout),
)
self.layers.append(nn.ModuleList([attention, mlp]))
# 深度次数后的的Transformer,加上一个层归一化
self.norm = nn.LayerNorm(embedding_feature_dim)
def forward(self, x, mask):
for attention, mlp in self.layers:
x = x + attention(x, mask)
x = x + mlp(x)
return self.norm(x)
class TextClassifyTransformer(nn.Module):
def __init__(self, class_num, vocab_size, sequence_length, depth, embedding_feature_dim, attention_feature_dim,
heads, mlp_feature_dim, dropout=0., emb_dropout=0., device=None):
"""
:param class_num: 分类数量
:param vocab_size: 词表大小,
:param sequence_length: 序列长度(上下文长度)
:param depth: Transformer模型深度:例如3,即自注意力模块和mlp重复多少次
:param embedding_feature_dim: 词嵌入特征维度: 例300
:param attention_feature_dim: 注意力特征维度(每一个头),即dk 例:128
:param heads: 多头注意力的头数 例:8
:param mlp_feature_dim: mlp的特征维度:例1200
:param dropout: 丢弃率.这里一共会使用4处,1.注意力矩阵计算完成,对注意力矩阵执行. 2.从注意力转为词向量的全连接层之后 3.mlp网络第一次全连接,激活后,4.mlp第二次全连接后
:param emb_dropout: 对抗过拟合,此处为编码完成之后的dropout,即对词嵌入后执行的
"""
super(TextClassifyTransformer, self).__init__()
# 建立词嵌入层,词表长度,嵌入维度
self.embedding = nn.Embedding(vocab_size, embedding_feature_dim)
# 归一化
self.ln1 = nn.LayerNorm(embedding_feature_dim)
# 位置编码器
self.pos_encoder = PositionalEncoding(d_model=embedding_feature_dim, max_len=sequence_length, device=device)
self.emb_dropout = nn.Dropout(emb_dropout)
# Transformer模块
self.transformer = Transformer(depth, embedding_feature_dim, attention_feature_dim, heads, mlp_feature_dim,
dropout)
self.fc = nn.Linear(embedding_feature_dim, class_num)
def forward(self, x, mask):
# 执行词嵌入,将输入input转对应词向量 6x512x300
x = self.embedding(x)
# 嵌入位置 6x512x300
x = self.pos_encoder(x)
x = self.emb_dropout(x)
# 执行layerNorm,即 6x512x300 的300维度执行归一化,即对一句话的每一个词向量执行归一化,2组权重参数β和γ
x = self.ln1(x)
# 执行Transformer模块,得 6x512x300
x = self.transformer(x, mask)
# 执行分类操作,直接执行 300->4的分类是缺少意义的,是针对一句话的每一个词(512)分别求分类,需要在第1维度作平均,平均一句话的内容
x = x.mean(dim=1)
# 全连接执行分类操作,得分类的logtis值
return self.fc(x)
class TextClassify:
def __init__(self, train_data_path=None, workers=8,
batch_size=128, epochs_num=2, lr=1e-5, target_path="./target"):
"""
:param train_data_path: 数据集的路径
:param workers: 加载器工作线程数
:param batch_size:
:param epochs_num:
:param lr:
:param target_path: 权重文件保存位置
"""
# 定义设备
self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print(f"use divice:{self.device}")
# tensorboard 记录器
self.writer = SummaryWriter(log_dir='./log/' + time.strftime('%m-%d_%H.%M', time.localtime()))
# 定义目标目录
self.target_path = target_path
# 设定超参数:minibatch大小,迭代次数,学习率,正则惩罚
self.batch_size = batch_size
self.epochs_num = epochs_num
self.lr = lr
self.workers = workers
# 分词器和词表,注意这里的词表,仅仅使用了一个分词功能和id映射功能,没有使用到词嵌入的映射哦
self.tokenizer = BertTokenizerFast.from_pretrained('bert-base-uncased')
# 词表长度:30522
print(f"The vocabulary size is: {self.tokenizer.vocab_size}")
# 构造模型
self.model = TextClassifyTransformer(4, self.tokenizer.vocab_size, sequence_length=512, depth=6,
embedding_feature_dim=300, attention_feature_dim=128, heads=8,
mlp_feature_dim=1200, dropout=0.1, emb_dropout=0.1, device=self.device)
self.model.to(self.device)
# 损失函数,优化器
self.criterion = nn.CrossEntropyLoss()
self.optimizer = optim.AdamW(self.model.parameters(), lr=self.lr, weight_decay=1e-2)
self.lr_scheduler = optim.lr_scheduler.StepLR(self.optimizer, step_size=1, gamma=0.7)
# 数据集,预处理方式
self.train_data_loader = None
self.valid_data_loader = None
self.train_data_size = None
self.valid_data_size = None
# 临时变量,用以计数,训练和验证执行的batch的index,在一次训练中始终从0开始,一直递增
self.valid_batch_index = 0
self.train_batch_index = 0
# 加载数据
self.__load_data(train_data_path)
def __load_data(self, train_data_path):
train_data = datasets.AG_NEWS(root=train_data_path, split='train')
valid_data = datasets.AG_NEWS(root=train_data_path, split='test')
self.train_data_loader = DataLoader(dataset=train_data, batch_size=self.batch_size, shuffle=True)
self.valid_data_loader = DataLoader(dataset=valid_data, batch_size=self.batch_size, shuffle=True)
# self.train_data_size = len(train_data)
# self.valid_data_size = len(valid_data)
self.train_data_size = 120000
self.valid_data_size = 7600
def train_model(self):
print("start train model...")
print(f"hyper-parameters: batch_size:{self.batch_size}; "
f"epochs_num:{self.epochs_num}; lr:{self.lr};")
# 批次最佳
best_epoch_acc_rate_valid = 0
# 重置批次计数
self.train_batch_index = 0
self.valid_batch_index = 0
for epoch in range(self.epochs_num):
epoch_loss_train, epoch_acc_rate_train = self.__do_train(epoch)
# 更新优化器
self.lr_scheduler.step()
# 执行验证
epoch_loss_valid, epoch_acc_rate_valid = self.__do_valid(epoch)
# 记录每个epoch的优化情况
self.writer.add_scalars("epoch_loss", {"train": epoch_loss_train}, epoch)
self.writer.add_scalars("epoch_loss", {"valid": epoch_loss_valid}, epoch)
self.writer.add_scalars("epoch_acc", {"train": epoch_acc_rate_train}, epoch)
self.writer.add_scalars("epoch_acc", {"valid": epoch_acc_rate_valid}, epoch)
print(f"epoch {epoch}/{self.epochs_num - 1} : "
f"epoch_loss_train:{epoch_loss_train:.4f}; epoch_acc_rate_train:{epoch_acc_rate_train:.4f}; "
f"epoch_loss_valid:{epoch_loss_valid:.4f}; epoch_acc_rate_valid:{epoch_acc_rate_valid:.4f}; ")
# 更新最优参数
if epoch_acc_rate_valid >= best_epoch_acc_rate_valid:
best_epoch_acc_rate_valid = epoch_acc_rate_valid
torch.save(self.model.state_dict(),
f"{self.target_path}/state_dict_{datetime.now().strftime('%Y%m%d%H%M%S')}"
f"_{epoch_acc_rate_train:.4f}_{best_epoch_acc_rate_valid:.4f}.pth")
def __do_train(self, epoch):
"""
训练数据
:param epoch:
:return:
"""
# 进入训练模式,计算梯度
self.model.train()
epoch_loss_sum = 0
epoch_acc_sum = 0
# minibatch计数
ix = 0
for label, texts in self.train_data_loader:
label = label - 1
label = label.to(self.device)
# 分词,找词表对应下标,还有填充,这里一个方法一起做了
encoded_batch = self.tokenizer.batch_encode_plus(
texts,
max_length=512,
truncation=True,
padding='max_length',
return_tensors='pt'
)
# token的id,6x512
input_ids = encoded_batch['input_ids'].to(self.device)
attention_mask = encoded_batch['attention_mask'].to(self.device)
with (torch.set_grad_enabled(True)):
# 传入填充过的mask,计算分类
output = self.model(input_ids, attention_mask)
loss = self.criterion(output, label)
_, prediction = torch.max(output, 1)
# 执行反向传播
loss.backward()
self.optimizer.step()
self.optimizer.zero_grad()
# 计算当前小批量的正确率
batch_acc = prediction.eq(label).sum()
# 计算批次准确率
# 当前批量大小,由于数据集可能都不足一个batch_size,所以要获取到准确的batch_size
now_batch_size = label.shape[0]
# 将损失乘上size是为了在计算平均损失时,考虑到每个样本对损失的贡献。
epoch_loss_sum += (loss * now_batch_size)
epoch_acc_sum += batch_acc
# 记录事件
self.writer.add_scalars("batch_train", {"loss": loss}, self.train_batch_index)
self.writer.add_scalars("batch_train", {"acc": batch_acc / now_batch_size}, self.train_batch_index)
print('[%d/%d][%d/%d]\t%s\t loss: %.4f\t acc_rate: %.4f'
% (epoch, self.epochs_num, ix, self.train_data_size, datetime.now(), loss.item(), batch_acc / now_batch_size))
self.train_batch_index += 1
ix += self.batch_size
epoch_loss = epoch_loss_sum / self.train_data_size
epoch_acc_rate = epoch_acc_sum / self.train_data_size
return epoch_loss, epoch_acc_rate
def __do_valid(self, epoch):
"""
验证过程
:param epoch:
:return:
"""
# 进入推理模式,不计算梯度
self.model.eval()
epoch_loss_sum = 0
epoch_acc_sum = 0
ix = 0
for label, texts in self.valid_data_loader:
label = label - 1
label = label.to(self.device)
# 分词,找词表对应下标,还有填充,这里一个方法一起做了
encoded_batch = self.tokenizer.batch_encode_plus(
texts,
max_length=512,
truncation=True,
padding='max_length',
return_tensors='pt'
)
# token的id,6x512
input_ids = encoded_batch['input_ids'].to(self.device)
attention_mask = encoded_batch['attention_mask'].to(self.device)
with (torch.set_grad_enabled(False)):
# 传入填充过的mask,计算分类
output = self.model(input_ids, attention_mask)
loss = self.criterion(output, label)
_, prediction = torch.max(output, 1)
# 计算当前小批量的正确率
batch_acc = prediction.eq(label).sum()
# 计算批次准确率
# 当前批量大小,由于数据集可能都不足一个batch_size,所以要获取到准确的batch_size
now_batch_size = label.shape[0]
# 将损失乘上size是为了在计算平均损失时,考虑到每个样本对损失的贡献。
epoch_loss_sum += (loss * now_batch_size)
epoch_acc_sum += batch_acc
# 记录事件
self.writer.add_scalars("batch_valid", {"loss": loss}, self.valid_batch_index)
self.writer.add_scalars("batch_valid", {"acc": batch_acc / now_batch_size}, self.valid_batch_index)
print('[%d/%d][%d/%d]\t%s\t loss: %.4f'
% (epoch, self.epochs_num, ix, self.valid_data_size, datetime.now(), loss.item()))
self.valid_batch_index += self.batch_size
ix += 1
epoch_loss = epoch_loss_sum / self.valid_data_size
epoch_acc_rate = epoch_acc_sum / self.valid_data_size
return epoch_loss, epoch_acc_rate
"""
多头示例
"""
if __name__ == '__main__':
cc = TextClassify(train_data_path="./resources", batch_size=2, epochs_num=2, lr=1e-4, target_path="./target")
cc.train_model()
指明padding的index是什么,会加速收敛速度. 不指明模型其实也可以习得,
对于构建 Transformer 模型的关键参数选择,以下是建议的设定:
embedding_feature_dim
)heads
)depth
)attention_feature_dim
)mlp_feature_dim
)2 * embedding_feature_dim
到 4 * embedding_feature_dim
2 * embedding_feature_dim
: 适合资源有限或模型简化的场景。4 * embedding_feature_dim
: 提供更高的模型表达能力,适合复杂任务。embedding_feature_dim = 300
heads = 8
depth = 6
attention_feature_dim = 128
mlp_feature_dim = 600
(或 900
)这些推荐值可以根据具体任务、硬件资源和期望的模型性能进行调整。初始选择后,可以通过实验和调优进一步优化。