跳转至

8 迁移学习实践

学习目标

  • 了解并掌握迁移学习-中文分类任务开发
  • 了解并掌握迁移学习-中文填空任务开发
  • 了解并掌握迁移学习-中文句子关系任务
  • 了解通过微调脚本微调后模型的使用方法

8.1 通过微调方式进行迁移学习的两种类型

  • 类型一: 直接加载预训练模型进行输入文本的特征表示, 后接自定义网络进行微调输出结果。
  • 类型二: 使用指定任务类型的微调脚本微调预训练模型, 后接带有输出头的预定义网络输出结果。
  • 说明: 所有类型的实战演示, 都将针对中文文本进行,使用类型一。

8.2 迁移学习-中文文本分类

8.2.1 任务介绍

对输入的评论内容进行分类,区分出评论是好评还是差评。

8.2.2 数据介绍

数据文件有三个train.csv,test.csv,validation.csv,数据样式都是一样的:

Python
label,text
1,选择珠江花园的原因就是方便有电动扶梯直接到达海边周围餐馆食廊商场超市摊位一应俱全酒店装修一般但还算整洁 泳池在大堂的屋顶因此很小不过女儿倒是喜欢 包的早餐是西式的还算丰富 服务吗一般
1,15.4寸笔记本的键盘确实爽基本跟台式机差不多了蛮喜欢数字小键盘输数字特方便样子也很美观做工也相当不错
0,房间太小其他的都一般。。。。。。。。。
0,"1.接电源没有几分钟,电源适配器热的不行. 2.摄像头用不起来. 3.机盖的钢琴漆,手不能摸,一摸一个印. 4.硬盘分区不好办."
1,"今天才知道这书还有第6卷,真有点郁闷:为什么同一套书有两种版本呢?当当网是不是该跟出版社商量商量,单独出个第6卷,让我们的孩子不会有所遗憾。"

导入工具包和辅助工具实例化对象:

Python
# 导入工具包
import torch
import torch.nn as nn
from torch.utils.data import DataLoader
from datasets import load_dataset
from transformers import BertTokenizer, BertModel
from torch.optim import AdamW
import time

# 检查是否有可用的GPU
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f'Using device: {device}')

# 加载字典和分词工具 实例化分词工具
my_tokenizer = BertTokenizer.from_pretrained('model/bert-base-chinese')

# 加载预训练模型 实例化预训练模型
my_model_pretrained = BertModel.from_pretrained('model/bert-base-chinese').to(device)

# 查看预训练模型的输出维度
hidden_size = my_model_pretrained.config.hidden_size
print('hidden_size--->', hidden_size)  # 768

通过huggingface的datasets工具加载数据集,代码如下:

Python
def dm_file2dataset():

    # 实例化数据源对象my_dataset_train
    print('\n加载训练集')
    my_dataset_train = load_dataset('csv', data_files='data/train.csv', split='train')
    print('dataset_train--->', my_dataset_train)
    print(my_dataset_train[0:3])

    # 实例化数据源对象my_dataset_test
    print('\n加载测试集')
    my_dataset_test = load_dataset('csv', data_files='data/test.csv', split='train')
    print('my_dataset_test--->', my_dataset_test)
    print(my_dataset_test[0:3])

    print('\n加载验证集')
    # 实例化数据源对象my_dataset_train
    my_dataset_validation = load_dataset('csv', data_files='data/validation.csv', split="train")
    print('my_dataset_validation--->', my_dataset_validation)
    print(my_dataset_validation[0:3])

输出结果:

Python
加载训练集
dataset_train---> Dataset({
    features: ['label', 'text'],
    num_rows: 9600
})
{'label': [1, 1, 0], 'text': ['选择珠江花园的原因就是方便,有电动扶梯直接到达海边,周围餐馆、食廊、商场、超市、摊位一应俱全。酒店装修一般,但还算整洁。 泳池在大堂的屋顶,因此很小,不过女儿倒是喜欢。 包的早餐是西式的,还算丰富。 服务吗,一般', '15.4寸笔记本的键盘确实爽,基本跟台式机差不多了,蛮喜欢数字小键盘,输数字特方便,样子也很美观,做工也相当不错', '房间太小。其他的都一般。。。。。。。。。']}

