文章目录
- 任务
- 数据
- 流程
- 模型
-
-
- baseline
- 自己写的
-
- 输入输出
- AlbertClassifierModel
-
- 训练细节
-
-
- 参数
- 实验
-
- 在test集输出
-
-
- 规则
- serch_f1
- 线上
-
任务
参加了一个贝壳找房公司在DataFountain上举办的比赛
任务:
本次赛题的任务是:给定app中的交流片段,片段包含一个客户问题以及随后的经纪人若干对话消息,从这些随后的经纪人消息中找出一个是对客户问题的。
这是一个二分类的问题,就是对许多的 (问题,答案)句子组合 进行 预测,如果问题和回答匹配了,就标注为1。
数据
训练集: 6000段对话, 每段对话是一句用户的问题对上多句客服的回答,这些回答里只有一部分是对客户的问题进行直接回答的,被标注为 1 , 其余的标注为0。
数据的格式可以被处理为: [query_id, reply_id,query,reply,label]
例如:
用户的问题是: 靠近沙川路嘛?
客服 回了三句话, 只有 "有一点靠近沙川路"这一条reply 的label是1(因为这句话直接回答了用户的问题), 其余的标注是0
流程
- 数据预处理
- preprocess.py
- 找规则
- 训练
- train.py
- 模型
- model.py
- 采用ALBERT+TextCNN
- 在test集输出
- test.py
模型
baseline
其实在比赛网站的排名榜上,得分第一名的人(是北航校友,已毕业)开源了他的代码
我下载下来看了一下,他是用jupyter notebook 写的,深度学习框架用的是tensorflow
他的模型结构是:
BERT + CNN
他的训练方法:
五折交叉验证,需要训练很久,所以我用它的notebook跑,没有跑出来结果。
自己写的
由于我只用过pytorch和python脚本训练模型,像tensorflow+notebook这样的训练方式我真的不习惯,所以就自己写了一个用pytorch和python脚本训练分类模型的代码。
-
模型来源:
-
ALBERT:海量中文语料上预训练ALBERT模型:比Bert参数更少,效果更好。预训练小模型也能拿下13项NLP任务,ALBERT三大改造登顶GLUE基准,是适合咱们小模型使用的词向量。
-
ALBERT-base在sentence order prediction(SOP,即预测一个句子是不是另一个句子的下一个句子)上可以训练到99%的准确率,其实咱们比赛的这个问题也是一个SOP的问题:
-
TextCNN:文本分类的关键在于准确提炼文档或者句子的中心思想,而提炼中心思想的方法是抽取文档或句子的关键词作为特征,基于这些特征去训练分类器并分类。因为CNN的卷积和池化过程就是一个抽取特征的过程,当我们可以准确抽取关键词的特征时,就能准确的提炼出文档或句子的中心思想。
-
-
自己的模型: ALBERT+TextCNN
输入输出
只看输入输出的话,模型整体是这样的:
输入是一条数据(query,reply),先经过Albert分词器把数据转化成albert要求的形式, 再输进我们自己设计的分类器AlbertClassifierModel,输出是对 输入做二分类,预测出的概率。
AlbertClassifierModel
具体的AlbertClassifierModel结构是这样的:
把 Albert作为词向量,把句子映射为词向量的列表,
然后用TextCNN提取特征,再加上Albert本身的特征向量CLS,
连接之后,就是一个[1,embedding_size + out_channels * len(kernel_size)]维度的向量
输入到 第一个全连接层fc_layer1, 并dropout一次,向量维度不变
再输入到 第二个全连接层fc_layer1,输出为1个值(二分类的概率)
训练细节
参数
参数名 | 默认值 | 作用和意义 |
---|---|---|
max_epoch | 5 | 训练多少个epoch,一个epoch会把训练集所有数据用一遍 |
batch_size | 32 | 一次输入model里的数据有batch_size条 |
lr | 1e-4 | 初始学习率 |
schedule_step | 1 | scheduler,在schedule_step的时候,把学习率乘0.1 |
weight_decay | 1e-6 | 每个epoch学习率减少的大小,保证越靠后的训练lr越小,防止梯度消失 |
seed | 666666 | 随机数种子,为了保证训练过程的可复现 |
dropout | 0.3 | 模型对输入的一些维度进行随机遗忘的比例,为了防止过拟合 |
optim | adam | 优化器种类 |
实验
python train.py --batch_size 32 --lr 1e-4 --weight_decay 0
发现,已经快过拟合了。。
所以从这里面选一个ckpt就行,剩下的看运气了,如果和测试集的分布一样就分高。
在test集输出
规则
在prepocess.py的时候,我们就利用正则表达式查找了一些在query和reply里分布非常奇怪的句子
比如:
在训练集上找含有"NAME"的句子:
在测试上找"NAME":
用了规则兜底,如果不行,还可以人工check,所以命令就是2种选择了,加或者不加规则:
python test.py --ckpt xxx --thres xx --use_rule
python test.py --ckpt xxx --thres xx
serch_f1
因为二分类概率p其实得最后转化为一个整数标记(0或者1)
所以就可以自己定义一个阈值thres,如果 p > thes ,就把句子标注为1,否则标注为0
由于训练的时候我们采用的是BCEWithLogitsLoss
损失函数,它是不需要sigmoid的二分类损失函数,所以就直接把模型的输出和标注做loss计算就行,不需要在训练阶段就确定好阈值thres。
而在valid阶段,可以遍历一个范围,来找出一个最合适的thres,让f1得分最高。
线上
python test.py --ckpt model_epoch4_val0.978.pt --thres 0.15
一开始报错了:
发现是由于kwargs参数保留少了,之前很傻的只保存了’max_input_len’,还自以为是地把所有参数args输入到model里做初始化。
现在终于知道为什么,大家写model的__init__函数要把参数都列出来了,因为ckpt加载的时候,加载出kwargs要全部重新输入到model的__init__函数里面
现在把model的__init__函数改了,重新训练:
python train.py --batch_size 32 --lr 1e-4 --weight_decay 0 --max_epoch 10
选了一个在验证集上f1分数0.97的去test集上输出
最后提交得分是0.7,且加了规则之后得分更低了