LLM动手实践(一): 微调google的bert和vit模型完成文本和图片的分类任务

1. 写在前面

最近大模型比较火热,也正好在公司开始接触这块相关的业务,大模型是未来的趋势,对于研发工程师来讲,是powerful的效能工具,所以想沉淀一些大模型实践相关的笔记来记录自己在使用大模型产品,部署开源大模型解决实际问题或需求,以及fine-tune大模型实现某个功能过程中的所思和所想

本篇是动手实践的第一篇文章,我们从微调Bert和Vit模型开始,这两个模型在NLP和CV领域的地位不言而喻,自从2017年transformer问世之后, NLP领域的研究就进入了一个新的时代。2018年,谷歌为 NLP 应用程序开发了一个基于 Transformer 的强大的机器学习模型,该模型在不同的基准数据集中均优于以前的语言模型,这个模型被称为Bert, 7年过去了,现在依然能从NLP的各大场景中,看到这哥们的一个身影,Transformer 模型在自然语言处理任务中的成功引发了CV领域的兴趣,2020年,谷歌团队又尝试着将纯 Transformer 的架构应用于图像分类任务,没想到还真成了,在大规模数据集上也是均由于先前的视觉模型,这就是伟大的Vit, 到现在,在视觉领域,Vit模型依然是"无可撼动"的角色。 所以,第一篇文章,我们也来亲自动手微调下这两个模型,亲身感受下这两个模型的魅力, 其次,就是通过微调的技术,用比较少的数据,就能使这两个"猛兽"快速适配到我们自己的任务,且取的不错的效果,站在巨人的肩膀上, 快速前行。

PS: 本系列是实践文章,以实践应用为主,不会有太多理论部分的介绍,关于理论部分,会给出参考文章。给出的实验代码均可直接跑通,实验环境, 一块3090GPU。 提前安装好包:

python 复制代码
pip install numpy
pip install pandas
pip install scikit-learn

# pytorch相关包
pip install torch torchvision

# huggingface相关包
pip install transformers
pip install huggingface_hub
pip install datasets
pip install evaluate

# 可能用到
pip install --upgrade Pillow
pip install accelerate -U

还需要提前了解下transformer的原理, 我之前写过一篇文章: 自然语言处理之Attention大详解(Attention is all you need)

