社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  机器学习算法

深度学习连载11:站在巨人的肩膀上 - 预训练模型之全量微调

Y叔 余光创 • 4 周前 • 115 次点击  

从头训练大模型,需要海量的数据。但是很多通用的规律其实已经被别人用大模型学好了。所以我们只需要借用一个已经会看世界的模型的眼睛就可以了。

别人做的预训练就是在大数据上学了通用特征,也就是权重,而我们要做的,就是用小数据来学任务特化,在别人的权重上继续训练。这是有现实意义的,比如在生信领域,有标注的基因组数据是稀少的。而无标注的基因组序列是非常多的。

这里用一个预训练模型Nucleotide Transformer v2 (NT v2, 50M, multi-species),用的是ESM架构。这是一个DNA的语言模型,它把DNA序列当成一种语言,每个token是一个6-mer。

配置项
值
含义
词表大小 vocab_size
4107
4⁶=4096 种 6-mer + 6 个特殊 token(pad/mask/cls 等)+ 5个单碱基(ATCGN)
层数 num_hidden_layers
12
12 层 Transformer encoder
隐藏维度 hidden_size
512
每个 token 的表征向量长度
注意力头 num_attention_heads
16
16 头多头注意力
FFN intermediate_size
2048
前馈层宽度(512×4)
位置编码
rotary(RoPE)
旋转位置编码,而非经典 BERT 的绝对位置编码
最大长度
2050
最长支持 2050 个 token
50M 参数是这12层的总和。


它做的训练就是喂了海量多物种基因组,训练任务是:随机遮住一些6-mer token,让模型根据上下文把它猜出来。模型为了猜对,就被迫学会了DNA的“语法”:motif、GC含量等。这就是它会看基因组的原因。

但它的“头”是预测下一个token,那么这里我们要做就是扔掉这个语言模型头,换成我们要做的任务头。这里我们还是做回 《识别DNA启动子》的任务。那么就要把头换成随机初始化的2分类头,同时保留12层骨干的预训练权重。所以“微调”就是借用它已经学好的DNA通用特征,并教会它这个二分类任务。

加载必要的库

  • 1
  • 2
  • 3
  • 4
import randomimport torchfrom torch.utils.data import DataLoader, TensorDatasetfrom transformers import AutoTokenizer, AutoModelForSequenceClassification

数据