加载测试集
my_dataset_test---> Dataset({
    features: ['label', 'text'],
    num_rows: 1200
})
{'label': [1, 0, 0], 'text': ['这个宾馆比较陈旧了,特价的房间也很一般。总体来说一般', '怀着十分激动的心情放映,可是看着看着发现,在放映完毕后,出现一集米老鼠的动画片!开始还怀疑是不是赠送的个别现象,可是后来发现每张DVD后面都有!真不知道生产商怎么想的,我想看的是猫和老鼠,不是米老鼠!如果厂家是想赠送的话,那就全套米老鼠和唐老鸭都赠送,只在每张DVD后面添加一集算什么??简直是画蛇添足!!', '还稍微重了点,可能是硬盘大的原故,还要再轻半斤就好了。其他要进一步验证。贴的几种膜气泡较多,用不了多久就要更换了,屏幕膜稍好点,但比没有要强多了。建议配赠几张膜让用用户自己贴。']}

加载验证集
my_dataset_validation---> Dataset({
    features: ['label', 'text'],
    num_rows: 1200
})
{'label': [1, 1, 0], 'text': ['這間酒店環境和服務態度亦算不錯,但房間空間太小~~不宣容納太大件行李~~且房間格調還可以~~ 中餐廳的廣東點心不太好吃~~要改善之~~~~但算價錢平宜~~可接受~~ 西餐廳格調都很好~~但吃的味道一般且令人等得太耐了~~要改善之~~', '<荐书> 推荐所有喜欢<红楼>的红迷们一定要收藏这本书,要知道当年我听说这本书的时候花很长时间去图书馆找和借都没能如愿,所以这次一看到当当有,马上买了,红迷们也要记得备货哦!', '商品的不足暂时还没发现,京东的订单处理速度实在.......周二就打包完成,周五才发货...']}

8.2.3 数据预处理

对持久化文件中数据进行处理,以满足模型训练要求:

Python
# 数据集处理自定义函数
def collate_fn1(data):

    # data传过来的数据是list eg: 批次数8,8个字典
    # [{'text':'xxxx','label':0} , {'text':'xxxx','label':1}, ...]
    sents = [i['text'] for i in data]
    labels = [i['label'] for i in data]

    # 编码text2id 对多句话进行编码用batch_encode_plus函数
    data = my_tokenizer.batch_encode_plus(batch_text_or_text_pairs=sents,
                                   truncation=True,
                                   padding='max_length',
                                   max_length=500,
                                   return_tensors='pt')

    # input_ids:编码之后的数字
    # attention_mask:是补零的位置是0,其他位置是1
    input_ids = data['input_ids'].to(device)
    attention_mask = data['attention_mask'].to(device)
    token_type_ids = data['token_type_ids'].to(device)
    labels = torch.LongTensor(labels).to(device)

    # 返回text2id信息 掩码信息 句子分段信息 标签y
    return input_ids, attention_mask, token_type_ids, labels


# 测试数据
def dm01_test_dataset():

    # 实例化数据源 通过训练文件
    my_dataset_train = load_dataset('csv', data_files='data/train.csv', split="train")
    print('my_dataset_train--->', my_dataset_train)

    # 实例化数据迭代器 my_dataloader
    # collate_fn: 作用于batch_size个数据点,定义如何合并数据到batch
    # drop_last: 是否丢弃最后一个不完整的batch
    my_dataloader = DataLoader(my_dataset_train,
                               batch_size=8,
                               collate_fn=collate_fn1,
                               shuffle=True,
                               drop_last=True)
    print('my_dataloader--->', len(my_dataloader))

    # 调整数据迭代器对象数据返回格式
    for i, (input_ids, attention_mask, token_type_ids, labels) in enumerate(my_dataloader):
        print(input_ids.shape, attention_mask.shape, token_type_ids.shape, labels)
        # 打印句子text2id后的信息
        print('input_ids', input_ids)
        # 打印句子attention掩码信息
        print('attention_mask', attention_mask)
        # 打印句子分段信息
        print('token_type_ids', token_type_ids)
        # 打印目标y信息
        print('labels', labels)
        break

输出结果:

Python
# 显示训练集字段和样本数目
my_dataset_train---> Dataset({
    features: ['label', 'text'],
    num_rows: 9600
})
my_dataloader---> 1200

# 显示处理后送给模型的数据信息
torch.Size([8, 500]) torch.Size([8, 500]) torch.Size([8, 500]) tensor([1, 1, 0, 0, 1, 0, 0, 0])

# 句子text2id后的信息
input_ids tensor([[ 101, 6848, 2885,  ...,    0,    0,    0],
        [ 101, 8115,  119,  ...,    0,    0,    0],
        [ 101, 2791, 7313,  ...,    0,    0,    0],
        ...,
        [ 101, 3322, 1690,  ...,    0,    0,    0],
        [ 101, 1457, 1457,  ...,    0,    0,    0],
        [ 101, 6821, 3315,  ...,    0,    0,    0]])

