口语理解任务源码详解系列(四)Ernie模型实现

2023-10-27

Ernie模型实现口语理解任务

一、构建词表

def word2id_func(raw_dataset):
# returns a dictionary of words and their ids
#     print('raw_dataset:' , raw_dataset)
    words = []
    for entry in raw_dataset:
       words.extend(entry['utterance'].split())
    words = list(set(words))
    words_dict = {'[PAD]': PAD_TOKEN}
    words_dict.update({w:i+1 for i, w in enumerate(words)})
    words_dict['[UNK]'] = len(words_dict)
    print('words_dict:', words_dict)
    return words_dict

将训练数据、验证数据、测试数据全部传入,输出一个字典,字典的键为词,值为对应序号。
在这里插入图片描述
在这里插入图片描述
第一个为'[PAD]': 0,最后一个为 '[UNK]': 12135

words_dict.update({w:i+1 for i, w in enumerate(words)})这段代码使用了列表推导式(list comprehension)来生成一个字典,其中键是列表 words 中的元素,值是对应元素在列表中的索引加一。

  1. enumerate(words) 遍历列表 words 中的元素,并同时返回元素的索引和值;
  2. i+1 表示将索引值加一,从而使索引从1开始而不是默认的0。w 是当前元素的值;
  3. w:i+1 for i, w in enumerate(words) 指定了字典的键和值的生成方式;
    举例来说,如果 words = [‘I’, ‘love’, ‘you’],则该代码会生成一个字典 { ‘I’: 1, ‘love’: 2, ‘you’: 3 }。

构建槽位索引序列

def slot2id_func(raw_dataset):
# returns a dictionary of slots and their ids
    slots = ['[PAD]']
    for entry in raw_dataset:
       slots.extend(entry['slots'].split())
    slots = list(set(slots))
    slots_dict = {s:i for i, s in enumerate(slots)}
    print('slots_dict:', slots_dict)
    return slots_dict

在这里插入图片描述
根据给定的原始数据集,返回一个包含槽位及其ID的字典。

  1. 定义了一个名为slots的列表,并将[PAD]作为初始元素添加到该列表中;
  2. 对于原始数据集中的每个条目,将其槽位信息通过空格分割拆分为多个槽位,并extend到slots列表中;
  3. 使用set()函数将slots列表转换为集合,在此过程中去除重复的槽位,并再次将其转换为列表;
  4. 使用字典推导式创建一个字典slots_dict,其中槽位作为键,它们的索引作为值。索引值通过enumerate(slots)获取,从0开始递增。

构建意图标签索引

def intent2id_func(raw_dataset):
# returns a dictionary of intents and their ids
    intents = [entry['intent'] for entry in raw_dataset]
    intents = list(set(intents))
    intents_dict = {inte:i for i, inte in enumerate(intents)}
    # print('intents_dict:', intents_dict)
    return intents_dict

在这里插入图片描述
构建词表

def vocab_func(raw_dataset):
    vocab = set()
    for entry in raw_dataset:
        vocab = vocab.union(set(entry['utterance'].split()))
    print('list(vocab):', list(vocab))
    return ['[PAD]'] + list(vocab) + ['[UNK]']

在这里插入图片描述

二、定义IntentsAndSlots类

class IntentsAndSlots(data.Dataset):
    # Mandatory methods are __init__, __len__ and __getitem__
    def __init__(self, dataset, lang, unk='[UNK]'):
        self.utterances = []
        self.intents = []
        self.slots = []
        self.unk = unk
        for x in dataset:
            self.utterances.append(x['utterance'])
            self.slots.append(x['slots'])
            self.intents.append(x['intent'])

        self.utt_ids = self.mapping_seq(self.utterances, lang.word2id)
        self.slot_ids = self.mapping_seq(self.slots, lang.slot2id)
        self.intent_ids = self.mapping_lab(self.intents, lang.intent2id)

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

    def __getitem__(self, idx):
        utt = torch.Tensor(self.utt_ids[idx])
        slots = torch.Tensor(self.slot_ids[idx])
        intent = self.intent_ids[idx]
        sample = {'utterance': utt, 'slots': slots, 'intent': intent}
        return sample

    # Auxiliary methods

    def mapping_lab(self, data, mapper):
        return [mapper[x] if x in mapper else mapper[self.unk] for x in data]

    def mapping_seq(self, data, mapper):  # Map sequences to number
        res = []
        for seq in data:
            tmp_seq = []
            for x in seq.split():
                if x in mapper:
                    tmp_seq.append(mapper[x])
                else:
                    tmp_seq.append(mapper[self.unk])
            res.append(tmp_seq)
        return res