这个和前面的《用CNN识别DNA启动子》是一样的:

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
def generate_data(n=2000, length=100, motif="TATAAA"):    random.seed(42)    pos, neg = [], []    bases = "ACGT"    for _ in range(n // 2):        seq = "".join(random.choice(bases) for _ in range(length))        i = random.randint(0, length - len(motif))        pos.append(seq[:i] + motif + seq[i + len(motif):])        seq = "".join(random.choice(bases) for _ in range(length))        while motif in seq:   seq="".join(random.choice(bases)for_inrange(length))        neg.append(seq)    seqs = pos + neg    y = torch.tensor([1]*len(pos) + [0]*len(neg), dtype=torch.long)    perm = torch.randperm(len(y))    return [seqs[i] for i in perm], y[perm]

tokenizer

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
def load_tokenizer():    tokenizer = AutoTokenizer.from_pretrained(        "InstaDeepAI/nucleotide-transformer-v2-50M-multi-species", trust_remote_code=True)    return tokenizer    


    
def tokenize(seqs, tokenizer):    enc = tokenizer(seqs, padding=True, truncation=True,   max_length=128,return_tensors="pt")    return enc["input_ids"], enc["attention_mask"]

首先是加载一个预训练的模型,首次加载会自动去下载。AutoTokenizer我们可以认为是一个泛型类,它会根据加载的模型,去加载相应的类,这里的类是EsmTokenizer。

那么我们可以试一下:

  • 1
  • 2
tokenizer = load_tokenizer()tokenize(['TATAAACGTT', 'ACGTACGTACGTACGT'], tokenizer)

它的输出是:

  • 1
  • 2
  • 3
  • 4
(tensor([[   3, 1094, 4104, 4105, 4103, 4103,    1],   [3,728,3379,4102,4104,4105, 4103]]), tensor([[1, 1, 1, 1, 1, 1, 0],         [1, 1, 1, 1, 1, 1, 1]]))

第一个输出就是把序列变成数字ID,按照词表来转换。这个词表是预训练时定死的。我们必须要用它的tokenizer,不然模型的权重就没有用了。第二个输出是告诉我们哪个位置的内容是真内容(1),哪个是 padding(0)。所以后面这个输出也是重要的,不能丢,不然模型会把  当成是真的碱基去算attention,padding位置就混进了结果。

这里是以6-mer为一个token,像第一个序列‘TATAAACGTT’,10个碱基,就是前面6个是一个token,后面4个凑不成6-mer,就拆成单碱基,以这样的规则来做的。

  • 1
  • 2
  • 3
位置:  012345 6TATAAACGTT:  =3   TATAAA=1094    C=4104 G=4105 T=4103 T=4103 =1ACGTACGT...: =3   ACGTAC=728     GTACGT A=4102 C=4104 G=4105 T=4103

前面的代表是分类,是整条序列的代言人。

加载预训练模型

  • 1
  • 2
  • 3
  • 4
  • 5
def load_model(device):    model = AutoModelForSequenceClassification.from_pretrained(        "InstaDeepAI/nucleotide-transformer-v2-50M-multi-species",         num_labels=2, trust_remote_code=True).to(device)


    
    return(model)

这里的AutoModelForSequenceClassification 会根据模型的名字去找相应的config.json,然后就能确定是该实例化哪个具体的类。在这个具体的例子中,会被解析成EsmForSequenceClassification,我们用真正干活的这个类,也是可以的。但是代码就被写死了,换一个模型换一个类。用这种写法,只要换模型名字就行,具体干活的类根本不用管。这里num_labels=2就触发了换头,它丢掉了语言模型头,新加一个分类头classifier,输出2个logits。

Auto家族:

类
主干
任务头
用途
AutoModel
✅
❌ 只输出 hidden states
你想自己接任意头
AutoModelForSequenceClassification
✅
分类头
A4 用的
AutoModelForMaskedLM
✅
语言模型头
预训练/继续预训练
AutoModelForTokenClassification
✅
逐 token 分类头
每个碱基/密码子标注

根据不同的任务,我们可以用不同的Auto类。

训练和评估

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
def train_one_epoch(model, loader, optimizer, criterion, device):    model.train()    total_loss = 0.0    for batch in loader:   input_ids,mask,y=[t.to(device)fortin batch]        optimizer.zero_grad()        outputs = model(input_ids=input_ids, attention_mask=mask)        loss = criterion(outputs.logits, y)        loss.backward()        optimizer.step()        total_loss += loss.item()    return total_loss / len(loader)


    


def evaluate(model, loader, device):    model.eval()    correct, total = 0, 0    with torch.no_grad():        for batch in loader:   input_ids,mask,y=[t.to(device)fortin batch]            outputs = model(input_ids=input_ids, attention_mask=mask)   correct+=(outputs.logits.argmax(dim=1)==y).sum().item()            total += y.size(0)    return correct / total * 100

这个是没区别的。

主函数

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
def main():    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")    print(f"使用设备: {device}")
    print("生成数据...")    seqs, y = generate_data(8000)
    print("加载 tokenizer 与预训练模型(首次下载约 500MB)...")    tokenizer = load_tokenizer()    model = load_model(device)
    print("分词...")    input_ids, attention_mask = tokenize(seqs, tokenizer)        dataset = TensorDataset(input_ids, attention_mask, y)    train_loader = DataLoader(TensorDataset(*[t[:1600] for t  in dataset.tensors]), batch_size=16, shuffle=True)    test_loader  = DataLoader(TensorDataset(*[t[1600:] for t in dataset.tensors]), batch_size=32)
    criterion = torch.nn.CrossEntropyLoss()    optimizer = torch.optim.Adam(model.parameters(), lr=5e-5)
    print("微调 3 个 epoch(预训练模型收敛很快)...")    for epoch in range(1, 11):        loss = train_one_epoch(model, train_loader, optimizer, criterion, device)        acc = evaluate(model, test_loader, device)        print(f"Epoch {epoch:2d}/10 | 损失: {loss:.4f} | 准确率: {acc:.2f}%")
    print(f"\n完成!准确率 {acc:.2f}%(数据量足够时迁移学习也能到 95%+)")

这些基本上都没有变化,我们省了设计模型这一步,直接用了预训练的。然后其它都基本没区别。训的时候,从预训练的权重开始,而不是随机权重开始,所以收敛会很快,我们的learning rate也要相应地设小一些。因为预训练的权重已经很好了,lr太大会【灾难性遗忘】,把通用特征冲掉。所以微调的lr比从头训练小1-2个数量级。

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
使用设备: cuda生成数据...加载 tokenizer 与预训练模型(首次下载约 500MB)...
Some weightsofthemodelcheckpointatInstaDeepAI/nucleotide-transformer-v2-50M-multi-specieswerenotusedwheninitializingEsmForSequenceClassification:['lm_head.bias','lm_head.decoder.weight','lm_head.dense.bias','lm_head.dense.weight','lm_head.layer_norm.bias','lm_head.layer_norm.weight']- ThisISexpectedifyouareinitializingEsmForSequenceClassificationfromthecheckpointofamodeltrainedonanothertaskorwithanotherarchitecture(e.g.initializingaBertForSequenceClassificationmodelfromaBertForPreTraining model).- ThisISNOTexpectedifyouareinitializing EsmForSequenceClassificationfromthecheckpointofamodelthatyouexpecttobeexactlyidentical(initializingaBertForSequenceClassificationmodelfromaBertForSequenceClassification model).Some weightsofEsmForSequenceClassificationwerenotinitializedfromthemodelcheckpointatInstaDeepAI/nucleotide-transformer-v2-50M-multi-speciesandarenewlyinitialized:['classifier.dense.bias','classifier.dense.weight','classifier.out_proj.bias','classifier.out_proj.weight']You shouldprobablyTRAINthismodelonadown-streamtasktobeabletouseitforpredictionsand inference.
分词...微调 3 个 epoch(预训练模型收敛很快)...Epoch  1/10 | 损失: 0.6308 | 准确率: 78.56%Epoch  2/10 | 损失: 0.2605 | 准确率: 93.92%Epoch  3/10 | 损失: 0.0790 | 准确率: 95.22%Epoch  4/10 | 损失: 0.0156 | 准确率: 95.69%Epoch  5/10 | 损失: 0.0067 | 准确率: 95.39%Epoch  6/10 | 损失: 0.0279 | 准确率: 93.78%Epoch  7/10 | 损失: 0.0234 | 准确率: 95.66%Epoch  8/10 | 损失: 0.0100 | 准确率: 96.00%Epoch  9/10 | 损失: 0.0196 | 准确率: 89.23%Epoch 10/10 | 损失: 0.0586 | 准确率: 93.73%
完成!准确率 93.73%(数据量足够时迁移学习也能到 95%+)

这就是输出,中间的message是由于换头所导致的,可以不用管。从这个输出来看,3-4个Epoch就够了,后面明显过拟合了。然后我们还看到epoch 9的96->89这种剧烈抖动的,这个我们不能完全归结于过拟合,还需要有lr scheduler才好。

Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/201939