# 句子注意力机制掩码信息
attention_mask tensor([[1, 1, 1,  ..., 0, 0, 0],
        [1, 1, 1,  ..., 0, 0, 0],
        [1, 1, 1,  ..., 0, 0, 0],
        ...,
        [1, 1, 1,  ..., 0, 0, 0],
        [1, 1, 1,  ..., 0, 0, 0],
        [1, 1, 1,  ..., 0, 0, 0]])

# 句子分段信息
token_type_ids tensor([[0, 0, 0,  ..., 0, 0, 0],
        [0, 0, 0,  ..., 0, 0, 0],
        [0, 0, 0,  ..., 0, 0, 0],
        ...,
        [0, 0, 0,  ..., 0, 0, 0],
        [0, 0, 0,  ..., 0, 0, 0],
        [0, 0, 0,  ..., 0, 0, 0]])

# 句子的标签信息
labels tensor([1, 1, 0, 0, 1, 0, 0, 0])

8.2.4 自定义下游任务网络模型

自定义单层的全连接网络作为微调网络。根据实际经验, 自定义的微调网络参数总数应大于0.5倍的训练数据量, 小于10倍的训练数据量, 这样有助于模型在合理的时间范围内收敛。

自定义下游任务网络模型代码实现:

Python
# 定义下游任务模型
class MyModel(nn.Module):
    def __init__(self):
        super().__init__()

        # 定义全连接层
        self.fc = nn.Linear(768, 2)

    def forward(self, input_ids, attention_mask, token_type_ids):

        # 预训练模型不训练 只进行特征抽取 [8,500] ---> [8,768]
        with torch.no_grad():
            out = my_model_pretrained(input_ids=input_ids,
                       attention_mask=attention_mask,
                       token_type_ids=token_type_ids)

        # 下游任务模型训练 数据经过全连接层 [8,768] --> [8,2]
        # out.last_hidden_state: 最后一层的隐藏状态张量
        # [:, 0] 选择的是序列的第一个token的隐藏状态
        # 通常这个token是特殊的[CLS],该token被设计用于表示整个序列的语义。
        # BERT训练时,特别是文本分类任务,使用[CLS]的表示来作为整个句子的表示。
        # out = self.fc(out.last_hidden_state[:, 0])
        # pooler_output: 通过last_hidden_state[:, 0]拿到[CLS]向量表示后又经过linear层(形状不变)
        out = self.fc(out.pooler_output)
        return out

模型测试:

Python
# 下游任务模型输入和输出测试
def dm02_test_mymodel():

    # 实例化数据源 通过训练文件
    my_dataset_train = load_dataset('csv', data_files='data/train.csv', split="train")
    # print('my_dataset_train--->', dataset_train)

    # 实例化数据迭代器 my_dataloader
    my_dataloader = DataLoader(dataset=my_dataset_train,
                               batch_size=8,
                               collate_fn=collate_fn1,
                               shuffle=True,
                               drop_last=True)
    # print('my_dataloader--->', len(my_dataloader))

    # 冻结预训练模型的前6层encoder层
    # for i, layer in enumerate(my_model_pretrained.encoder.layer):
    #   if i < 6:
    #       for param in layer.parameters():
    #           param.requires_grad = False

    # 不训练,不需要计算梯度
    for param in my_model_pretrained.parameters():
        param.requires_grad_(False)

    # 实例化下游任务模型
    my_model = MyModel().to(device)
    print('my_model--->', my_model)

    # 调整数据迭代器对象数据返回格式
    for i, (input_ids, attention_mask, token_type_ids, labels) in enumerate(my_dataloader):
        # 数据送给模型
        y_out = my_model(input_ids, attention_mask, token_type_ids)
        print('y_out---->', y_out.shape, y_out)
        break

输出结果:

Python
# 模型信息打印
my_model---> MyModel(
  (fc): Linear(in_features=768, out_features=2, bias=True)
)

# 模型运算后分类结果展示
y_out----> torch.Size([8, 2]) tensor([[0.4062, 0.5938],
        [0.2788, 0.7212],
        [0.3671, 0.6329],
        [0.2496, 0.7504],
        [0.2995, 0.7005],
        [0.2566, 0.7434],
        [0.2537, 0.7463],
        [0.3832, 0.6168]], grad_fn=<SoftmaxBackward>)