调用了mapping_seq方法,将utterances、slots和intents转换为相应的ID序列,并保存在utt_ids、slot_ids和intent_ids中。
__getitem__方法:给定的索引idx,获取相应位置上的utt_ids、slot_ids和intent_ids,并使用torch.Tensor进行转换。然后创建一个字典sample,将转换后的数据作为键值对存储在字典中。
mapping_lab将给定的数据列表data根据映射字典mapper进行映射转换。该方法使用列表推导式,遍历数据列表data中的元素x。如果x存在于映射字典mapper中,则将x映射为对应的值;否则,将x映射为指定的未知标记unk对应的值。
数据加载器

def collate_fn(data):
    def merge(sequences):
        '''
        merge from batch * sent_len to batch * max_len
        '''
        lengths = [len(seq) for seq in sequences]
        max_len = 1 if max(lengths) == 0 else max(lengths)
        # Pad token is zero in our case
        # So we create a matrix full of PAD_TOKEN (i.e. 0) with the shape
        # batch_size X maximum length of a sequence
        padded_seqs = torch.LongTensor(len(sequences), max_len).fill_(PAD_TOKEN)
        for i, seq in enumerate(sequences):
            end = lengths[i]
            padded_seqs[i, :end] = seq  # We copy each sequence into the matrix
        # print(padded_seqs)
        padded_seqs = padded_seqs.detach()  # We remove these tensors from the computational graph
        return padded_seqs, lengths

    # Sort data by seq lengths
    data.sort(key=lambda x: len(x['utterance']), reverse=True)
    new_item = {}
    for key in data[0].keys():
        new_item[key] = [d[key] for d in data]
    # We just need one length for packed pad seq, since len(utt) == len(slots)
    src_utt, _ = merge(new_item['utterance'])
    y_slots, y_lengths = merge(new_item["slots"])
    intent = torch.LongTensor(new_item["intent"])

    src_utt = src_utt.to(device)  # We load the Tensor on our seleceted device
    y_slots = y_slots.to(device)
    intent = intent.to(device)
    y_lengths = torch.LongTensor(y_lengths).to(device)

    new_item["utterances"] = src_utt
    new_item["intents"] = intent
    new_item["y_slots"] = y_slots
    new_item["slots_len"] = y_lengths
    return new_item

collate_fn函数的作用是对数据进行填充和合并操作,以便于模型训练过程中对不同长度的序列进行批次化处理。

三、定义参数

hid_size = 200
emb_size = 300

lr = 0.0001 # learning rate
clip = 5 # gradient clipping

out_slot = len(lang.slot2id)
out_int = len(lang.intent2id)
vocab_len = len(lang.word2id)

train_raw = load_data(os.path.join('data', dataset, 'train.json'))
test_raw = load_data(os.path.join('data', dataset, 'test.json'))
dev_raw = load_data(os.path.join('data', dataset, 'valid.json'))

四、定义模型