大纲如下

  • [1. 写在前面](#1. 写在前面)
  • [2. 微调google-bert完成文本分类](#2. 微调google-bert完成文本分类)
    • [2.1 模型下载](#2.1 模型下载)
    • [2.2 简单介绍](#2.2 简单介绍)
      • [2.2 1 基本概念](#2.2 1 基本概念)
      • [2.2.2 主要特点](#2.2.2 主要特点)
      • [2.2.3 模型的结构](#2.2.3 模型的结构)
      • [2.2.4 预训练任务](#2.2.4 预训练任务)
      • [2.2.5 应用场景](#2.2.5 应用场景)
      • [2.2.6 使用BERT的流程](#2.2.6 使用BERT的流程)
    • [2.3 微调流程](#2.3 微调流程)
      • [2.3.1 Bert输入](#2.3.1 Bert输入)
      • [2.3.2 Bert输出](#2.3.2 Bert输出)
      • [2.3.3 微调](#2.3.3 微调)
  • [3. 微调google-vit完成图片文类](#3. 微调google-vit完成图片文类)
    • [3.1 模型下载](#3.1 模型下载)
    • [3.2 简单介绍](#3.2 简单介绍)
      • [3.2.1 背景介绍](#3.2.1 背景介绍)
      • [3.2.2 基本概念](#3.2.2 基本概念)
      • [3.2.3 工作原理](#3.2.3 工作原理)
    • [3.3 微调流程](#3.3 微调流程)
      • [3.3.1 Vit输入](#3.3.1 Vit输入)
      • [3.3.2 Vit输出](#3.3.2 Vit输出)
      • [3.3.3 微调](#3.3.3 微调)
  • [4. 小总](#4. 小总)

Ok, let's go 😉

2. 微调google-bert完成文本分类

2.1 模型下载

微调模型第一步:下载google-bert, 如果下载慢的话,也可以用镜像,需要提前在huggingface网站上注册一个号, 拿到token,然后用hfd的方式下载会快些。

python 复制代码
./hfd.sh google-bert/bert-base-cased --tool aria2c -x 4 --hf_username wuzhongqiang --hf_token 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

这个可以理解成是预训练好的一款模型,已经能分析基本的语义信息, 我这里面的目录结果如下:

2.2 简单介绍

BERT(Bidirectional Encoder Representations from Transformers)是由Google提出的一种用于自然语言处理(NLP)的预训练模型。它在许多NLP任务上取得了显著的性能提升。作为NLP的初学者,需要了解BERT模型的基本概念、结构和应用场景, 原理部分建议跟一下沐神的bert论文带读

2.2 1 基本概念

BERT是基于Transformer架构的预训练模型。Transformer是一种专为处理序列数据(如文本)而设计的模型,具有并行化计算的优势。

BERT的核心思想是通过预训练生成一个通用的语言表示,然后在具体任务上进行微调(fine-tuning)。

2.2.2 主要特点

  1. 双向性:BERT是双向的,即它同时从左到右和从右到左读取文本。这与传统的单向语言模型(如GPT)不同,使得BERT能够更好地理解上下文信息。
  2. 预训练和微调:BERT首先在大规模语料库上进行预训练,然后在具体任务上进行微调。这种方法使得BERT在各种NLP任务上表现出色。
  3. Transformer架构:BERT基于Transformer的Encoder部分,利用多头自注意力机制(Multi-head Self-attention)捕捉文本中的长距离依赖关系。

2.2.3 模型的结构

BERT模型由多个Transformer Encoder层堆叠而成。每个Encoder层包含两个主要部分:

  • 多头自注意力机制:用于捕捉文本中的依赖关系。
  • 前馈神经网络:用于进一步处理和转换表示。

BERT有两个主要版本:

  • BERT-Base:12个Encoder层,768维隐藏层,12个注意力头,总参数量为110M。
  • BERT-Large:24个Encoder层,1024维隐藏层,16个注意力头,总参数量为340M。

2.2.4 预训练任务

BERT的预训练包含两个任务:

  • 掩码语言模型(Masked Language Model, MLM):随机掩盖输入文本中的部分单词,并训练模型预测这些被掩盖的单词。这使得BERT能够利用上下文信息理解词义。
  • 下一句预测(Next Sentence Prediction, NSP):判断两段文本是否是连续的。该任务使得BERT能够理解句子之间的关系。

2.2.5 应用场景

BERT可以用于各种NLP任务,包括但不限于:

  1. 文本分类:情感分析、新闻分类等。
  2. 命名实体识别(NER):识别文本中的实体,如人名、地名等。
  3. 问答系统:从文本中提取答案。
  4. 文本生成:如对话生成、摘要生成等。

2.2.6 使用BERT的流程

  1. 预训练:BERT在大规模文本语料库(如Wikipedia和BooksCorpus)上进行预训练。预训练的模型参数通常可以从Hugging Face的Transformers库中下载。
  2. 微调:在具体任务的数据集上对预训练模型进行微调。这一步包括在任务特定的数据集上训练模型,使其适应特定任务的要求。

2.3 微调流程

下面是微调bert做文本分类的内容, 整个目录结构:

2.3.1 Bert输入

Bert的输入: 一系列tokens(word)作为输入, 每个token序列中,会有[CLS][SEP]作为特殊token

  • 每个token都会被转换成一个向量, 这个向量会包含token的语义信息,以及上下文信息
  • [CLS]的向量会作为整个序列的语义表示, 用于下游任务
  • [SEP]的向量会作为两个序列之间的分隔符
  • Bert模型的最大token 512,如果序列中tokens小于512,可以使
    [PAD]填充未使用的tokens, 如果长于512, 需要进行截断

示意图:

intput.py

python 复制代码
"""
 demo: 使用huggingface的transformers库里的BertTokenizer, 将文本转换成token序列
"""
from transformers import BertTokenizer

if __name__ == '__main__':
    bert_path = '/home/zhongqiang/bigmodellearning/LLM/bert-base-cased'
    tokenizer = BertTokenizer.from_pretrained(bert_path)
    example_text = 'Hello, my dog is cute'
    bert_input = tokenizer(example_text, padding='max_length', max_length=10, truncation=True, return_tensors='pt')
    print(bert_input)

    # 如果两个句子
    example_text_1 = 'I will watch friends tonight'
    example_text_2 = 'It is a popular TV show'

    bert_input = tokenizer(example_text_1,
                           example_text_2,
                           padding='max_length',
                           max_length=20,
                           truncation=True,
                           return_tensors="pt")  # "pt": 返回 PyTorch 张量(tensor)"tf": 返回 TensorFlow 张量(tensor)
    print(bert_input)

    # 打印详细信息
    for i, token in enumerate(tokenizer.convert_ids_to_tokens(bert_input['input_ids'][0])):
        print(
            f"Token: {token}, Token Type ID: {bert_input['token_type_ids'][0][i]}, Attention Mask: {bert_input['attention_mask'][0][i]}"
        )
"""
{'input_ids': tensor([[  101,  8667,   117,  1139,  3676,  1110, 10509,   102,     0,     0]]), 每个词的id,理解成词表中的索引,默认有[CLS]和[SEP]
'token_type_ids': tensor([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]), 用来区分不同句子段落的ID标识,每个值标识对应的token属于哪个句子段落,对于单个句子,所有token的token_type_ids都是0
'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1, 0, 0]])},指示哪些token是实际的文本,哪些是填充的token,1表示实际的文本,0表示填充的token

{'input_ids': tensor([[ 101,  146, 1209, 2824, 2053, 3568,  102, 1135, 1110,  170, 1927, 1794,
         1437,  102,    0,    0,    0,    0,    0,    0]]),
'token_type_ids': tensor([[0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0]]),
'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0]])}

Token: [CLS], Token Type ID: 0, Attention Mask: 1
Token: I, Token Type ID: 0, Attention Mask: 1
Token: will, Token Type ID: 0, Attention Mask: 1
Token: watch, Token Type ID: 0, Attention Mask: 1
Token: friends, Token Type ID: 0, Attention Mask: 1
Token: tonight, Token Type ID: 0, Attention Mask: 1
Token: [SEP], Token Type ID: 0, Attention Mask: 1
Token: It, Token Type ID: 1, Attention Mask: 1
Token: is, Token Type ID: 1, Attention Mask: 1
Token: a, Token Type ID: 1, Attention Mask: 1
Token: popular, Token Type ID: 1, Attention Mask: 1
Token: TV, Token Type ID: 1, Attention Mask: 1
Token: show, Token Type ID: 1, Attention Mask: 1
Token: [SEP], Token Type ID: 1, Attention Mask: 1
Token: [PAD], Token Type ID: 0, Attention Mask: 0
Token: [PAD], Token Type ID: 0, Attention Mask: 0
Token: [PAD], Token Type ID: 0, Attention Mask: 0
Token: [PAD], Token Type ID: 0, Attention Mask: 0
Token: [PAD], Token Type ID: 0, Attention Mask: 0
Token: [PAD], Token Type ID: 0, Attention Mask: 0

BERT 中,输入序列的总长度是指 token 的数量,而不仅仅是字符的数量。每个单词、标点符号和特殊字符(如句子分隔符和填充符)都会被视为一个 token
"""

2.3.2 Bert输出

output.py

python 复制代码
"""
BERT(Bidirectional Encoder Representations from Transformers)的输出可以分为几种不同的类型,具体取决于你如何使用它。
"""

from transformers import BertModel
from transformers import BertTokenizer

if __name__ == '__main__':
    bert_path = '/home/zhongqiang/bigmodellearning/LLM/bert-base-cased'
    tokenizer = BertTokenizer.from_pretrained(bert_path)
    model = BertModel.from_pretrained(bert_path, output_hidden_states=True, output_attentions=True)

    example_text = 'Hello, my dog is cute'
    bert_input = tokenizer(example_text, padding='max_length', max_length=10, truncation=True, return_tensors='pt')

    # 获取模型的输出
    outputs = model(**bert_input)

    # 1. 最后一层隐藏状态(Hidden States)
    #    BERT 的每一层都会生成一个隐藏状态向量(hidden state vector),表示输入序列中每个 token 的表示。通常,最常用的是最后一层的隐藏状态。
    #    形状: (batch_size, sequence_length, hidden_size)
    #    用途: 这个输出通常用于句子级任务,如文本分类或问答系统中的token级任务。
    last_hidden_states = outputs.last_hidden_state

    # 2. 池化输出(PooledOutput)池化输出
    #    BERT特殊的[CLS]token的表示,它通常用于句子级任务,例如句子分类。
    #    形状: (batch_size, hidden_size)
    #    用途: 句子分类、句子相似度等任务。
    pooled_output = outputs.pooler_output

    # 3. 所有层的隐藏状态(All Hidden States)
    #    如果你需要所有层的隐藏状态(而不仅仅是最后一层),你可以配置 BERT 模型来输出所有层的隐藏状态。
    #    形状: (num_layers, batch_size, sequence_length, hidden_size)
    #    用途: 研究和分析不同层的表示,或在某些高级任务中使用多个层的表示。
    all_hidden_states = outputs.hidden_states

    # 4. 注意力权重(Attention Weights)
    #    BERT 的注意力机制产生了每个注意力头的注意力权重,你可以配置模型来输出这些权重。
    #    形状: (num_layers, num_attention_heads, batch_size, sequence_length, sequence_length)
    #    用途: 分析模型的注意力模式,理解模型如何关注输入序列的不同部分。
    attentions = outputs.attentions

    print("Last Hidden States Shape:", last_hidden_states.shape)
    print("Pooled Output Shape:", pooled_output.shape)
    print("Number of Hidden States:", len(all_hidden_states))
    print("Number of Attention Layers:", len(attentions))
"""
Last Hidden States Shape: torch.Size([1, 10, 768])
Pooled Output Shape: torch.Size([1, 768])
Number of Hidden States: 13, 每一层的大小torch.Size([1, 10, 768])
Number of Attention Layers: 12, 每一层的大小torch.Size([1, 12, 10, 10])
"""

细节内容参考: Bert的pooler_output是什么?

2.3.3 微调

下载微调数据集:

kaggle上的BBC新闻分类数据集,5个类别tech, business, sport, entertainment, politics

这里先需要写两个工具函数,处理数据集,建一个util目录, 写一个dataset_util.py, 新建一个Dataset类,组织数据, 后面调用torch的DataLoader导入。

python 复制代码
import typing

import numpy as np
import pandas as pd
import torch
from transformers import BertTokenizer


class DataSet(torch.utils.data.Dataset):
    def __init__(self, df: pd.DataFrame, label2id: typing.Dict[str, int], tokenizer: BertTokenizer):
        self.labels = [label2id[label] for label in df['Category']]
        self.texts = [
            tokenizer(text, padding='max_length', max_length=512, truncation=True, return_tensors="pt")
            for text in df['Text']
        ]

    def classes(self):
        return self.labels

    def __len__(self):
        return len(self.labels)

    def get_batch_labels(self, idx):
        # Fetch a batch of labels
        return np.array(self.labels[idx])

    def get_batch_texts(self, idx):
        # Fetch a batch of inputs
        return self.texts[idx]

    def __getitem__(self, idx):
        batch_texts = self.get_batch_texts(idx)
        batch_y = self.get_batch_labels(idx)
        return batch_texts, batch_y


if __name__ == '__main__':
    # Load the dataset
    df = pd.read_csv('/home/wuzhongqiang/data/BBC_News_Train.csv')
    # Split the dataset
    np.random.seed(112)
    df_train, df_val, df_test = np.split(df.sample(frac=1, random_state=42), [int(.8 * len(df)), int(.9 * len(df))])
    print(len(df_train), len(df_val), len(df_test))

    label2id = {'business': 0, 'entertainment': 1, 'sport': 2, 'tech': 3, 'politics': 4}
    id2label = {v: k for k, v in label2id.items()}

    bert_path = '/home/zhongqiang/bigmodellearning/LLM/bert-base-cased'
    tokenizer = BertTokenizer.from_pretrained(bert_path)

    dataset = DataSet(df_train, tokenizer, label2id)
    print(dataset)

写一个model_util.py, 因为是做下游任务, 这里需要拿到bert最后一层的输出,然后写一个分类器往后处理,得到下游任务的输出,也就是我们这里的5分类结果。

python 复制代码
import torch.nn as nn

# 基于Bert模型的分类器模型
class BertClassifier(nn.Module):
    def __init__(self, bert, dropout=0.5):
        super(BertClassifier, self).__init__()
        self.bert = bert
        self.dropout = nn.Dropout(dropout)
        self.linear = nn.Linear(768, 5)
        self.relu = nn.ReLU()

    def forward(self, input_id, mask):
        _, pooled_output = self.bert(input_ids=input_id, attention_mask=mask, return_dict=False)
        dropout_output = self.dropout(pooled_output)
        linear_output = self.linear(dropout_output)
        final_layer = self.relu(linear_output)
        return final_layer

下面写微调bert模型的代码(finetune.py)

python 复制代码
import os.path
import typing

import numpy as np
import pandas as pd
import torch
import torch.nn as nn
import torch.optim as optim
from tqdm import tqdm
from transformers import BertModel
from transformers import BertTokenizer
from utils.dataset_util import DataSet
from utils.model_util import BertClassifier


class FineTuneBert(object):
    def __init__(self, bert_path: str, label2id: typing.Dict[str, int]):
        self._bert_base = BertModel.from_pretrained(bert_path)
        self._tokenizer = BertTokenizer.from_pretrained(bert_path)
        self._bert_classification = BertClassifier(bert=self._bert_base)
        self._label2id = label2id
        self._id2label = {v: k for k, v in self._label2id.items()}

        # 判断是否使用GPU
        self._use_cuda = torch.cuda.is_available()
        self._device = torch.device("cuda" if self._use_cuda else "cpu")

    def train(self, train_data, val_data, learning_rate, epochs):

        # 通过Dataset类获取训练和验证集
        train, val = DataSet(train_data, self._label2id, self._tokenizer), DataSet(val_data, self._label2id,
                                                                                   self._tokenizer)

        # DataLoader根据batch_size获取数据,训练时选择打乱样本
        train_dataloader = torch.utils.data.DataLoader(train, batch_size=1, shuffle=True)
        val_dataloader = torch.utils.data.DataLoader(val, batch_size=1)

        # 定义损失函数和优化器
        criterion = nn.CrossEntropyLoss()
        model = self._bert_classification
        optimizer = optim.Adam(model.parameters(), lr=learning_rate)

        if self._use_cuda:
            model = model.cuda()
            criterion = criterion.cuda()
        # 开始进入训练循环
        for epoch_num in range(epochs):
            # 定义两个变量,用于存储训练集的准确率和损失
            total_acc_train = 0
            total_loss_train = 0
            # ------ 训练模型 -----------
            # 进度条函数tqdm
            for train_input, train_label in tqdm(train_dataloader):
                train_label = train_label.to(self._device)
                mask = train_input['attention_mask'].squeeze(1).to(self._device)
                input_id = train_input['input_ids'].squeeze(1).to(self._device)
                # 通过模型得到输出
                output = model(input_id, mask)
                # 计算损失
                batch_loss = criterion(output, train_label)
                total_loss_train += batch_loss.item()
                # 计算精度
                acc = (output.argmax(dim=1) == train_label).sum().item()
                total_acc_train += acc
                # 模型更新
                model.zero_grad()
                batch_loss.backward()
                optimizer.step()

            # ------ 验证模型 -----------
            # 定义两个变量,用于存储验证集的准确率和损失
            total_acc_val = 0
            total_loss_val = 0
            # 不需要计算梯度
            with torch.no_grad():
                # 循环获取数据集,并用训练好的模型进行验证
                for val_input, val_label in val_dataloader:
                    # 如果有GPU,则使用GPU,接下来的操作同训练
                    val_label = val_label.to(self._device)
                    mask = val_input['attention_mask'].squeeze(1).to(self._device)
                    input_id = val_input['input_ids'].squeeze(1).to(self._device)
                    # 通过模型得到输出
                    output = model(input_id, mask)
                    # 计算损失
                    batch_loss = criterion(output, val_label)
                    total_loss_val += batch_loss.item()
                    # 计算精度
                    acc = (output.argmax(dim=1) == val_label).sum().item()
                    total_acc_val += acc

            # ------ 打印本epoch的训练和验证效果 -----------
            print(f'''Epochs: {epoch_num + 1}
                | Train Loss: {total_loss_train / len(train_data): .3f}
                | Train Accuracy: {total_acc_train / len(train_data): .3f}
                | Val Loss: {total_loss_val / len(val_data): .3f}
                | Val Accuracy: {total_acc_val / len(val_data): .3f}''')
        return model

    def infer(self, fine_tune_bert_param_path: str, input):

        bert_input = self._tokenizer(input, padding='max_length', max_length=512, truncation=True, return_tensors="pt")

        model = self._bert_classification
        model.load_state_dict(torch.load(fine_tune_bert_param_path))
        if self._use_cuda:
            model = model.cuda()
        model.eval()
        print('load my model over')

        # 使用bert tokenizer后的input
        mask = bert_input['attention_mask'].squeeze(1).to(self._device)
        input_id = bert_input['input_ids'].squeeze(1).to(self._device)

        with torch.no_grad():
            preds = model(input_id, mask)
            preds = preds.detach().cpu().numpy()  # tensor([[0.1403, 0.0000, 2.9974, 0.0032, 0.0000]], device='cuda:0')
            preds = np.argmax(preds, axis=1)  # [2]
            return preds[0], self._id2label[preds[0]]


if __name__ == '__main__':
    # 加载数据集
    print('加载数据集...')
    df = pd.read_csv('/home/zhongqiang/bigmodellearning/google_bert_learning/data/BBC_News_Train.csv')
    np.random.seed(2024)
    # np.split 根据指定的位置将数组分割成子数组, int(.8*len(df)) 和 int(.9*len(df)) 分别计算出第80%和第90%的位置
    # 前80%是训练集, 中间10%是验证集, 最后10%是测试集
    df_train, df_val, df_test = np.split(df.sample(frac=1, random_state=42), [int(.8 * len(df)), int(.9 * len(df))])
    print(f"训练集样本数: {len(df_train)}, 验证集样本数: {len(df_val)}, 测试集样本数: {len(df_test)}")
    # 初始化模型
    label2id = {'business': 0, 'entertainment': 1, 'sport': 2, 'tech': 3, 'politics': 4}
    BERT_PATH = '/home/zhongqiang/bigmodellearning/LLM/bert-base-cased'
    fune_bert = FineTuneBert(BERT_PATH, label2id)

    # 训练模型
    print('开始训练模型...')
    EPOCHS = 3
    LR = 1e-6
    model = fune_bert.train(df_train, df_val, LR, EPOCHS)

    # 保存模型, 注意这里保存的是个pytorch格式模型, 并非huggingface的transformers模型
    print('保存模型...')
    model_parent_path = '/home/zhongqiang/bigmodellearning/google_bert_learning/models/text-classifier'
    if not os.path.exists(model_parent_path):
        os.makedirs(model_parent_path)
    torch.save(model.state_dict(), os.path.join(model_parent_path, 'finetune_bert.pt'))

这里的保存注意是pytorch模型,而不是huggineface里面的模型,这里面也是有讲究的这篇文章

微调结束后, 可以推理infer.py

python 复制代码
import pandas as pd
from bert_finetune import FineTuneBert

if __name__ == '__main__':
    # 初始化模型
    label2id = {'business': 0, 'entertainment': 1, 'sport': 2, 'tech': 3, 'politics': 4}
    bert_path = '/home/zhongqiang/bigmodellearning/LLM/bert-base-cased'
    fune_bert = FineTuneBert(bert_path, label2id)
    fine_tune_bert_path = '/home/zhongqiang/bigmodellearning/google_bert_learning/models/text-classifier/finetune_bert.pt'

    # 加载数据
    test_data = pd.read_csv('/home/zhongqiang/bigmodellearning/google_bert_learning/data/BBC_News_Test.csv')
    sample = test_data.sample(5).reset_index(drop=True)
    for i in range(sample.shape[0]):
        artifact_id = sample.iloc[i]['ArticleId']
        artifact_text = sample.iloc[i]['Text']
        print(f"***********{artifact_id}: {artifact_text}***********")
        preds, class_label = fune_bert.infer(fine_tune_bert_path, artifact_text)
        print(f"模型预测结果:{preds}, 真实标签:{class_label}")

这样,就完成了微调bert模型完成一个新闻分类的任务。 "麻雀虽小,五脏俱全",其他的微调任务,我理解大致流程应该也差不多, 拿到预训练bert的最终输出,写一个下游任务的输出头即可。

3. 微调google-vit完成图片文类

上面是一个NLP任务,下面再搞一个视觉的。对比文本分类器, 代码上的主要变化:

  1. 使用了huggingface datasets库、evaluate库、Trainer,简化了代码量
  2. 自定义模型使用AutoModelForImageClassification,可以直接保存成huggingface格式

3.1 模型下载

模型地址https://huggingface.co/google/vit-base-patch16-224,patch分辨率16*16,输入分辨率224*224, 可以直接通过hfd脚本下载。

训练过程:pre-trained on ImageNet-21k (14 million images, 21,843 classes) at resolution 224x224, and fine-tuned on ImageNet 2012 (1 million images, 1,000 classes) at resolution 224x224

3.2 简单介绍

这里也对vit做一个简单介绍,原理依然可以参考沐神的VIT论文带读

3.2.1 背景介绍

  • 传统的卷积神经网络(CNN)在处理图像任务时表现非常出色。然而,Transformer 模型在自然语言处理任务中的成功引发了将其应用于计算机视觉的兴趣。
  • 视觉转换器(ViT)是第一个将纯 Transformer 应用于图像分类任务的模型。

3.2.2 基本概念

  • 图像分块(Patches):将输入图像划分为固定大小的图像块,每个图像块被展平成一个向量。这个过程类似于 NLP 中的单词嵌入。
  • 位置编码(Positional Encoding):因为 Transformer 对序列数据的顺序没有固有的理解,ViT 通过添加位置编码来注入位置信息。
  • Transformer 架构:使用标准的 Transformer 编码器架构,每个图像块的嵌入向量经过多个自注意力层和前馈网络。

3.2.3 工作原理

  1. 将输入图像 𝑥 划分为固定大小的图像块。
  2. 将每个图像块展平并通过一个线性层得到图像块的嵌入向量。
  3. 添加位置编码。
  4. 通过多层 Transformer 编码器处理嵌入向量。
  5. 使用一个分类头对最终的嵌入向量进行分类。

3.3 微调流程

微调ViT模型的步骤:

  1. 准备数据
    确保你的数据已经被预处理并划分为训练集和验证集。可以使用诸如 PyTorch 或 TensorFlow 等深度学习框架。
  2. 加载预训练模型
    从库(例如 Hugging Face Transformers 或 Timm)中加载预训练的 ViT 模型。预训练模型已经在大规模数据集上训练,可以提供一个良好的初始权重。
  3. 修改分类头
    将模型的最后一层分类头替换为适合你特定任务的分类头。例如,如果你的任务有 10 个类别,确保分类头输出的类别数为 10。
  4. 设置训练参数
    设置超参数,例如学习率、优化器(例如 AdamW)、损失函数(例如交叉熵损失)。
  5. 开始训练
    使用训练集数据进行模型微调。确保在每个 epoch 之后在验证集上评估模型的表现,以防止过拟合。
  6. 评估和调整
    根据验证集的表现,调整超参数或进行更多的训练 epoch。你可以使用早停法(Early Stopping)来防止过拟合

目录结构:

3.3.1 Vit输入

需要使用VitImageProcessor处理下,最终是pixel_values的tensor。

ViTImageProcessor 是一个专门用于处理和预处理图像数据的类,通常用于将原始图像转换为适合输入 Vision Transformer (ViT) 模型的格式

  1. 图像大小调整(Resizing):
    ViT 模型通常需要固定大小的图像输入。例如,ViT-B/16 需要 224x224 像素的图像。ViTImageProcessor 会将输入图像调整到指定的大小。
  2. 归一化(Normalization):
    对图像像素值进行归一化,以便它们适合模型的输入范围。通常将像素值缩放到 [0, 1] 或 [-1, 1] 的范围内。
  3. 中心裁剪(Center Cropping):
    如果图像的长宽比例与目标尺寸不匹配,可以使用中心裁剪来确保图像的主要部分被保留。
  4. 将图像转换为张量(Tensor Conversion):
    将图像数据转换为 PyTorch 或 TensorFlow 的张量格式,以便于模型处理。

input.py

python 复制代码
from PIL import Image
from transformers import AutoImageProcessor
from transformers import ViTImageProcessor

if __name__ == '__main__':
    # 加载图像
    img_path = "/home/zhongqiang/bigmodellearning/google_vit_learning/data/flower_images/Tulip/eb2e456c5f.jpg"
    image = Image.open(img_path)

    # 创建 ViTImageProcessor 实例
    # 专门为 Vision Transformer (ViT) 模型设计的图像处理类。它包含了 ViT 所需的所有预处理步骤,如调整大小、归一化、中心裁剪等
    # 适合明确知道要处理的是 ViT 模型,并且需要控制和理解具体的预处理步骤
    vit_path = "/home/zhongqiang/bigmodellearning/LLM/vit-base-patch16-224"
    processor = ViTImageProcessor.from_pretrained(vit_path)  # 这里要指明用的哪个模型,ViTImageProcesser会根据不同的模型调整输入大小

    # 处理图像
    inputs = processor(images=image, return_tensors="pt")

    # 打印处理后的张量
    print(inputs['pixel_values'].shape)  # torch.Size([1, 3, 224, 224])

    # 使用 AutoImageProcessor
    # 通用的图像处理器,它可以根据输入的模型自动选择合适的图像处理类。它的设计目的是简化图像处理的过程,无需用户明确指定处理器的类型,只需提供模型名称即可
    # 多种模型兼容,包括 ViT、DeiT、BEiT 等
    # 适合需要兼容多种不同的模型,并且希望通过简化接口快速进行图像处理的场景
    processor = AutoImageProcessor.from_pretrained(vit_path)

    # 处理图像
    inputs = processor(images=image, return_tensors="pt")
    print(inputs['pixel_values'].shape)  # torch.Size([1, 3, 224, 224])

3.3.2 Vit输出

既可以输出向量,也可以输出概率

output.py

python 复制代码
"""
Vision Transformer (ViT) 模型的输出可以根据具体任务的不同而有所变化
"""

import torch
from PIL import Image
from transformers import ViTConfig
from transformers import ViTForImageClassification
from transformers import ViTImageProcessor

if __name__ == '__main__':
    # 加载图像
    img_path = "/home/zhongqiang/bigmodellearning/google_vit_learning/data/flower_images/Tulip/eb2e456c5f.jpg"
    image = Image.open(img_path)

    # 创建 ViTImageProcessor 实例
    vit_path = "/home/zhongqiang/bigmodellearning/LLM/vit-base-patch16-224"
    processor = ViTImageProcessor.from_pretrained(vit_path)  # 这里要指明用的哪个模型,ViTImageProcesser会根据不同的模型调整输入大小

    # 处理图像
    inputs = processor(images=image, return_tensors="pt")

    # 加载预训练的 ViT 模型
    model = ViTForImageClassification.from_pretrained(vit_path)

    # 获取模型输出, 保留hidden states
    with torch.no_grad():
        outputs = model(**inputs, output_hidden_states=True)

    # logits(分类任务)
    # 对于图像分类任务,ViT 模型的输出通常是一个 logits 向量,每个元素对应一个类别的分数。
    # 通过 softmax 函数将 logits 转换为概率分布,然后选择概率最高的类别作为预测结果
    # 维度: [batch_size, num_classes]
    logits = outputs.logits
    print(logits.shape)  # torch.Size([1, 1000])  默认是个1000分类 其预训练时使用的是 ImageNet 数据集,ImageNet 数据集包含 1000 个类别
    # 预测的类别
    print(model.config.num_labels)  # 1000
    predicted_class_idx = logits.argmax(-1).item()
    print(
        f"Predicted class index: {predicted_class_idx}, Predicted class: {model.config.id2label[predicted_class_idx]}")
    # Predicted class index: 664, Predicted class: monitor, 微调的时候需要匹配到自己的任务

    # 获取特征向量
    # 对于特征提取任务,ViT 模型的输出通常是 [CLS] 标记对应的特征向量。这个特征向量可以用于下游任务,如图像检索、相似度计算等。
    # [CLS] 向量 是 Vision Transformer 模型中的全局表示,位于每个 Transformer 层的输出的第一个 token
    cls_embedding = outputs.hidden_states[-1][:, 0]  # 从最后一层的隐藏状态中提取 [CLS] 向量(索引为 0 的 token)
    print(len(outputs.hidden_states))  # 13  vit-base-patch16-224 模型具有 12 个 Transformer 层 + 1个初始隐藏状态
    print(cls_embedding.shape)  # torch.Size([1, 768])

    # 如果需要将特征向量转换为 numpy 数组
    cls_embedding_np = cls_embedding.numpy()

3.3.3 微调

依然建立一个util目录,写一个处理数据集的类dataset_util.py

python 复制代码
import typing

from datasets import load_dataset
from torchvision import transforms
from transformers import AutoImageProcessor


class DataSet(object):
    def __init__(self, data_dir: str, mode: str):
        self._data_dir = data_dir
        self._mode = mode
        if self._mode == "imagefolder":
            self._dataset = load_dataset(self._mode, data_dir=data_dir)

    def get_dataset(self):
        return self._dataset

    def get_label_id_map(self) -> typing.Tuple[dict, dict]:
        labels = self._dataset["train"].features["label"].names
        label2id, id2label = dict(), dict()
        for i, label in enumerate(labels):
            label2id[label] = i
            id2label[i] = label
        return label2id, id2label


class ImageProcessor(object):
    def __init__(self, vit_path: str):
        self._vit_path = vit_path
        self._image_processor = AutoImageProcessor.from_pretrained(vit_path)
        self._normalize = transforms.Normalize(mean=self._image_processor.image_mean,
                                               std=self._image_processor.image_std)

    def _get_crop_size(self):

        if "height" in self._image_processor.size:
            size = (self._image_processor.size["height"], self._image_processor.size["width"])
            crop_size = size
            max_size = None
            return size, crop_size, max_size
        elif "shortest_edge" in self._image_processor.size:
            size = self._image_processor.size["shortest_edge"]
            crop_size = (size, size)
            max_size = self._image_processor.size.get("longest_edge")
            return size, crop_size, max_size

    def get_image_processor(self):
        return self._image_processor

    def get_train_transforms(self):
        size, crop_size, _ = self._get_crop_size()
        return transforms.Compose([
            transforms.RandomResizedCrop(crop_size),
            transforms.RandomHorizontalFlip(),
            transforms.ToTensor(), self._normalize
        ])

    def get_val_transforms(self):
        size, crop_size, _ = self._get_crop_size()
        return transforms.Compose(
            [transforms.Resize(size),
             transforms.CenterCrop(crop_size),
             transforms.ToTensor(), self._normalize])

微调代码finetune.py

python 复制代码
"""
Kaggle上的图片分类数据集,5000张图片,共5类(Lilly, Lotus, Orchid, Sunflower and Tulip),每类1000张
下载地址:https://www.kaggle.com/datasets/kausthubkannan/5-flower-types-classification-dataset
"""

import evaluate
import numpy as np
import torch
from transformers import AutoModelForImageClassification
from transformers import Trainer
from transformers import TrainingArguments
# from transformers import ViTImageProcessor
from utils.dataset_util import DataSet
from utils.dataset_util import ImageProcessor

if __name__ == '__main__':

    # Step1: 获取数据集: 直接用huggingface的dataset库,可以简化代码逻辑
    img_dir = "/home/zhongqiang/bigmodellearning/google_vit_learning/data/flower_images"
    img_dataset = DataSet(data_dir=img_dir, mode="imagefolder")
    flower_dataset = img_dataset.get_dataset()
    label2id, id2label = img_dataset.get_label_id_map()

    # 处理数据集: 这里用的torchvision里面的transforms包, 做一些图像的变换操作
    vit_path = "/home/zhongqiang/bigmodellearning/LLM/vit-base-patch16-224"
    img_processor = ImageProcessor(vit_path=vit_path)
    image_processor = img_processor.get_image_processor()

    # 这里也可以直接用vit的processor
    # image_processor = ViTImageProcessor.from_pretrained(vit_path)


    def preprocess_train(example_batch):
        train_transforms = img_processor.get_train_transforms()
        # train_transforms = image_processor
        example_batch["pixel_values"] = [train_transforms(image.convert("RGB")) for image in example_batch["image"]]
        return example_batch

    def preprocess_val(example_batch):
        val_transforms = img_processor.get_val_transforms()
        # val_transforms = image_processor
        example_batch["pixel_values"] = [val_transforms(image.convert("RGB")) for image in example_batch["image"]]
        return example_batch

    # 划分数据集
    splits = flower_dataset['train'].train_test_split(test_size=0.1)
    train_val_ds = splits['train']
    test_ds = splits['test']
    splits = train_val_ds.train_test_split(test_size=0.1)
    train_ds = splits['train']
    val_ds = splits['test']
    train_ds.set_transform(preprocess_train)  # 为训练集设置预处理函数
    val_ds.set_transform(preprocess_val)  # 为验证集设置预处理函数
    test_ds.set_transform(preprocess_val)  # 为验证集设置预处理函数

    # Step2: 定义评估函数
    # 用于评估模型的准确率指标
    metric = evaluate.load("accuracy")

    def compute_metrics(eval_pred):
        # 这个函数接受 eval_pred 对象,提取预测结果并计算准确率
        predictions = np.argmax(eval_pred.predictions, axis=1)
        return metric.compute(predictions=predictions, references=eval_pred.label_ids)

    def collate_fn(examples):
        # 将一批样本整理成模型输入格式,主要是将图像像素值和标签堆叠成张量
        pixel_values = torch.stack([example["pixel_values"] for example in examples])
        labels = torch.tensor([example["label"] for example in examples])
        return {"pixel_values": pixel_values, "labels": labels}

    # Step3: 定义训练参数
    my_model_name = "/home/zhongqiang/bigmodellearning/google_vit_learning/models/vit-finetuned-classifier"
    batch_size = 1
    args = TrainingArguments(
        my_model_name,  # 模型名
        remove_unused_columns=False,  # 不移除未使用的列
        evaluation_strategy="epoch",  # 每个 epoch 结束后进行评估  (评估策略,可以是 "no"、"steps" 或 "epoch")
        save_strategy="epoch",  # 每个 epoch 结束后保存模型  (保存策略,可以是 "no"、"steps" 或 "epoch")
        save_total_limit=5,  # 最多保存 5 个模型检查点 (保存的最大检查点数目)
        learning_rate=5e-5,  # 设置学习率
        per_device_train_batch_size=batch_size,  # 设置训练批次大小 (每个设备的训练批次大小)
        gradient_accumulation_steps=1,  # 设置梯度累积步数
        per_device_eval_batch_size=batch_size,  # 设置验证批次大小 (每个设备的评估批次大小)
        num_train_epochs=5,  # 设置训练的 epoch 数
        warmup_ratio=0.1,  # 设置学习率预热比例
        logging_steps=100,  # 每 100 步记录一次日志
        load_best_model_at_end=True,  # 训练结束后加载最佳模型
        metric_for_best_model="accuracy",
    )  # 用于选择最佳模型的评估指标

    # Step4: 定义Trainer
    # 自动选择适当模型架构的类,用于图像分类任务
    # 模型路径也可以指定hugging face的模型,比如:"google/vit-base-patch16-224", 这时会自动下载模型
    model = AutoModelForImageClassification.from_pretrained(
        vit_path,  # 指定了预训练模型的路径或模型名称
        label2id=label2id,  # 字典,将类别名称映射到其对应的ID, 在有自定义标签并且需要重新映射标签ID时使用。
        id2label=id2label,  # 字典,将标签ID映射到其对应的类别名称,用于将预测的 ID 转换回标签名称,以便进行解释和分析。
        ignore_mismatched_sizes=True)  # 设置为 True 时,模型将忽略加载过程中由于模型权重和模型配置大小不匹配的问题。通常用于模型参数维度不完全匹配的情况,例如,当你使用一个已经微调过的模型时

    # Hugging Face 的 Trainer 类是一个高级 API,旨在简化模型的训练和评估过程。
    # 它封装了很多常见的训练任务,如训练循环、验证循环、保存和加载模型等,帮助开发者快速进行模型的训练和评估
    trainer = Trainer(
        model,  # 模型
        args,  # 训练参数 TrainingArguments 对象包含了训练的配置参数,如学习率、批次大小、训练轮数
        train_dataset=train_ds,  # 训练数据集
        eval_dataset=val_ds,  # 验证数据集
        tokenizer=image_processor,  # 图像处理器
        compute_metrics=compute_metrics,  # 评估函数
        data_collator=collate_fn,
    )  # 数据整理函数

    # Step5: 开始训练
    trainer.train()
    # trainer.train(
    #     resume_from_checkpoint=True,  # 从检查点恢复训练
    #     ignore_mismatched_sizes=True  # 忽略权重大小不匹配
    # )

    # Step6: 评估模型
    results = trainer.evaluate()  # 也可以传递验证集 results = trainer.evaluate(eval_dataset=val_ds)
    print(results)

    # Step7: 预测
    predictions = trainer.predict(test_ds)
    print(predictions.predictions)  # 预测的结果
    print(predictions.label_ids)  # 真实标签(如果测试集有标签)
    print(predictions.metrics)  # 评估指标

    # Step8: 保存模型
    # trainer.save_model("./model/vit-finetune-classifier")  # 保存整个模型,导入就可以直接使用

推理代码infer.py

python 复制代码
from PIL import Image
# from transformers import ViTForImageClassification
from transformers import AutoModelForImageClassification
from transformers import ViTImageProcessor

if __name__ == '__main__':

    img_path = "/home/zhongqiang/bigmodellearning/google_vit_learning/data/flower_images/Tulip/eb2e456c5f.jpg"

    # ImageProcessor还继续用VIT的
    vit_path = "/home/zhongqiang/bigmodellearning/LLM/vit-base-patch16-224"
    processor = ViTImageProcessor.from_pretrained(vit_path)
    image = Image.open(img_path)
    inputs = processor(images=image, return_tensors="pt")

    # Model加载新训练得到的vit-finetuned-classifier,识别5种花
    my_model_name = "/home/zhongqiang/bigmodellearning/google_vit_learning/models/vit-finetuned-classifier/checkpoint-20245"
    my_model = AutoModelForImageClassification.from_pretrained(my_model_name)
    outputs = my_model(**inputs)
    logits = outputs.logits

    # model predicts one of the 5 flower classes
    predicted_class_idx = logits.argmax(-1).item()
    print("Predicted class:", my_model.config.id2label[predicted_class_idx])  # Predicted class: Tulip


# 结果
zhongqiang@zouyilin:~/bigmodellearning/google_vit_learning$ python3 vit_infer.py 
Predicted class: Tulip

4. 小总

本篇文章整理了微调bert和vit的实践, 做的任务比较简单,但这是进入复杂微调任务的一个基础。 简单了解了下hugging face框架以及它的transformers库。 利用框架和库,我们现在很容易把大模型迁移到自己的任务上去, 并且会有一个不错的效果。 这一块往后是趋势,所以怎么利用好大模型这个工具是关键, 后面也会尝试通过实践更多的大模型任务,来把这块的内容慢慢丰富,继续加油呀 😉

参考:

相关推荐
大多_C19 小时前
BERT outputs
人工智能·深度学习·bert
通信仿真实验室2 天前
BERT模型入门(1)BERT的基本概念
人工智能·深度学习·自然语言处理·bert·transformer
goomind3 天前
BERT模型
人工智能·深度学习·bert
python_知世3 天前
基于LLaMA-Factory微调Llama3
人工智能·深度学习·程序人生·自然语言处理·大语言模型·llama·大模型微调
通信仿真实验室3 天前
BERT模型入门(2)BERT的工作原理
人工智能·深度学习·自然语言处理·bert·transformer
weixin_543662867 天前
BERT的中文问答系统55
人工智能·python·bert
微雨盈萍cbb9 天前
BERT--自然语言处理的革命性进展
人工智能·自然语言处理·bert
Jacob_AI10 天前
为什么 Bert 的三个 Embedding 可以进行相加?
人工智能·bert·embedding
傅科摆 _ py10 天前
ANOMALY BERT 解读
人工智能·深度学习·bert
通信仿真实验室11 天前
Google BERT入门(5)Transformer通过位置编码学习位置
人工智能·深度学习·神经网络·自然语言处理·nlp·bert·transformer