完全なコード: https://aistudio.baidu.com/aistudio/projectdetail/5194830
サブタスク 2: 解答チェック
タスクの概要
サブタスク 1 に含まれる回答抽出プロセスは、主に回答フラグメントと検索クエリの間の意味的な相関関係に依存しますが、回答フラグメント自体の正確性と信頼性は保証できません。したがって、回答抽出後は回答の検証方法を設計し、抽出された複数の回答断片の中から最も社会的認知度が高い信頼性の高い回答を選択して最終的に表示する必要があります。検索質問 q とそれに対応するドキュメント セット D が与えられた場合、サブタスク 2 は、ドキュメントに含まれる回答の一貫性に基づいてすべてのドキュメントをクラスタリングし、各クエリでユーザーが最も認識している回答を含むドキュメント セットを取得して、深いインテリジェンスを確保します。質問応答システムの最終的な回答の信頼性。
タスク定義
検索質問セット Q が与えられると、質問 q ごとに Web ページ文書コレクション Dq が検索され、このタスクでは参加システムがそこに含まれる回答をクラスタリングする必要があります。クラスタリング結果内の同じサブセットに属する回答は、一貫したビューと同様のセマンティクスを持つ必要があり、異なるサブセットに属する回答は、異なるビューまたは矛盾するセマンティクスを持つ必要があります。各コレクションについて、その回答の受け入れ度を、異なるドキュメントに含まれる回答の数として定義し (同じドキュメント内の回答が繰り返し言及されることを避けるため)、参加システムは回答コレクションに対応するドキュメント コレクションを返すように要求します。最も高い支持率を獲得しました。
参加者には、このタスクを答えの意味論的推論タスクに変換することをお勧めします (他の合理的な解決策を自由に選択できます)。検索クエリが与えられ、2 つの応答フラグメント ai、aj について、ai と aj がクエリに関連する応答と同じセマンティクスを持つか、含意関係がある場合、ai と aj は同じ応答セマンティクスを持つと言われ、次のようになります。 ai と aj がクエリに関連する回答の意味論と何の関係もない場合、または矛盾する関係がある場合、ai と aj は回答の意味論に一貫性がないため、2 つに分割する必要があると言われます。異なる回答セット。
データセット
このタスクによって提供されるデータは、同じクエリに対するさまざまな回答に対する意味論的推論タスクの完了をサポートします。トレーニング セットと検証セットの両方には、検索質問セット Q、Web ページ ドキュメント セット D、回答セット A、および同じクエリの下での回答ペア間の意味的一貫性関係 (支持、中立、反対) が含まれます。詳細なデータ形式については、以下のデータサンプルを参照してください。テスト中に実際の深いインテリジェントな質問応答シーンをシミュレートするには、テスト セットには回答セット A が提供されておらず、出場者はサブタスク 1 で設計されたシステムを使用して、意味的一貫性の計算を実行する前に回答抽出作業を完了する必要があります。最終参加システムは、最も高い受け入れ度をもつ回答セットに対応するドキュメント セットを返す必要があります。
トレーニング セットには、意味的一貫性がマークされた回答ペアの約 200,000 セットが含まれます。検証セットには、一貫性マークが付けられた回答ペアの約 10,000 セットが含まれます。最も認識された回答を取得するために、トレーニング セットと検証セットの両方に回答の一貫性をマークできます。セットと対応するドキュメント コレクション。テスト セットはクエリとドキュメント コレクションのみを提供します。データの主な特徴は次のとおりです。
一般に文書と回答の長さが長く、混乱を招く情報が多く、意味計算が困難です。
回答セット内に複雑な一貫性関係が存在する可能性があります。
数据样例
问题q:备孕偶尔喝冰的可以吗
篇章d1:备孕能吃冷的食物吗 炎热的夏天让很多人都觉得闷热...,下面一起来看看吧! 备孕能吃冷的食物吗 在中医养生中,女性体质属阴,不可以贪凉。吃了过多寒凉、生冷的食物后,会消耗阳气,导致寒邪内生,侵害子宫。另外,宫寒是肾阳虚的表现,不会直接导致不孕。但宫寒会引起妇科疾病,所以也不可不防。因此处于备孕期的女性最好不要吃冷的食物。 备孕食谱有哪些 ...
答案a1:在中医养生中,女性体质属阴,不可以贪凉。吃了过多寒凉、生冷的食物后,会消耗阳气,导致寒邪内生,侵害子宫。另外,宫寒是肾阳虚的表现,不会直接导致不孕。但宫寒会引起妇科疾病,所以也不可不防。因此处于备孕期的女性最好不要吃冷的食物。
篇章d2:病情分析:备孕通常不能喝冰饮料,避免影响胎儿健康。患者正处于备孕准备阶段,男性和女性患者都需要注意饮食不要太辛辣和刺激,不推荐冷冻和冷饮。...
答案a2:备孕通常不能喝冰饮料,避免影响胎儿健康。
篇章d3:备孕期间能喝冰水?备孕期间能喝冰水吗:这个应该不会有影响的 在线咨询...
答案a3:这个应该不会有影响的
答案对<a1, a2>一致性:支持; 答案对<a1, a3>一致性:反对; 答案对<a2, a3>一致性:反对
答案聚类结果:{a1, a2},其认可度=2; {a3},其认可度=1; 认可度最高答案集合为{a1, a2},所属的文档集合为{d1, d2}。
データが示している
train/dev/test で始まるファイルは、それぞれトレーニング、開発、テスト セット データです。
1. xxx_query_doc.json は、提供されたクエリといくつかのドキュメントです
。各クエリは json 形式で保存されます。最初のレベルには、クエリとドキュメントの 2 つのフィールドが含まれます。ドキュメントはリストです。各項目は、タイトル、URL、doc_text を含むドキュメントです。 doc_id の 4 つのフィールド、doc_text はドキュメントのテキスト、doc_id はドキュメントの ID 番号です。
2. xxx_answer_nli_data.tsv は、回答関係の注釈データです。
各行には、クエリ、url1、回答 1、url2、回答 2、ラベルのタブで区切られた 6 つの列が含まれています。ラベル 1 は、2 つの回答が相互に
サポートしており、一貫したセマンティクス (0 ) は、2 つの回答が中立的ではない、または関係に反していることを意味します。
データは、モデルをトレーニングし、回答間の関係を判断するために使用でき、最終タスクをサポートするために使用されます。
テスト セットは、データのこの部分が含まれています
3、xxx_label.tsv是答案检验任务所对应标注
每个query一行,每行包含两列,用制表符tab分割,第一列为query,第二列为认可度最高答案所属的文档集合,使用英文逗号连接的doc_id
测试集不包含该部分数据
数据加载与分析
train_query_doc=pd.read_json('data_task2/train_query_doc.json',lines=True)
答案检验任务所对应标注数据
# 案检验任务所对应标注
train_label=pd.read_table('data_task2/train_label.tsv')
train_label.columns=['query','doc_ids']
train_label.shape
思路1:基于无监督算法SinglePass对相似文档聚类
对query检索的文档直接进行聚类,选取相似性比较高的文档
import numpy as np
from gensim import corpora, models, matutils
class SingelPassClusterTfidf():
'''
1.利用tfidf vec计算cossim
'''
def tfidf_vec(self, corpus, pivot=10, slope=0.25):
dictionary = corpora.Dictionary(corpus) # 形成词典映射
self.dict_size = len(dictionary)
# print('dictionary size:{}'.format(len(dictionary)))
corpus = [dictionary.doc2bow(text) for text in corpus] # 词的向量表示
tfidf = models.TfidfModel(corpus, pivot=pivot, slope=slope)
corpus_tfidf = tfidf[corpus]
return corpus_tfidf
def get_max_similarity(self, cluster_cores, vector):
max_value = 0
max_index = -1
for k, core in cluster_cores.items():
similarity = matutils.cossim(vector, core)
if similarity > max_value:
max_value = similarity
max_index = k
return max_index, max_value
def single_pass(self, corpus_vec, corpus, theta):
clusters = {}
cluster_cores = {}
cluster_text = {}
num_topic = 0
cnt = 0
for vector, text in zip(corpus_vec, corpus):
if num_topic == 0:
clusters.setdefault(num_topic, []).append(vector)
cluster_cores[num_topic] = vector
cluster_text.setdefault(num_topic, []).append(text)
num_topic += 1
else:
max_index, max_value = self.get_max_similarity(cluster_cores, vector)
if max_value > theta:
clusters[max_index].append(vector)
text_matrix = matutils.corpus2dense(clusters[max_index], num_terms=self.dict_size,
num_docs=len(clusters[max_index])).T # 稀疏转稠密
core = np.mean(text_matrix, axis=0) # 更新簇中心
core = matutils.any2sparse(core) # 将稠密向量core转为稀疏向量
cluster_cores[max_index] = core
cluster_text[max_index].append(text)
else: # 创建一个新簇
clusters.setdefault(num_topic, []).append(vector)
cluster_cores[num_topic] = vector
cluster_text.setdefault(num_topic, []).append(text)
num_topic += 1
cnt += 1
if cnt % 100 == 0:
print('processing {}...'.format(cnt))
return clusters, cluster_text
def fit_transform(self, corpus, raw_data, theta=0.6):
tfidf_vec = self.tfidf_vec(corpus) # tfidf_vec是稀疏向量
clusters, cluster_text = self.single_pass(tfidf_vec, raw_data, theta)
return clusters, cluster_text
class ClusterTfidf:
def __init__(self):
self.clustor = SingelPassClusterTfidf()
return
"""聚类主函数"""
def cluster(self, corpus, text2index, theta=0.6):
clusters, cluster_text = self.clustor.fit_transform(corpus, text2index, theta)
return clusters, cluster_text
import jieba
import json
import collections
handler_tfidf = ClusterTfidf()
class SinglePassCluster(object):
"""初始化"""
def __init__(self):
pass
"""读取文件数据"""
def load_data(self, filepath):
datas = []
with open(filepath, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if not line:
continue
datas.append(line)
return datas
"""加载文档,并进行转换"""
def load_docs(self, docs):
corpus = [list(jieba.cut(s['title']+s['doc_text'])) for s in docs]
doc_ids = [s['doc_id'] for s in docs]
index2corpus = dict()
for index, line in zip(doc_ids,docs):
index2corpus[index] = line
text2index = list(index2corpus.keys())
# print('docs total size:{}'.format(len(text2index)))
return text2index, index2corpus, corpus
"""保存聚类结果"""
def save_cluster(self, method, index2corpus, cluster_text, cluster_path):
clusterTopic_list = sorted(cluster_text.items(), key=lambda x: len(x[1]), reverse=True)
# print(clusterTopic_list)
with open(cluster_path + '/cluster_%s.json' % method, 'w+', encoding='utf-8') as save_obj:
for k in clusterTopic_list:
data = dict()
data["cluster_id"] = k[0]
data["cluster_nums"] = len(k[1])
data["cluster_docs"] = [{"doc_id": index, "doc_content": index2corpus.get(value)} for index, value in
enumerate(k[1], start=1)]
json_obj = json.dumps(data, ensure_ascii=False)
save_obj.write(json_obj)
save_obj.write('\n')
"""聚类运行主控函数"""
def cluster(self, docs,method="doc2vec", theta=0.6):
# docs = self.load_data(self.train_corpus_filepath)
text2index, index2corpus, corpus = self.load_docs(docs)
# print("loaded %s samples...." % len(docs))
if method == "tfidf":
clusters, cluster_text = handler_tfidf.cluster(corpus, text2index, theta)
# self.save_cluster(method, index2corpus, cluster_text, cluster_path)
return clusters, cluster_text
else:
clusters, cluster_text = handler_docvec.cluster(corpus, text2index, theta)
return clusters, cluster_text
# self.save_cluster(method, index2corpus, cluster_text, cluster_path)
return
预测结果提交
submit_file=open('subtask2_test_pred.txt','w',encoding='utf-8')
for idx,row in test_query_doc.iterrows():
method = "tfidf"
theta = 0.4
handler = SinglePassCluster()
clusters, cluster_text=handler.cluster(row['docs'],method=method, theta=theta)
# 第一个query d1,d2,d4,d8
# 第二个query d0,d1,d3
similar_docs=[]
for key,value in cluster_text.items():
if len(value)>1:
similar_docs.extend(value)
submit_file.write(row['query']+'\t'+','.join(similar_docs)+'\n')
submit_file.close()
思路2:基于任务1抽取答案进行语义推理
思路二主要是:我们先通过答案关系标注数据(xxxx_nli)数据,训练出一个答案语义一致性推断模型,然后利用任务一堆docs里面的query和doc进行答案抽取,最后判断具有答案的文档的答案之间的相似性,将相似性大于一定值的文档放在一块即可
由于时间问题,目前训练比较慢,后续有分数继续更新提交部分代码
答案语义推理模型
直接当做二分类任务,用于训练集nli数据量比较大,我们可以进行采样进行训练
构建训练集
def concat_text(row):
return str(row['answer1']) + '[SEP]' + row['answer2']
# print(weight)
train = pd.read_table('data_task2/train_answer_nli_data.tsv',header=None)
train.columns=['query','url1','answer1','url2','answer2','label']
train.fillna('', inplace=True)
train['text'] = train.apply(lambda row: concat_text(row), axis=1)
模型训练
import os
import random
from functools import partial
from sklearn.utils.class_weight import compute_class_weight
import numpy as np
import paddle
import paddle as P
import paddle.nn.functional as F
import paddlenlp as ppnlp #===抱抱脸的transformers
import pandas as pd
from paddle.io import Dataset
from paddlenlp.data import Stack, Tuple, Pad
from paddlenlp.datasets import MapDataset
from paddlenlp.transformers import LinearDecayWithWarmup
from sklearn.model_selection import StratifiedKFold
from tqdm import tqdm
import numpy as np
import paddle.fluid as fluid
import paddle.nn as nn
# =============================== 初始化 ========================
class Config:
text_col = 'text'
target_col = 'label'
# 最大长度大小
max_len = 256 # len(text) or toeknizer:256覆盖95%
# 模型运行批处理大小
batch_size = 32
target_size = 2
seed = 71
n_fold = 5
# 训练过程中的最大学习率
learning_rate = 5e-5
# 训练轮次
epochs = 3 # 3
# 学习率预热比例
warmup_proportion = 0.1
# 权重衰减系数,类似模型正则项策略,避免模型过拟合
weight_decay = 0.01
model_name = "ernie-gram-zh"
print_freq = 100
def seed_torch(seed=42):
random.seed(seed)
os.environ['PYTHONHASHSEED'] = str(seed)
np.random.seed(seed)
CFG = Config()
seed_torch(seed=CFG.seed)
# y = train[CFG.target_col]
# class_weight = 'balanced'
# classes = train[CFG.target_col].unique() # 标签类别
# weight = compute_class_weight(class_weight=class_weight,classes= classes, y=y)
# CV split:5折 StratifiedKFold 分层采样
folds = train.copy()
Fold = StratifiedKFold(n_splits=CFG.n_fold, shuffle=True, random_state=CFG.seed)
for n, (train_index, val_index) in enumerate(Fold.split(folds, folds[CFG.target_col])):
folds.loc[val_index, 'fold'] = int(n)
folds['fold'] = folds['fold'].astype(int)
# ====================================== 数据集以及转换函数==============================
# Torch
class CustomDataset(Dataset):
def __init__(self, df):
self.data = df.values.tolist()
self.texts = df[CFG.text_col]
self.labels = df[CFG.target_col]
def __len__(self):
return len(self.texts)
def __getitem__(self, idx):
"""
索引数据
:param idx:
:return:
"""
text = str(self.texts[idx])
label = self.labels[idx]
example = {'text': text, 'label': label}
return example
def convert_example(example, tokenizer, max_seq_length=512, is_test=False):
"""
创建Bert输入
::
0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1
| first sequence | second sequence |
Returns:
input_ids(obj:`list[int]`): The list of token ids.
token_type_ids(obj: `list[int]`): List of sequence pair mask.
label(obj:`numpy.array`, data type of int64, optional): The input label if not is_test.
"""
encoded_inputs = tokenizer(text=example["text"], max_seq_len=max_seq_length)
input_ids = encoded_inputs["input_ids"]
token_type_ids = encoded_inputs["token_type_ids"]
if not is_test:
label = np.array([example["label"]], dtype="int64")
return input_ids, token_type_ids, label
else:
return input_ids, token_type_ids
def create_dataloader(dataset,
mode='train',
batch_size=1,
batchify_fn=None,
trans_fn=None):
if trans_fn:
dataset = dataset.map(trans_fn)
shuffle = True if mode == 'train' else False
if mode == 'train':
batch_sampler = paddle.io.DistributedBatchSampler(
dataset, batch_size=batch_size, shuffle=shuffle)
else:
batch_sampler = paddle.io.BatchSampler(
dataset, batch_size=batch_size, shuffle=shuffle)
return paddle.io.DataLoader(
dataset=dataset,
batch_sampler=batch_sampler,
collate_fn=batchify_fn,
return_list=True)
# tokenizer = ppnlp.transformers.ErnieTokenizer.from_pretrained(CFG.model_name)
tokenizer = ppnlp.transformers.ErnieGramTokenizer.from_pretrained(CFG.model_name)
trans_func = partial(
convert_example,
tokenizer=tokenizer,
max_seq_length=CFG.max_len)
batchify_fn = lambda samples, fn=Tuple(
Pad(axis=0, pad_val=tokenizer.pad_token_id), # input
Pad(axis=0, pad_val=tokenizer.pad_token_type_id), # segment
Stack(dtype="int64") # label
): [data for data in fn(samples)]
# ====================================== 训练、验证与预测函数 ==============================
@paddle.no_grad()
def evaluate(model, criterion, metric, data_loader):
"""
验证函数
"""
model.eval()
metric.reset()
losses = []
for batch in data_loader:
input_ids, token_type_ids, labels = batch
logits = model(input_ids, token_type_ids)
loss = criterion(logits, labels)
losses.append(loss.numpy())
correct = metric.compute(logits, labels)
metric.update(correct)
accu = metric.accumulate()
print("eval loss: %.5f, accu: %.5f" % (np.mean(losses), accu))
model.train()
metric.reset()
return accu
def train():
# ==================================== 交叉验证训练 ==========================
for fold in range(5):
print(f"===============training fold_nth:{fold + 1}======================")
trn_idx = folds[folds['fold'] != fold].index
val_idx = folds[folds['fold'] == fold].index
train_folds = folds.loc[trn_idx].reset_index(drop=True)
valid_folds = folds.loc[val_idx].reset_index(drop=True)
train_dataset = CustomDataset(train_folds)
train_ds = MapDataset(train_dataset)
dev_dataset = CustomDataset(valid_folds)
dev_ds = MapDataset(dev_dataset)
train_data_loader = create_dataloader(
train_ds,
mode='train',
batch_size=CFG.batch_size,
batchify_fn=batchify_fn,
trans_fn=trans_func)
dev_data_loader = create_dataloader(
dev_ds,
mode='dev',
batch_size=CFG.batch_size,
batchify_fn=batchify_fn,
trans_fn=trans_func)
model = ppnlp.transformers.ErnieGramForSequenceClassification.from_pretrained(CFG.model_name,
num_classes=25)
num_training_steps = len(train_data_loader) * CFG.epochs
lr_scheduler = LinearDecayWithWarmup(CFG.learning_rate, num_training_steps, CFG.warmup_proportion)
optimizer = paddle.optimizer.AdamW(
learning_rate=lr_scheduler,
parameters=model.parameters(),
weight_decay=CFG.weight_decay,
apply_decay_param_fun=lambda x: x in [
p.name for n, p in model.named_parameters()
if not any(nd in n for nd in ["bias", "norm"])
])
criterion = paddle.nn.loss.CrossEntropyLoss()
metric = paddle.metric.Accuracy()
global_step = 0
best_val_acc = 0
for epoch in range(1, CFG.epochs + 1):
for step, batch in enumerate(train_data_loader, start=1):
input_ids, segment_ids, labels = batch
logits = model(input_ids, segment_ids)
# probs_ = paddle.to_tensor(logits, dtype="float64")
loss = criterion(logits, labels)
probs = F.softmax(logits, axis=1)
correct = metric.compute(probs, labels)
metric.update(correct)
acc = metric.accumulate()
global_step += 1
if global_step % CFG.print_freq == 0:
print("global step %d, epoch: %d, batch: %d, loss: %.5f, acc: %.5f" % (
global_step, epoch, step, loss, acc))
loss.backward()
optimizer.step()
lr_scheduler.step()
optimizer.clear_grad()
acc = evaluate(model, criterion, metric, dev_data_loader)
if acc > best_val_acc:
best_val_acc = acc
P.save(model.state_dict(), f'{CFG.model_name}_fold{fold}.bin')
print('Best Val acc %.5f' % best_val_acc)
del model
if fold>0:
break# 训练一折
if __name__ == '__main__':
train()
预测结果提交
model = ppnlp.transformers.ErnieGramForSequenceClassification.from_pretrained(CFG.model_name,num_classes=2)
model.load_dict(P.load('ernie-gram-zh_fold0.bin'))
submit_file = open('subtask2_test_pred.txt', 'w', encoding='utf-8')
querys = qa_task2['query'].unique()
for query in querys:
# print(query)
group = qa_task2[qa_task2['query'] == query]
group=group[group['answer']!='NoAnswer'].reset_index(drop=True)
group = group.sort_values(by=['query', 'score'], ascending=False).reset_index(drop=True)
group['doc_id'] = group['doc_id'].apply(lambda x: x.split('_')[-1])
# print(group)
similar_docs = []
# 添加第一个文档,作为基准答案
top_text = group['answer'][0]
similar_docs.append(group['doc_id'][0])
texts = [{'text':top_text+'[SEP]'+text} for text in group['answer'][1:]]
candidate_docs = [ doc_id for doc_id in group['doc_id'][1:]]
pred = predict(model,texts, tokenizer, 16)
preds=list(pred[:,1])
print(len(texts),len(pred),len(candidate_docs))
# print(pred)
for doc_id,prob in zip(candidate_docs,preds):
# print(prob)
if prob >0.2:
similar_docs.append(doc_id)
submit_file.write(query+'\t'+','.join(similar_docs)+'\n')
del group,query
# break
submit_file.close()