class JERNIE(nn.Module):
    def __init__(self, out_int, out_slot):
        super(JERNIE, self).__init__()
        self.tokenizer = AutoTokenizer.from_pretrained("nghuyong/ernie-2.0-base-en")
        self.ERNIE = AutoModel.from_pretrained("nghuyong/ernie-2.0-base-en")
        self.ERNIE.to(device)
        # 定义两个线性层,用于意图分类与槽位填充的输出
        self.intent_classifier = nn.Linear(768, out_int)
        self.slot_classifier = nn.Linear(768, out_slot)

    def forward(self, input, lang):
        # get back the input sentence
        utterance = []
        for element in input:
            utterance.append(' '.join(lang.vocab[i] for i in element if i > 0))
        tokenized = self.tokenizer(utterance, return_tensors='pt', add_special_tokens=True, padding=True).to(device)
        output = self.ERNIE(**tokenized)
        intent = output.pooler_output
        slots = output.last_hidden_state[:, :input.size(1), :]

        intent = self.intent_classifier(intent)
        slots = self.slot_classifier(slots)
        slots = slots.permute(0, 2, 1)
        return intent, slots
  1. 前向传播函数中,输入数据被表示为一个列表input,每个元素代表一个句子。通过遍历输入句子,将索引转换为对应的词汇,形成句子列表utterance;
  2. 然后,使用tokenizer对utterance进行编码处理,得到tokenized对象,其中包含编码后的词汇索引和特殊标记等信息。将其移动到指定的设备上;
  3. 接下来,将编码后的句子输入ERNIE模型,获得模型的输出。其中,意图部分直接使用output.pooler_output作为意图分类的输入,槽部分则使用output.last_hidden_state进行处理;
  4. 最后,通过意图分类器和槽分类器分别对意图和槽进行分类,并对槽分类结果进行维度变换。返回意图分类结果intent和槽分类结果slots。

slots = slots.permute(0, 2, 1)这行代码使用了PyTorch的permute函数,用于交换张量的维度顺序。在这里,slots.permute(0, 2, 1)将slots张量的维度从(批大小, 序列长度, 隐藏单元数)变为(批大小, 隐藏单元数, 序列长度)。通过这个操作,槽分类结果的维度被重新排列,以便后续处理或计算的需要。

五、循环评估函数

from conll import evaluate
from sklearn.metrics import classification_report
def evaluation_loop(data, criterion_slots, criterion_intents, model, lang):
    model.eval()
    loss_array = []
    
    ref_intents = []
    hyp_intents = []
    
    ref_slots = []
    hyp_slots = []
    with torch.no_grad(): # It used to avoid the creation of computational graph
        for sample in data:
            intents, slots = model(sample['utterances'], lang)
            loss_intent = criterion_intents(intents, sample['intents'])
            loss_slot = criterion_slots(slots, sample['y_slots'])
            loss = loss_intent + loss_slot 
            loss_array.append(loss.item())
            # Intent inference
            # Get the highest probable class
            out_intents = [lang.id2intent[x] 
                           for x in torch.argmax(intents, dim=1).tolist()] 
            gt_intents = [lang.id2intent[x] for x in sample['intents'].tolist()]
            ref_intents.extend(gt_intents)
            hyp_intents.extend(out_intents)
            
            # Slot inference 
            output_slots = torch.argmax(slots, dim=1)
            for id_seq, seq in enumerate(output_slots):
                length = sample['slots_len'].tolist()[id_seq]
                utt_ids = sample['utterance'][id_seq][:length].tolist()
                gt_ids = sample['y_slots'][id_seq].tolist()
                gt_slots = [lang.id2slot[elem] for elem in gt_ids[:length]]
                utterance = [lang.id2word[elem] for elem in utt_ids]
                to_decode = seq[:length].tolist()
                ref_slots.append([(utterance[id_el], elem) for id_el, elem in enumerate(gt_slots)])
                tmp_seq = []
                for id_el, elem in enumerate(to_decode):
                    tmp_seq.append((utterance[id_el], lang.id2slot[elem]))
                hyp_slots.append(tmp_seq)
    try:            
        results = evaluate(ref_slots, hyp_slots)
    except Exception as ex:
        # Sometimes the model predics a class that is not in REF
        print(ex)
        ref_s = set([x[1] for x in ref_slots])
        hyp_s = set([x[1] for x in hyp_slots])
        print(hyp_s.difference(ref_s))
        
    report_intent = classification_report(ref_intents, hyp_intents, 
                                          zero_division=False, output_dict=True)
    return results, report_intent, loss_array

