赛题介绍
汽车领域多语种迁移学习挑战
游戏链接:点击直达
赛事背景
为了提高产品竞争力,更好地进入海外市场,国内汽车公司提出了海外市场智能互动的需求。然而,世界各国对数据安全有严格的法律约束。为了做好海外智能互动,当地企业面临的最大挑战是缺乏数据。本主题要求玩家通过NLP实现汽车领域多语种迁移学习的相关人工智能算法。
赛事任务
在这次迁移学习任务中,讯飞智能汽车BU车内将提供更多的人机交互式中文语料,以及少量的中、英、日、阿平行语料作为训练集。参赛者通过提供的数据构建模型,对意图分类和关键信息提取任务,最终用英语、日语和阿拉伯语进行测试和判断。
- 初赛
训练集:中文语料3万,中英平行语料1000,中日平行语料1000
测试集A:英语语料500,日语语料500
测试集B:英语语料500,日语语料500
- 复赛
训练集:中文语料与初赛相同,中阿拉伯平行语料1000条
测试集A:阿拉伯语言500
测试集B:阿拉伯语言500
数据说明
本次比赛为参赛者提供三种内部交互功能语料,包括命令控制、导航和音乐。更多的中文语料和更少的多语言平行语料具有意图分类和关键信息。参赛者需要充分利用提供的数据,在英语、日语和阿拉伯语料的意图分类和关键信息提取任务中取得良好的效果。数据中包含的标签类型和值类型如下表所示。
变量 | 数值格式 | 解释 |
---|---|---|
intent | 数值格式 | 解释 |
intent | string | 整句意图标签 |
device | string | 操作设备名称标签 |
mode | string | 操作设备模式标签 |
offset | string | 调作设备调节量标签 |
endloc | string | 目的地标签 |
landmark | string | 周边搜索参考标签 |
singer | string | 歌手 |
song | string | 歌曲 |
评估指标
根据提交的结果文件,采用本模型accuracy进行评价。
(1)意图分类accuracy = 数量意图正确 / 总数据量
(2)提取关键信息accuracy = 关键信息完全正确 / 总数据量
注:
多抽或少抽每个数据的关键信息都是错误的,最终得分取决于意图分类和关键信息抽取的平均值;
语言转换不得在预测过程中进行,意图分类和关键信息提取任务必须直接使用测试集提供的语言。
3.评估和排名 1.本赛题提供下载数据,选手在本地调试算法,在比赛页面提交结果。
每个团队每天最多提交3次。
3.排名将根据得分由高到低进行排名,排名将选择团队的历史最佳成绩进行排名。
提交工作要求
1、文件格式:按excel格式提交
2.文件大小:无要求:
3.提交次数限制:每个团队每天最多3次
4.文件详细说明:
-
以excel格式提交,编码为UTF-八、第一行为表头;
-
具体格式见提交示例,初赛包括两个sheet,半决赛包括一个sheet
5.不需要上传其他文件
赛程规则
本赛题实行二轮赛制
初赛 6月24日至7月24日 一、预赛阶段:6月24日至7月22日。
初赛二阶段:7月23日到7月24日。系统将在7月23日00:00更换测试数据,参赛队伍需要再次下载数据
2.本赛题以参赛队在规定时间内的最佳成绩为准。
第二阶段结果提交截止日期为7月24日17:00。
复赛 7月27日至8月27日 1.预赛第二阶段排名前20%的球队将晋级半决赛,团队信息将在比赛官网公布。选手将通过比赛官网下载新的训练集和开发集,本地调试算法,在线提交结果。
二、半决赛阶段:7月27日至8月25日。
第二阶段:8月26日至8月27日。测试数据将于8月26日00:00更换,参赛队伍需要再次下载数据
2.本赛题以参赛队在规定时间内的最佳成绩为准。
第二阶段结果提交截止日期为8月27日17:00。
赛题思路
本主题有两个子任务,一个是意图识别任务(文本分类),另一个是槽值识别任务(命名为实体识别),只提供少量的中文平行语料和大量的中文语料,具有资源低、跨语言、多任务、类别不平衡等特点。
在思维方面,可以使用两个模型来解决两个子任务,也可以使用单个模型来解决两个子任务。模型侯健安装文本分类和命名实体识别。
本文Baseline采用后者方案,采用多语言预训练模型共同解决单一模型的两个子任务。
可选择常用的多语言预训练模型bert-base-multilingual-uncased
和bert-base-multilingual-cased
多语言预训练模型等。
Baseline效果
Model | 线下 | 线上 |
---|---|---|
bert-base-multilingual-cased | 0.95771 | 0.74961 |
bert-base-multilingual-uncased | 0.96269 | 0.75622 |
后续优化推荐
- 使用FGM、PGD等对抗训练
- 更换损失函数,使用Focal loss / Dice loss尝试缓解类别不平衡的影响
- 使用EMA提高模型性能
- 使用CRF提升slot识别效果
- 构建平行预测与中文语料的联系
# 更新paddlenlp !pip install paddlenlp==2.3.3 # langid确定语言类型 !pip install langid
# 导入依赖 import os import json import random import numpy as np
import pandas as pd
from tqdm import tqdm
from sklearn.metrics import accuracy_score
from sklearn.model_selection import train_test_split
import langid
import paddle
import paddlenlp
import paddle.nn.functional as F
from functools import partial
from paddlenlp.data import Stack, Dict, Pad
from paddlenlp.datasets import load_dataset
from paddlenlp.transformers.bert.tokenizer import BertTokenizer
import matplotlib.pyplot as plt
import seaborn as sns
0 参数设置
# 设定随机种子,固定结果
seed = 1234
def set_seed(seed):
paddle.seed(seed)
random.seed(seed)
np.random.seed(seed)
set_seed(seed)
# 超参数设置,为了便于后续方案优化,将下方所有分散的超参数整合在本cell中
# 注意:修改此处的超参数并不能生效,需要手动删除下方演示的超参数,再通过本cell控制超参数调优
MODEL_NAME = 'bert-base-multilingual-cased' # bert-base-multilingual-cased
# 设置最大文本长度
max_seq_length = 48
# batch size这里设置
intent_train_batch_size = 32
intent_valid_batch_size = 32
intent_test_batch_size = 32
# pad slot时设置igore_label的值
ignore_label = -100
# 训练过程中的最大学习率
learning_rate = 5e-5
# 训练轮次
epochs = 10
# 学习率预热比例
warmup_proportion = 0.1
# 权重衰减系数,类似模型正则项策略,避免模型过拟合
weight_decay = 0.01
max_grad_norm = 1.0
# 训练结束后,存储模型参数
save_dir_curr = "checkpoint/intent_model"
# 记录训练epoch、损失等值
loggiing_print = 50
loggiing_eval = 50
# 提交文件名称
sumbit_excel_name = "work/submit_tsetA.xlsx"
1 读取数据和EDA
1.1 读取数据
# 查看数据
japan_train = pd.read_excel('data/data154631/日语_train.xlsx')
en_train = pd.read_excel('data/data154631/英文_train.xlsx')
zh_train = pd.read_excel('data/data154631/中文_trian.xlsx')
test_en = pd.read_excel('data/data154631/testA.xlsx',sheet_name='英文_testA')
test_japan = pd.read_excel('data/data154631/testA.xlsx',sheet_name='日语_testA')
test = pd.concat([test_en,test_japan],axis=0)
1.2 统计意图标签
JAPAN_INTENT_LIST = list(set(japan_train['意图']))
print("日语训练集共%d条\t意图标签数目%d个" % (japan_train.shape[0],len(JAPAN_INTENT_LIST)))
EN_INTENT_LIST = list(set(en_train['意图']))
print("英文训练集共%d条\t意图标签数目%d个" % (en_train.shape[0],len(EN_INTENT_LIST)))
ZH_INTENT_LIST = list(set(zh_train['意图']))
print("中文训练集共%d条\t意图标签数目%d个" % (zh_train.shape[0],len(ZH_INTENT_LIST )))
print("测试集共%d条" % (test.shape[0]))
# 意图标签
INTENT_LIST = list(set(EN_INTENT_LIST + ZH_INTENT_LIST + EN_INTENT_LIST))
print(f"意图标签共{
len(INTENT_LIST)}个\n分别有{
INTENT_LIST}")
len(set(JAPAN_INTENT_LIST) & set(ZH_INTENT_LIST)),len(set(JAPAN_INTENT_LIST) & set(EN_INTENT_LIST)),len(set(ZH_INTENT_LIST) & set(EN_INTENT_LIST)),len(INTENT_LIST)
日语训练集共1002条 意图标签数目17个
英文训练集共1001条 意图标签数目17个
中文训练集共32275条 意图标签数目17个
测试集共1291条
意图标签共18个
分别有['open_collect_music', 'map_control_confirm', 'raise_ac_temperature_little', 'open_ac_mode', 'adjust_ac_windspeed_to_number', 'lower_ac_temperature_little', 'play_collect_music', 'adjust_ac_temperature_to_number', 'navigate_poi', 'map_control_query', 'navigate_landmark_poi', 'collect_music', 'close_car_device', 'open_ac', 'open_car_device', 'view_trans', 'close_ac', 'music_search_artist_song']
(16, 17, 16, 18)
# 统计 label_i 的数目
plt.title("japan train")
sns.countplot(y='意图',data=japan_train)
plt.show()
plt.title("english train")
sns.countplot(y='意图',data=en_train)
plt.show()
plt.title("chinesetrain")
sns.countplot(y='意图',data=zh_train)
plt.show()
1.3 统计Slot标签
# 统计slot标签
SLOT_LIST = set()
SLOT_LIST.add('O')
for idx,rows in japan_train.iterrows():
if rows['槽值1'] is not np.NaN:
slot_name = (rows["意图"] +"-"+ rows["槽值1"].split(":")[0])
SLOT_LIST.add(slot_name)
if rows['槽值2'] is not np.NaN:
slot_name = (rows["意图"] +"-"+ rows["槽值2"].split(":")[0])
SLOT_LIST.add(slot_name)
for idx,rows in en_train.iterrows():
if rows['槽值1'] is not np.NaN:
slot_name = (rows["意图"] +"-"+ rows["槽值1"].split(":")[0])
SLOT_LIST.add(slot_name)
if rows['槽值2'] is not np.NaN:
slot_name = (rows["意图"] +"-"+ rows["槽值2"].split(":")[0])
SLOT_LIST.add(slot_name)
for idx,rows in zh_train.iterrows():
if rows['槽值1'] is not np.NaN:
slot_name = (rows["意图"] +"-"+ rows["槽值1"].split(":")[0])
SLOT_LIST.add(slot_name)
if rows['槽值2'] is not np.NaN:
slot_name = (rows["意图"] +"-"+ rows["槽值2"].split(":")[0])
SLOT_LIST.add(slot_name)
SLOT_LIST = list(set(SLOT_LIST))
SLOT_LIST = sorted(SLOT_LIST)
print(f"intent-slot标签共{
len(SLOT_LIST)}个\n分别是{
SLOT_LIST}")
intent-slot标签共11个
分别是['O', 'adjust_ac_temperature_to_number-offset', 'adjust_ac_windspeed_to_number-offset', 'close_car_device-device', 'music_search_artist_song-singer', 'music_search_artist_song-song', 'navigate_landmark_poi-landmark', 'navigate_landmark_poi-poi', 'navigate_poi-poi', 'open_ac_mode-mode', 'open_car_device-device']
1.4 统计文本长度
japan_train['len'] = [len(i) for i in japan_train["原始文本"]]
plt.title("japan train text length")
sns.distplot(japan_train['len'],bins=10)
plt.show()
en_train['len'] = [len(i.split(" ")) for i in en_train["原始文本"]]
plt.title("english train text length")
sns.distplot(en_train['len'],bins=10)
plt.show()
zh_train['len'] = [len(i) for i in zh_train["原始文本"]]
plt.title("chinese train text length")
sns.distplot(zh_train['len'],bins=10)
plt.show()
EDA结果:
- 在三类训练预料中意图标签极度不平衡
- 原始文本中并不是每一个文本都包含slot标签,slot标签也是不平衡的
- 文本长度的最大值均未超过40,在max_len的选择上不要设置太大,本文选取48作为max_len值,可能并非最优max_len
2 处理数据集
2.1 划分训练集和验证集
- 分割日文训练集和英文训练集作为验证集,剩余日英数据集为训练集(简单融合过所有中文训练集但效果很差,先使用低资源训练集得到Baseline)
- 分割比例8:2
# 使用sklearn的train_test_split划分数据集
intent_train_dataset1,intent_validation_dataset1 = train_test_split(japan_train, test_size=0.2, random_state=seed)
intent_train_dataset2,intent_validation_dataset2 = train_test_split(en_train, test_size=0.2, random_state=seed)
# 整合验证集
intent_validation_dataset = pd.concat([intent_validation_dataset1,intent_validation_dataset2],axis=0)
# 整合训练集
intent_train_dataset = pd.concat([intent_train_dataset1,intent_train_dataset2],axis=0)
# intent_train_dataset = pd.concat([intent_train_dataset,zh_train],axis=0)
intent_train_dataset.shape,intent_validation_dataset.shape
((1601, 6), (402, 6))
2.2 数据预处理和编码
- 意图识别任务的编码较为容易,统一为{‘texts’:[],‘intent_labels’:[]}
- Slot(槽值)识别任务采用简化的BIO编码,对于slot位置编码对应的slot类别,对于非slot位置使用’O’编码,如下所示
'目','O'
'的','O'
'地','O'
'を','O'
'阿','navigate_poi-poi'
'波','navigate_poi-poi'
'加','navigate_poi-poi'
'茂','navigate_poi-poi'
'駅','navigate_poi-poi'
'に','O'
'設','O'
'定','O'
'す','O'
'る','O'
其中navigate_poi-poi
的前半部分由意图组成,后半部分由slot组成,通过"-"连接,即navigate_poi为意图,poi是槽值名,这样设计易于统计所有意图和slot的组合。
baseline后续联合训练方式,因此将意图标签和slot标签合并,处理后的训练数据如下:
{
'words': ['目', '的', '地', 'を', '阿', '波', '加', '茂', '駅', 'に', '設', '定', 'す', 'る'],
'intents': 'navigate_poi',
'slots': ['O', 'O', 'O', 'O', 'navigate_poi-poi', 'navigate_poi-poi', 'navigate_poi-poi', 'navigate_poi-poi', 'navigate_poi-poi', 'O', 'O', 'O', 'O', 'O']
}
# 设置准备拉取的预训练模型名称
# 准备了两个模型
# 1.bert-base-multilingual-cased:BERT多语言预训练模型,cased 区分大小写
# 2.bert-base-multilingual-uncased:BERT多语言预训练模型,uncased 不区分大小写
MODEL_NAME = 'bert-base-multilingual-uncased' # bert-base-multilingual-cased
# 加载tokenizer
tokenizer = BertTokenizer.from_pretrained(MODEL_NAME)
# 给出的标签均为文本,创建label_map构建映射关系
def get_label_map(label_list):
id2label = dict([(idx, label) for idx, label in enumerate(label_list)])
label2id = dict([(label, idx) for idx, label in enumerate(label_list)])
return id2label, label2id
# 意图类型处理成字典
id2intent, intent2id = get_label_map(INTENT_LIST)
# 槽位类型、意图类型和隐藏意图类型处理成字典
id2slot, slot2id = get_label_map(SLOT_LIST)
由于处理中存在两种不同语言的平行预料,在slot标签的构建上略有不同
日语的处理方式与处理中文语料类似,使用检索可以快速构建slot标签
但英语序列标注有其特殊之处。其特殊之处在于,BERT会将复杂的英语单词拆分成多个简单英语单词,进行tokenize.
例如,以"A man waits in the arrivals hall at Heathrow Airport in London."为例子,通过tokenizer后得到如下编码:
ids: [101, 1037, 2158, 18074, 1999, 1996, 25470, 2534, 2012, 9895, 10524, 3199, 1999, 2414, 1012, 102]
type_ids: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
encode_text: ['[CLS]', 'a', 'man', 'waits', 'in', 'the', 'arrivals', 'hall', 'at', 'heath', '##row', 'airport', 'in', 'london', '.', '[SEP]']
可以看到,在句子中单词Heathrow被拆分成了两个token:heath和##row,这是BERT英语预训练模型在tokenize时的特殊之处。
基于上述原因,原来的标注序列:
['a', 'man', 'waits', 'in', 'the', 'arrivals', 'hall', 'at', 'heathrow', 'airport', 'in', 'london', '.']
['O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'B-LOC', 'I-LOC', 'O', 'B-LOC', 'O']
在对英语语料进行序列标注时,应当变成如下序列:
['[CLS]', 'a', 'man', 'waits', 'in', 'the', 'arrivals', 'hall', 'at', 'heath', '##row', 'airport', 'in', 'london', '.', '[SEP]']
['O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'B-LOC', 'B-LOC', 'I-LOC', 'O', 'B-LOC', 'O', 'O']
多了一个B-LOC
和两个O
,也就是说,拆分后的多个简单英语单词的序列标注标签将会跟随原先复杂单词的标签。
参考资料:
山阴少年. NLP(三十七)使用keras-bert实现英语序列标注任务
def en_slot_tokenizer_gen(texts,intents=None,slot_label=None,slot_value=None,modeSlot=True): # 使用tokenizer编码原始文本和slot值 new_texts = list(tokenizer.convert_ids_to_tokens(tokenizer(texts)['input_ids'])) new_slots = np.full(shape=len(new_texts),fill_value='O',dtype='object') # 如果是无slot的文本直接返回编码后的全O标签,有slot则使用新的序列进行搜索生成新slot序列 if modeSlot is False: return new_slots else: new_slot_value = tokenizer.convert_ids_to_tokens(tokenizer(slot_value)['input_ids'])[1:-1] s = new_texts .index(new_slot_value[0]) e = s + len(new_slot_value) new_slots[s:e] = [(intents+"-"+slot_label)] * len(new_slot_value) return new_slots # 根据本地文件格式和上述方式定义数据读取生成器 def read(df,istrain=True): # 是否是训练集,否则仅返回文本,是则分会文本和标签 if istrain: for idx,rows in tqdm(df.iterrows(),total=df.shape[0]): text = rows['原始文本'] intents = rows['意图'] # 使用langid判定文本属于哪一种语言 language = langid.classify(text)[0] if language == 'en': texts = text slots = en_slot_tokenizer_gen(texts,modeSlot=False) else: texts = [i for i in text] slots = np.full(shape=len(texts),fill_value='O',dtype='object') # 排除槽值为空的情况 slot_cols = ['槽值1','槽值2'] if rows['槽值1'] is np.NaN: slot_cols.remove('槽值1') if rows['槽值2'] is np.NaN: slot_cols.remove('槽值2') # 根据槽值字段列表存在的情况对槽值进行处理 if len(slot_cols) > 0: for solt_col in slot_cols: # 获取 槽值标签 和 槽值内容 slot_value = rows[solt_col].split(":")[1] slot_label = rows[solt_col].split(":")[0] # 若是英语则调用tokenizer进行处理,非英文则搜索对应槽值找到起始位置和结束为止生成序列标记 if language == 'en': slots = en_slot_tokenizer_gen(texts,intents,slot_label,slot_value,modeSlot=True) else: s = text.index(slot_value) e = s + len(slot_value) slots_name = (intents+"-"+slot_label) slots[s:e] = [slots_name] * len(slot_value) # 对于非英文语言,需要在首尾为[CLS][SEP]补充添加"O" 标签:
1601连接器