8.2.5 模型训练

Python
# 模型训练
def dm03_train_model():
    # 实例化数据源 通过训练文件
    my_dataset_train = load_dataset('csv', data_files='data/train.csv', split="train")

    # 实例化数据迭代器对象my_dataloader
    my_dataloader = DataLoader(dataset=my_dataset_train,
                               batch_size=8,
                               collate_fn=collate_fn1,
                               shuffle=True,
                               drop_last=True)

    # 实例化下游任务模型my_model
    my_model = MyModel().to(device)

    # 实例化优化器my_optimizer
    my_optimizer = AdamW(my_model.parameters(), lr=5e-4)

    # 实例化损失函数my_criterion
    my_criterion = nn.CrossEntropyLoss()

    # 实例化数据源对象my_dataset_train
    my_dataset_train = load_dataset('csv', data_files='data/train.csv', split="train")
    print('dataset_train--->', my_dataset_train)

    # 不训练预训练模型 只让预训练模型计算数据特征 不需要计算梯度
    for param in my_model_pretrained.parameters():
        param.requires_grad_(False)

    # 设置训练参数
    epochs = 3

    # 设置模型为训练模型
    my_model.train()

    # 外层for循环 控制轮数
    for epoch_idx in range(epochs):

        # 每次轮次开始计算时间
        starttime = int(time.time())

        # 内层for循环 控制迭代次数
        for i, (input_ids, attention_mask, token_type_ids, labels) in enumerate(my_dataloader, start=1):

            # 给模型喂数据 [8,500] --> [8,2]
            my_out = my_model(input_ids=input_ids,
                        attention_mask=attention_mask,
                        token_type_ids=token_type_ids)

            # 计算损失
            my_loss = my_criterion(my_out, labels)

            # 梯度清零
            my_optimizer.zero_grad()

            # 反向传播
            my_loss.backward()

            # 梯度更新
            my_optimizer.step()

            # 每5次迭代 算一下准确率
            if i % 5 == 0:
                out = my_out.argmax(dim=1) # [8,2] --> (8,)
                acc = (out == labels).sum().item() / len(labels)
                print('轮次:%d 迭代数:%d 损失:%.6f 准确率%.3f 时间%d' \
                      %(epoch_idx, i, my_loss.item(), acc, int(time.time())-starttime))

        # 每个轮次保存模型
        torch.save(my_model.state_dict(), 'train_model/my_model_class_%d.bin' % (epoch_idx + 1))

输出结果:

Python
轮次:0 迭代数:5 损失:0.735494 准确率0.250 时间40
轮次:0 迭代数:10 损失:0.614211 准确率0.875 时间81
轮次:0 迭代数:15 损失:0.635408 准确率0.750 时间119
轮次:0 迭代数:20 损失:0.575522 准确率1.000 时间157
轮次:0 迭代数:25 损失:0.661196 准确率0.625 时间196
轮次:0 迭代数:30 损失:0.546462 准确率0.875 时间234
轮次:0 迭代数:35 损失:0.609517 准确率0.875 时间272
轮次:0 迭代数:40 损失:0.529246 准确率1.000 时间310
轮次:0 迭代数:45 损失:0.474820 准确率1.000 时间348
轮次:0 迭代数:50 损失:0.540127 准确率0.875 时间387
轮次:0 迭代数:55 损失:0.575326 准确率0.625 时间426
# 从以上的训练输出效果来看,预训练模型是十分强大的,只需要短短的几次迭代,就可以让准确率上88%

8.2.6 模型评估

Python
# 模型测试
def dm04_evaluate_model():
    # 实例化数据源对象my_dataset_test
    print('\n加载测试集')
    my_dataset_test = load_dataset('csv', data_files='data/test.csv', split='train')
    print('my_dataset_test--->', my_dataset_test)
    # print(my_dataset_test[0:3])

    # 实例化下游任务模型my_model
    path = 'train_model/my_model_class_3.bin'
    my_model = MyModel().to(device)
    my_model.load_state_dict(torch.load(path))
    print('my_model-->', my_model)

    # 设置下游任务模型为评估模式
    my_model.eval()

    # 设置评估参数
    correct = 0
    total = 0

    # 实例化化my_dataloader
    my_loader_test = DataLoader(my_dataset_test,
                                batch_size=8,
                                collate_fn=collate_fn1,
                                shuffle=True,
                                drop_last=True)

    # 给模型送数据 测试预测结果
    for i, (input_ids, attention_mask, token_type_ids,
            labels) in enumerate(my_loader_test):

        # 预训练模型进行特征抽取
        with torch.no_grad():
            my_out = my_model(input_ids=input_ids,
                        attention_mask=attention_mask,
                        token_type_ids=token_type_ids)

        # 贪心算法求预测结果
        out = my_out.argmax(dim=1)

        # 计算准确率
        correct += (out == labels).sum().item()
        total += len(labels)

        # 每5次迭代打印一次准确率
        if i % 5 == 0:
            print(correct / total, end=" ")
            print(my_tokenizer.decode(input_ids[0], skip_special_tokens=True), end=" ")
            print('预测值 真实值:', out[0].item(), labels[0].item())