评估循环函数。它用于对模型在给定数据集上进行评估,并返回评估结果。

  1. 将模型设为评估模式(model.eval())
  2. 初始化损失数组(loss_array)以及存储参考意图、预测意图、参考槽值和预测槽值的列表(ref_intents、hyp_intents、ref_slots、hyp_slots)
  3. 使用torch.no_grad()块,禁止梯度计算,以加快评估过程
  4. 针对数据集中的每个样本,使用模型输入句子的编码表示并获得输出的意图和槽表示
  5. 计算意图分类损失和槽分类损失,并将它们相加得到总损失,将总损失添加到损失数组中
  6. 根据意图输出,将索引转换为对应的意图标签,分别存储参考意图和预测意图
  7. 对槽输出进行处理,将索引转换为对应的槽标签,并将参考槽和预测槽分别存储在ref_slots和hyp_slots中
  8. 在evaluate函数中评估参考槽和预测槽之间的性能,得到评估结果。如果出现异常(例如模型预测的类别不在参考槽中),则打印异常信息和差异部分
  9. 对意图分类的预测结果和参考结果进行评估,得到分类报告
  10. 返回评估结果、意图分类报告和损失数组。
def training_loop(data, optimizer, criterion_slots, criterion_intents, model, lang):
    model.train() 
    loss_array = []
    for sample in data:
        optimizer.zero_grad()  # Zeroing the gradient
        intent, slots = model(sample['utterances'], lang)
        loss_intent = criterion_intents(intent, sample['intents'])
        loss_slot = criterion_slots(slots, sample['y_slots'])
        loss = loss_intent + loss_slot  # In joint training we sum the losses.
        # Is there another way to do that?
        loss_array.append(loss.item())
        loss.backward()
        torch.nn.utils.clip_grad_norm_(model.parameters(), clip)
        optimizer.step()  # Update the weights
    return loss_array
  1. 代码首先将模型设置为训练模式,然后创建一个空的损失数组用于保存每个训练样本的损失值。
  2. 接下来,通过一个循环遍历训练数据集中的每个样本。在每次迭代中,首先将优化器的梯度清零(optimizer.zero_grad()),以便计算新的梯度。
  3. 通过调用模型(model)传入输入序列(sample[‘utterances’])和lang,获取模型对意图(intent)和槽(slots)的预测结果。
  4. 分别计算意图损失(loss_intent)和槽损失(loss_slot)。在联合训练中,将两者的损失相加得到总损失(loss)。
  5. 将当前样本的总损失值添加到损失数组(loss_array)中,以便后续分析和可视化。
  6. 通过调用 loss.backward() 计算损失相对于模型参数的梯度,并通过 torch.nn.utils.clip_grad_norm_() 对梯度进行裁剪,以避免梯度爆炸的问题。
  7. 最后,调用 optimizer.step() 来更新模型的权重参数,使其朝着损失减小的方向更新。
  8. 函数返回损失数组,用于后续分析和可视化训练过程中的损失变化。

六、开始训练

from tqdm import tqdm

for x in tqdm(range(1, n_epochs)):
    loss = training_loop(train_loader, optimizer, criterion_slots,
                         criterion_intents, model, lang)
    if x % 5 == 0:
        sampled_epochs.append(x)
        losses_train.append(np.asarray(loss).mean())
        results_dev, intent_res, loss_dev = evaluation_loop(dev_loader, criterion_slots,
                                                            criterion_intents, model, lang)
        losses_dev.append(np.asarray(loss_dev).mean())
        f1 = results_dev['total']['f']

        if f1 > best_f1:
            best_f1 = f1
        else:
            # halve optimizer learning rate
            if patience % 3 == 0:
                for param_group in optimizer.param_groups:
                    param_group['lr'] = param_group['lr'] / 2
            patience -= 1
        if patience <= 0:  # Early stopping with patience
            break  # Not nice but it keeps the code clean
results_test, intent_test, _ = evaluation_loop(test_loader, criterion_slots,
                                               criterion_intents, model, lang)

完整的训练过程:

  1. 首先定义了一个循环,循环次数是n_epochs(训练轮数)。在每一轮训练中,调用training_loop函数对模型进行训练,并将返回的损失值保存在变量loss中。
  2. if条件语句检查当前轮数x是否可以被5整除。如果满足条件,则进行以下操作:
  3. 将当前轮数x添加到sampled_epochs列表中;
  4. 计算训练集的平均损失,并将其添加到losses_train列表中;
  5. 调用evaluation_loop函数对开发集进行评估,计算评估结果results_dev、意图结果intent_res和损失值loss_dev;
  6. 计算验证集评估结果中的总体F1分数f1;
  7. 如果当前的F1分数大于最佳F1分数best_f1,则将最佳F1分数更新为当前F1分数;
  8. 否则,减少优化器的学习率(learning rate);
  9. 如果patience(耐心)被3整除,则将所有参数组的学习率除以2,即将学习率减半;
  10. 如果耐心(patience)小于等于0,则结束训练循环,即提前停止训练。
  11. 对测试集进行评估,计算评估结果results_test、意图结果intent_test和损失值。

七、运行结果

num measure model score
1 Slot F1 ERNIE 0.9454038997214484
1 Intent Accuracy ERNIE 0.8628571428571429
2 Slot F1 ERNIE 0.9372222222222222
2 Intent Accuracy ERNIE 0.8642857142857143
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

口语理解任务源码详解系列(四)Ernie模型实现 的相关文章