输出结果:

Python
0.875                                           预测值 真实值: 0 0
0.8125            ,                      ,        .                   ,        .               ,     ,                            . 预测值 真实值: 1 0
0.8409090909090909 1.                                                                                                 预测值 真实值: 0 0
0.828125                                       预测值 真实值: 1 1
0.8511904761904762                                                                                        预测值 真实值: 0 0

8.3 迁移学习-中文完型填空

8.3.1 任务介绍

Python
# 输入一句话,MASK一个字,训练模型进行填空
[CLS]             便   [MASK]               [SEP]
# 本句MASK的字为“电”

8.3.2 数据介绍

数据文件有三个train.csv,test.csv,validation.csv,数据样式都是一样的:

Python
label,text
1,选择珠江花园的原因就是方便有电动扶梯直接到达海边周围餐馆食廊商场超市摊位一应俱全酒店装修一般但还算整洁 泳池在大堂的屋顶因此很小不过女儿倒是喜欢 包的早餐是西式的还算丰富 服务吗一般
1,15.4寸笔记本的键盘确实爽基本跟台式机差不多了蛮喜欢数字小键盘输数字特方便样子也很美观做工也相当不错
0,房间太小其他的都一般。。。。。。。。。
0,"1.接电源没有几分钟,电源适配器热的不行. 2.摄像头用不起来. 3.机盖的钢琴漆,手不能摸,一摸一个印. 4.硬盘分区不好办."
1,"今天才知道这书还有第6卷,真有点郁闷:为什么同一套书有两种版本呢?当当网是不是该跟出版社商量商量,单独出个第6卷,让我们的孩子不会有所遗憾。"

导入工具包和辅助工具实例化对象:

Python
import torch
import torch.nn as nn
from torch.utils.data import DataLoader
from datasets import load_dataset
from transformers import BertTokenizer, BertModel
from torch.optim import AdamW
import time

# 检查是否有可用的GPU
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f'Using device: {device}')

# 加载字典和分词工具
my_tokenizer = BertTokenizer.from_pretrained('model/bert-base-chinese')

# 加载预训练模型
my_model_pretrained = BertModel.from_pretrained('model/bert-base-chinese').to(device)

# 查看预训练模型的输出维度
hidden_size = my_model_pretrained.config.hidden_size
print('hidden_size--->', hidden_size)  # 768

通过huggingface的datasets工具加载数据集,代码如下:

Python
def dm_file2dataset():
    # 获取训练数据集
    train_dataset_tmp = load_dataset('csv', data_files='data/train.csv', split='train')
    print('train_dataset_tmp--->', train_dataset_tmp)
    print('train_dataset_tmp[0]--->', train_dataset_tmp[0])
    # 过滤掉样本的评论内容长度小于等于32的样本
    # x->{'label':0, 'text':xxxx}
    my_train_dataset = train_dataset_tmp.filter(lambda x: len(x['text']) > 32)
    print('my_train_dataset--->', my_train_dataset)

    # 获取测试数据集
    test_dataset_tmp = load_dataset('csv', data_files='data/test.csv', split='train')
    my_test_dataset = test_dataset_tmp.filter(lambda x: len(x['text']) > 32)

    return my_train_dataset, my_test_dataset

8.3.3 数据预处理

对持久化文件中数据进行处理,以满足模型训练要求。

数据预处理和相关测试函数:

Python
# 数据集处理自定义函数
def collate_fn2(data):
    sents = [i['text'] for i in data]

    # 文本数值化
    data = my_tokenizer.batch_encode_plus(batch_text_or_text_pairs=sents,
                                   truncation=True,
                                   padding='max_length',
                                   max_length=32,
                                   return_tensors='pt')

    # input_ids 编码之后的数字
    # attention_mask 是补零的位置是0,其他位置是1
    input_ids = data['input_ids'].to(device)
    attention_mask = data['attention_mask'].to(device)
    token_type_ids = data['token_type_ids'].to(device)

    # 取出每批的8个句子 在第17个位置clone出来 做真实标签
    labels = input_ids[:, 16].clone()  
    # tmpa = input_ids[:, 16]
    # print('tmpa--->', tmpa, tmpa.shape)       # torch.Size([8])
    # print('labels-->', labels.shape, labels)  # torch.Size([8])

    # 将第17个词替换成[MASK]的下标值
    # 获取[MASK]字符
    # print(my_tokenizer.mask_token)
    # 获取[MASK]字符的下标
    # print(my_tokenizer.mask_token_id)
    input_ids[:, 16] = my_tokenizer.get_vocab()[my_tokenizer.mask_token]

    return input_ids, attention_mask, token_type_ids, labels


# 数据源 数据迭代器 测试
def dm01_test_dataset():

    # 生成数据源dataset对象
    dataset_train_tmp = load_dataset('csv', data_files='data/train.csv', split="train")
    # print('dataset_train_tmp--->', dataset_train_tmp)

    # 按照条件过滤数据源对象
    my_dataset_train = dataset_train_tmp.filter(lambda x: len(x['text']) > 32)
    # print('my_dataset_train--->', my_dataset_train)
    # print('my_dataset_train[0:3]-->', my_dataset_train[0:3])

    # 通过dataloader进行迭代
    my_dataloader = DataLoader(my_dataset_train, 
                               batch_size=8, 
                               collate_fn=collate_fn2, 
                               shuffle=True, 
                               drop_last=True)
    print('my_dataloader--->', my_dataloader)

    # 调整数据迭代器对象数据返回格式
    for i, (input_ids, attention_mask, token_type_ids, labels) in enumerate(my_dataloader):

        print(input_ids.shape, attention_mask.shape, token_type_ids.shape, labels)

        print('\n第1句mask的信息')
        print(my_tokenizer.decode(input_ids[0]))
        print(my_tokenizer.decode(labels[0]))

        print('\n第2句mask的信息')
        print(my_tokenizer.decode(input_ids[1]))
        print(my_tokenizer.decode(labels[1]))
        break

输出结果:

Python
my_dataloader---> <torch.utils.data.dataloader.DataLoader object at 0x7fbe28611cd0>
torch.Size([8, 32]) torch.Size([8, 32]) torch.Size([8, 32]) tensor([3160, 1905, 8152, 7415, 7231, 1331, 2360, 5831])

第1句mask的信息
[CLS]                [MASK]               [SEP]


第2句mask的信息
[CLS]                [MASK]    ?          [SEP]

迁移学习 中文填空 End

8.3.4 自定义下游任务网络模型

自定义下游任务网络模型代码实现:

Python
# 定义下游任务模型
class MyModel(nn.Module):
    def __init__(self):
        super().__init__()
        # 定义全连接层
        self.fc = nn.Linear(768, my_tokenizer.vocab_size, bias=False)
        # 设置全连接层偏置为零
        self.fc.bias = nn.Parameter(torch.zeros(my_tokenizer.vocab_size))

    def forward(self, input_ids, attention_mask, token_type_ids):
        # 预训练模型不进行训练
        with torch.no_grad():
            out = my_model_pretrained(input_ids=input_ids,
                             attention_mask=attention_mask,
                             token_type_ids=token_type_ids)

        # 第17个token的语义表示经过全连接层
        # 下游任务进行训练 形状[8,768] ---> [8, 21128]
        out = self.fc(out.last_hidden_state[:, 16])

        # 返回
        return out

模型测试:

Python
# 模型输入和输出测试
def dm02_test_mymodel():
    # 生成数据源dataset对象
    dataset_train_tmp = load_dataset('csv', data_files='data/train.csv', split="train")
    # print('dataset_train_tmp--->', dataset_train_tmp)

    # 按照条件过滤数据源对象
    my_dataset_train = dataset_train_tmp.filter(lambda x: len(x['text']) > 32)
    # print('my_dataset_train--->', my_dataset_train)
    # print('my_dataset_train[0:3]-->', my_dataset_train[0:3])

    # 通过dataloader进行迭代
    my_dataloader = DataLoader(my_dataset_train, 
                               batch_size=8, 
                               collate_fn=collate_fn2, 
                               shuffle=True, 
                               drop_last=True)
    print('my_dataloader--->', my_dataloader)

    # 不训练,不需要计算梯度
    for param in my_model_pretrained.parameters():
        param.requires_grad_(False)

    # 实例化下游任务模型
    mymodel = MyModel().to(device)

    # 调整数据迭代器对象数据返回格式
    for i, (input_ids, attention_mask, token_type_ids, labels) in enumerate(my_dataloader):

        print(input_ids.shape, attention_mask.shape, token_type_ids.shape, labels)

        print('\n第1句mask的信息')
        print(my_tokenizer.decode(input_ids[0]))
        print(my_tokenizer.decode(labels[0]))

        print('\n第2句mask的信息')
        print(my_tokenizer.decode(input_ids[1]))
        print(my_tokenizer.decode(labels[1]))

        # 给模型喂数据 [8,768] ---> [8,21128] 填空就是分类 21128个单词中找一个单词
        myout = mymodel(input_ids, attention_mask, token_type_ids)
        print('myout--->', myout.shape, myout)
        break