随机推荐

  • java实现微信授权登录

    服务端实现app授权登录 1 导入jar包
  • 智能算法系列之蚁群算法

    本博客封面由ChatGPT DALL E 2共同创作而成 文章目录 前言 1 算法思想 2 算法流程 3 细节梳理 4 算法实现 4 1 问题场景 4 2 代码实现 代码仓库 IALib GitHub 前言 本篇是智能算法 Python复现
  • COMPUTATIONAL BIOLOGYAND BIOIINFORMATICS投稿经验分享

    IEEE ACM Transactions on Computational Biology and Bioinformatics 关于latex 用模板选择 IEEE模板选择器 进行选择就行 注意页数 超了加钱 1 注册登录 进入之后开始
  • 模版之变参模板

    title 模版之变参模板 date 2023 02 22 20 38 03 permalink pages 46d761 categories 通用领域 编程语言 C tags C 元编程 author name zhengzhibing
  • 多进程启动appium服务

    因为小鱼在上一章节写了 多台appium的启动 然后就联想到 能不能搞一个多进程启动appium服务 于是乎 那就搞起来 关于并发的问题 小鱼写过专题文章 常见并发问题 多线程并发 多线程并发框架 但是呢 今天小鱼不写多线程并发 写个多进程
  • JAVA语言基础(1)

    Java基础 1 Java关键字 访问控制 关键字 说明 private 私有的 protected 受保护的 public 公共的 default 默认 类 方法和变量修饰符 abstract 声明抽象 class 类 extends 扩
  • 什么是泛型,为什么要使用泛型? 泛型类和泛型方法的实现以及限定通配符的使用。什么是泛型擦除,为什么有泛型擦除,泛型擦除到底是怎么实现的

    1 泛型的定义和意义 1 1 什么叫泛型 泛型 顾名思义就是广泛的类型 专业术语为 参数化类型 当我们创建对象时没指定类型 任何引用类型都可以使用 兼容多种数据类型 如果是基本类型 会自动装箱转为对应的包装类 如下图 1 2 但是指定类型后
  • OpenAI接口详细介绍,包括输入参数和返回值

    OpenAI的接口有多种不同的类型 这里介绍一种常见的文本生成类型的接口 输入参数 prompt 要生成的文本的上下文 这可以是一个字符串 表示要生成的文本的前缀或后缀 max tokens 生成的文本的最大长度 temperature 生
  • 【微信小程序】小程序选择图片、上传、预览、删除

    1 了解所需要用到的API 1 1wx chooseImage Object object 从本地相册选择图片或使用相机拍照 参数如下 count 9 最多可以选择的图片张数 sizeType original compressed 所选的
  • wxPython Grid 表格控件的使用

    因工作原因使用了一下 wxPython 总的来说不推荐 我的观点是干什么事情 要用那个领域最成熟的库 比如桌面软件 用 C Qt 不流行的的 wx 有 bug 参考文档少 下面贴出我的代码 这个代码展示了 Grid 的使用 可以 增删改 按
  • Linux系统 虚拟机安装教程

    Linux系统 虚拟机安装教程 1 第一步新建虚拟机 2 选择典型安装 3 选择稍后安装操作系统 4 选择系统和版本 5 选择虚拟机路径 6 默认硬盘或者自行调大硬盘 7 配置完成 稍后调整硬件 8 选择编辑虚拟机文件 9 根据个人的机器来
  • node爬取掘金/csdn文章

    前言 平常看到一些好的文章 想在个人博客上转发记录一下 一下一下的去copy太麻烦了 因此有了这个想法 能不能通过文章链接 直接取到当前文章 然后放到markdown编辑器里面 这样copy起来不是方便了很多 哈哈哈 csdn文章获取 需要
  • 【满分】【华为OD机试真题2023 JS】最多提取子串数目

    华为OD机试真题 2023年度机试题库全覆盖 刷题指南点这里 最多提取子串数目 知识点字符串统计 时间限制 1s 空间限制 256MB 限定语言 不限 题目描述 给定由 a z 26个英文小写字母组成的字符串A和B 其中A中可能存在重复字母
  • 【整理七】

    1 Vue2和Vue3的区别至少说5点 点击查看详情 2 Vue3中组件通信的流程 父传子 子传父 父组件传到子组件 父组件是通过props属性给子组件通信的 数据是单向流动 父 gt 子 子组件中修改props数据 是无效的 会有一个红色
  • linux目录和用户组

    一 目录结构 1 根目录 绝对路径 2 bin 二进制 普通用户常用系统命令 3 sbin 管理员用的系统命令 4 dev 设备信息 5 home 普通用户家目录 6 root root 家目录 7 lib 32位库文件 8 lib64 6
  • [OGRE]基础教程来四发:来谈一谈地形系统

    OGRE 基础教程来四发 来谈一谈地形系统 标签 OGRE 2013 10 09 17 22 2238人阅读 评论 1 收藏 举报 分类 OGRE 11 版权声明 本文为博主原创文章 未经博主允许不得转载 英文链接如下 http www o
  • 传统算法与神经网络算法,进化算法优化神经网络

    神经网络算法与进化算是什么关系 应该没有太大的关系吧 我对遗传算法了解一点 遗传算法主要用来优化神经网络第一次运行时所用的连接权值 因为随机的连接权值往往不能对针对的问题有比较好的收敛效果 Matlab神经网络工具箱自动生成的初始权值其实已
  • Redis第九讲 Redis之Hash数据结构Dict字典哈希算法与hash存储过程

    Redis dict使用的哈希算法 前面提到 一个kv键值对 添加到哈希表时 需要用一个映射函数将key散列到一个具体的数组下标 Redis 目前使用两种不同的哈希算法 MurmurHash2 是种32 bit 算法 这种算法的分布率和速度
  • java imap 乱码_用 imaplib 只取信件头会取到乱码,取整封信则正常

    最近用 Python 的 imaplib 和 email 从一个 gmail 信箱中取信 因为信可能有很多 而我只想取特定发件人发来的信 所以就只先取信头 通过类似 rv data self M fetch num BODY HEADER
  • 口语理解任务源码详解系列(四)Ernie模型实现

    Ernie模型实现口语理解任务 一 构建词表 def word2id func raw dataset returns a dictionary of words and their ids print raw dataset raw da