输出结果:

Python
my_dataloader---> <torch.utils.data.dataloader.DataLoader object at 0x7fd7c0765c10>
torch.Size([8, 32]) torch.Size([8, 32]) torch.Size([8, 32]) tensor([ 702, 6381,  857, 8024, 2218, 1961, 3175, 2141])

第1句mask的信息
[CLS]  ,     .       [MASK]  ,     (       [SEP]


第2句mask的信息
[CLS]   11  22           [MASK]               [SEP]

myout---> torch.Size([8, 21128]) tensor([[-0.3201,  0.3877,  0.1041,  ...,  0.2262,  0.5397,  0.5053],
        [ 0.0626,  0.1335,  0.7057,  ..., -0.6277, -0.2287, -0.0532],
        [ 0.3807,  0.2024,  0.0514,  ..., -0.0113,  0.3084,  0.4678],
        ...,
        [ 0.3452,  0.1774,  0.0127,  ..., -0.3960,  0.2417, -0.0260],
        [-0.4155,  0.2038,  0.2512,  ..., -0.4112, -0.1052,  0.3574],
        [ 0.3464,  0.3439,  0.6628,  ..., -0.1706,  0.1020,  0.4141]],
       grad_fn=<AddmmBackward>)

8.3.5 模型训练

Python
# 模型训练 - 填空
def dm03_train_model():

    # 实例化数据源对象my_dataset_train
    dataset_train_tmp = load_dataset('csv', data_files='data/train.csv', split="train")
    my_dataset_train = dataset_train_tmp.filter(lambda x: len(x['text']) > 32)
    print('my_dataset_train--->', my_dataset_train)

    # 实例化数据迭代器对象my_dataloader
    my_dataloader = DataLoader(my_dataset_train,
                               batch_size=8,
                               collate_fn=collate_fn2,
                               shuffle=True,
                               drop_last=True)

    # 实例化下游任务模型my_model
    my_model = MyModel().to(device)

    # 实例化优化器my_optimizer
    my_optimizer = AdamW(my_model.parameters(), lr=5e-4)

    # 实例化损失函数my_criterion
    my_criterion = nn.CrossEntropyLoss()

    # 不训练预训练模型 只让预训练模型计算数据特征 不需要计算梯度
    for param in my_model_pretrained.parameters():
        param.requires_grad_(False)

    # 设置训练参数
    epochs = 3

    # 设置模型为训练模型
    my_model.train()

    # 外层for循环 控制轮数
    for epoch_idx in range(epochs):
        starttime = int(time.time())
        # 内层for循环 控制迭代次数
        for i, (input_ids, attention_mask, token_type_ids, labels) in enumerate(my_dataloader, start=1):
            # 给模型喂数据 [8,32] --> [8,21128]
            my_out = my_model(input_ids=input_ids,
                        attention_mask=attention_mask,
                        token_type_ids=token_type_ids)

            # 计算损失
            my_loss = my_criterion(my_out, labels)

            # 梯度清零
            my_optimizer.zero_grad()

            # 反向传播
            my_loss.backward()

            # 梯度更新
            my_optimizer.step()

            # 每5次迭代 算一下准确率
            if i % 20 == 0:
                out = my_out.argmax(dim=1) # [8,21128] --> (8,)
                acc = (out == labels).sum().item() / len(labels)
                print('轮次:%d 迭代数:%d 损失:%.6f 准确率%.3f 时间%d' \
                      %(epoch_idx, i, my_loss.item(), acc, int(time.time())-starttime))

        # 每个轮次保存模型
        torch.save(my_model.state_dict(), 'train_model/my_model_mask_%d.bin' % (epoch_idx + 1))

输出结果:

Python
轮次:2 迭代数:680 损失:0.324525 准确率0.875 时间370
轮次:2 迭代数:700 损失:0.776103 准确率0.750 时间381
轮次:2 迭代数:720 损失:0.681674 准确率0.875 时间392
轮次:2 迭代数:740 损失:0.654384 准确率0.750 时间402
轮次:2 迭代数:760 损失:0.612616 准确率0.875 时间413
轮次:2 迭代数:780 损失:0.918874 准确率0.625 时间424
轮次:2 迭代数:800 损失:0.640087 准确率0.750 时间435
轮次:2 迭代数:820 损失:0.410612 准确率1.000 时间446
轮次:2 迭代数:840 损失:0.395016 准确率1.000 时间457
轮次:2 迭代数:860 损失:0.313001 准确率0.875 时间468
轮次:2 迭代数:880 损失:1.534165 准确率0.750 时间479
轮次:2 迭代数:900 损失:0.384014 准确率0.875 时间490
轮次:2 迭代数:920 损失:0.386283 准确率1.000 时间501
轮次:2 迭代数:940 损失:1.168535 准确率0.750 时间512
轮次:2 迭代数:960 损失:0.568617 准确率0.875 时间522
轮次:2 迭代数:980 损失:1.178597 准确率0.750 时间533
轮次:2 迭代数:1000 损失:0.534274 准确率0.875 时间544
轮次:2 迭代数:1020 损失:0.371301 准确率1.000 时间555
轮次:2 迭代数:1040 损失:0.106059 准确率1.000 时间566
轮次:2 迭代数:1060 损失:1.153722 准确率0.750 时间577
轮次:2 迭代数:1080 损失:0.352150 准确率1.000 时间588
轮次:2 迭代数:1100 损失:1.242567 准确率0.625 时间598
轮次:2 迭代数:1120 损失:0.548795 准确率0.875 时间609
# 只需要3个轮次,就可以填空准确率达到88%以上

8.3.6 模型评估

Python
# 模型测试:填空
def dm04_evaluate_model():
    # 实例化数据源对象my_dataset_test
    print('\n加载测试集')
    my_dataset_tmp = load_dataset('csv', data_files='data/test.csv', split='train')
    my_dataset_test = my_dataset_tmp.filter(lambda x: len(x['text']) > 32)
    print('my_dataset_test--->', my_dataset_test)
    # print(my_dataset_test[0:3])

    # 实例化下游任务模型my_model
    path = 'train_model/my_model_mask_3.bin'
    my_model = MyModel().to(device)
    my_model.load_state_dict(torch.load(path))
    print('my_model-->', my_model)

    # 设置下游任务模型为评估模式
    my_model.eval()

    # 设置评估参数
    correct = 0
    total = 0

    # 实例化化dataloader
    my_loader_test = DataLoader(my_dataset_test,
                                batch_size=8,
                                collate_fn=collate_fn2,
                                shuffle=True,
                                drop_last=True)

    # 给模型送数据 测试预测结果
    for i, (input_ids, attention_mask, token_type_ids,
            labels) in enumerate(my_loader_test):
        with torch.no_grad():
            my_out = my_model(input_ids=input_ids,
                        attention_mask=attention_mask,
                        token_type_ids=token_type_ids)

        out = my_out.argmax(dim=1)
        correct += (out == labels).sum().item()
        total += len(labels)

        if i % 25 == 0:
            print(i+1, my_tokenizer.decode(input_ids[0]))
            print('预测值:', my_tokenizer.decode(out[0]), '\t真实值:', my_tokenizer.decode(labels[0]))
            print(correct / total)

输出结果:

Python
26 [CLS]                [MASK]               [SEP]
预测值:   真实值: 
0.62
51 [CLS]      13          [MASK]               [SEP]
预测值:   真实值: 
0.6625
76 [CLS]                [MASK]               [SEP]
预测值:   真实值: 
0.68
101 [CLS]                [MASK]               [SEP]
预测值:   真实值: 
0.6775
126 [CLS]                [MASK]               [SEP]
预测值:   真实值: 
0.681

8.4 小结

  • 学习了迁移学习方式中文分类任务开发
    • 数据预处理、数据源封装、数据迭代器的使用
    • 搭建中文分类任务下游任务模型,并进行模型训练、模型预测
  • 学习了迁移学习方式中文填空任务开发
    • 数据预处理、数据源封装、数据迭代器的使用
    • 搭建中文填空下游任务模型,并进行模型训练、模型预测