1 Introduction
1.1 大赛概况
受疫情催化影响,近一年内全球电商及在线零售行业进入高速发展期。作为线上交易场景的重要购买入口,搜索行为背后是强烈的购买意愿,电商搜索质量的高低将直接决定最终的成交结果,因此在AI时代,如何通过构建智能搜索能力提升线上GMV转化成为了众多电商开发者的重要研究课题。本次比赛由阿里云天池平台和问天引擎联合举办,诚邀社会各界开发者参与竞赛,共建AI未来。
1.2 赛程安排
本次大赛分为报名组队、初赛、复赛和决赛三个阶段,具体安排和要求如下:
- 报名组队:3月2日—4月10日
- 初赛阶段:3月2日—4月13日
- 复赛阶段:4月18日—5月18日
- 决赛答辩:6月1日
1.2.1 初赛阶段(2022年3月2日-4月13日)
选手报名成功后,参赛队伍通过天池平台下载数据,本地调试算法,在线提交结果。初赛提供训练数据集,供参赛选手训练算法模型;同时提供测试数据集,供参赛选手提交评测结果,参与排名。
初赛时间为3月2日—4月13日,系统每天提供3次提交机会,系统进行实时评测并返回成绩,排行榜每小时进行更新,按照评测指标从高到低排序。排行榜将选择参赛队伍在本阶段的历史最优成绩进行排名展示。
初赛淘汰:2022年4月13日中午11:59:59,初赛一阶段未产出成绩队伍,将被取消复赛参赛资格。
初赛结束,初赛排名前100名的参赛队伍将进入复赛,复赛名单将在 4月15日18点 前公布。
1.2.2 复赛阶段(2022年4月18日-5月18日)
复赛时间为4月18日—5月18日,系统每天提供3次提交机会,系统进行实时评测并返回成绩,排行榜每小时进行更新,按照评测指标从高到低排序。排行榜将选择参赛队伍在本阶段的历史最优成绩进行排名展示。
复赛提交截止时间5月18日中午11:59:59(选手需预估出上传时间,在中午11:59:59提交入口关闭前完成上传)
本阶段内,选手需保证最后提交的是最优模型对应的完整端到端代码(包含数据处理和模型训练等)并能运行复现最优成绩。复赛结束后,该阶段最优成绩对应提交的镜像将直接用于代码审核,如最优成绩对应的镜像代码不是完整代码运行得出,将会直接淘汰,因此如果最后阶段出现无法复现的最优成绩可在复赛提交结束前联系组委会协助删除最优记录,复赛结束后不再受理。
榜单将在复赛截止后公布。复赛结束后,组委会将对排行榜TOP 10参赛队伍进行最优提交成绩的模型和完整代码审核,选手需保证提交最优模型对应完整端到端代码(包含数据处理和模型训练等)且能运行复现最优成绩,不接受随机成绩。如最优成绩对应的镜像代码不是完整代码运行得出,将会直接淘汰。对于未提交、复现未成功或审核不通过的队伍,将取消决赛资格和比赛奖励。
最终审核通过的前5名参赛队伍晋级决赛,决赛名单将在 5月23日18点 前公布。
1.2.3 决赛答辩(2022年6月1日)
入围团队需要在5月30日上午11:59:59前提交答辩PPT,并在线下决赛前一天参与决赛彩排完成设备调试。线下决赛具体地址将在复赛结束后公布。
决赛评分参考:复赛榜单、代码质量和答辩。
答辩需要准备答辩材料,包括答辩PPT(中英文均可)、参赛总结、算法核心。本次赛事决赛入围团队的最终得分将由复赛成绩、决赛答辩成绩加权得出,评分权重为:复赛成绩占80%,决赛答辩成绩占20%。
大赛最终获奖名单将在 6月3日18点 前公布。
1.3 比赛内容
本次题目围绕电商领域搜索算法,开发者们可以通过基于阿里巴巴集团自研的高性能分布式搜索引擎问天引擎(提供高工程性能的电商智能搜索平台),可以快速迭代搜索算法,无需自主建设检索全链路环境。
本次评测的数据来自于 淘宝搜索
真实的业务场景,其中整个搜索商品集合按照商品的类别随机抽样保证了数据的多样性,搜索
Query
和相关的商品来自点击行为日志并通过
模型+人工确认
的方式完成校验保证了训练和测试数据的准确性。
比赛形式分为初赛和复赛两部分,分别从向量召回角度和精排模型角度让选手比拼算法模型。
初赛
提供HA3环境,让选手PK向量召回模型的效果,选手拿到100万全量Doc和10万对Query-Doc相关训练集,自行训练向量召回模型。选手每次提交的内容为100万全量Doc通过模型转换的embedding(固定维度,如128)以及测试集1000条Query转换的embedding。我们通过回流数据,建向量索引,查询测试,给出评测指标(MRR@10,正确Doc排的位置越靠前分越高)。
复赛
对于进入到复赛的选手开放精排模型的PK,选手需要在PAI上按照我们要求的模型格式训练精排模型。选手每次提交的内容除了初赛的Doc和Query的embedding,还包括训练好的精排模型。我们通过回流数据,建向量索引,查询测试(该阶段会做超时限制,防止选手无限制扩大模型复杂度),给出评测指标。
1.4 比赛数据
corpus.tsv
- 介绍:语料库,从淘宝商品搜索的标题数据随机抽取doc,量级约100万。
- 格式:doc_id从1开始编号的,title是是商品标题。
train.query.txt
- 介绍:训练集的query,训练集量级为10万。
- 格式:query_id从1开始编号,query是搜索日志中抽取的查询词。
qrels.train.tsv
- 介绍:训练集的query与doc对应关系,训练集量级为10万。
- 格式:query_id和doc_id。数据来自于搜索点击日志,人工标注query和doc之间具备高相关性,训练集用来训练模型。
dev.query.txt
- 介绍:测试集的query,测试集量级为1000。
- 格式:query_id和query,训练集id从1开始编号,测试集id从200001开始编号,query是搜索日志中抽取的查询词。
Note:比赛数据文件列之间的分隔符均为
tab
符(
1.5 结构提交
1.5.1 初赛
选手上传数据格式:评测数据必须包括
doc_embedding
,query_embedding
两个文件,文件名必须固定,文件打包为 .tar.gz
格式的压缩包
1 | tar zcvf foo.tar.gz doc_embedding query_embedding |
注意:请严格遵守下面要求的文件内容和格式,才能顺利得到结果,提交前也可以使用比赛提供的数据校验脚本检查通过后再进行打包提交。
脚本使用方式:将脚本data_check.py与待提交文件 doc_embedding
和 query_embedding
放在相同目录,执行:
1 | python data_check.py |
doc_embedding:语料库
embedding
,100万语料库通过选手训练的向量召回模型转化后的向量,维度限制128维。格式:doc_id embedding
query_embedding:测试集embedding,1000条测试集query通过选手训练的向量召回模型转化后的向量,维度限制128维。
格式:query_id embedding
1.5.2 复赛
- doc_embedding:语料库embedding,100万语料库通过选手训练的向量召回模型转化后的向量,维度限制128维。
- query_embedding:测试集embedding,1000条测试集query通过选手训练的向量召回模型转化后的向量,维度限制128维。
- rank_model:精排相关性模型目录,TF SavedModel格式,大小限制1GB以内,模型结构不限
- corpus_index:语料库精排模型输入id序列,100万语料库通过选手训练的精排相关性模型转换后的输入id序列,维度限制128维
- query_index:测试集精排模型输入id序列,1000条测试集query通过选手训练的精排相关性模型转换后的输入id序列,维度限制128维
1.6 评价指标
本次比赛采用MRR指标来评测选手基于HA3构建搜索系统的检索效果:
\[ MRR = \frac{1}{Q} \sum_1^{|Q|} \frac{1}{rank_i} \]
其中Q代表所有测试集(1000条query),rank_i代表第i条测试query对应的相关doc在搜索系统返回中的位置。对于第一条query的相关doc在选手的系统中排在第一位,该测试query的MRR值为1;排在第二位,则MRR值为0.5,最终指标为全部测试query MRR值的平均数。
具体到本次比赛,采用MRR@10作为最终评测指标,即如果测试query相关doc不在top 10,则MRR值为0。
2. Data processing
2.1 Setup
1 | import numpy as np |
2.2 Load data
corpus
- 介绍:语料库,从淘宝商品搜索的标题数据随机抽取doc,量级约100万。
- 格式:doc_id从1开始编号的,title是是商品标题。
1 | if os.path.exists(dict_corpus_dir): |
Results:
doc_id | title | |
---|---|---|
0 | 1 | 铂盛弹盖文艺保温杯学生男女情侣车载时尚英文锁扣不锈钢真空水杯 |
1 | 2 | 可爱虎子华为荣耀X30i手机壳荣耀x30防摔全包镜头honorx30max液态硅胶虎年情侣女... |
2 | 3 | 190色素色亚麻棉平纹布料 衬衫裙服装定制手工绣花面料 汇典亚麻 |
3 | 4 | 松尼合金木工开孔器实木门开锁孔木板圆形打空神器定位打孔钻头 |
4 | 5 | 微钩绿蝴蝶材料包非成品 赠送视频组装教程 需自备钩针染料 |
1 | corpus.shape |
Results:
(1001500, 2)
train_query_dir
- 介绍:训练集的query,训练集量级为10万。
- 格式:query_id从1开始编号,query是搜索日志中抽取的查询词。
1 | if os.path.exists(dict_train_query_dir): |
Result:
query_id | query | |
---|---|---|
0 | 1 | 美赞臣亲舒一段 |
1 | 2 | 慱朗手动料理机 |
2 | 3 | 電力貓 |
3 | 4 | 掏夹缝工具 |
4 | 5 | 飞推vip |
1 | train_query.shape |
Results:
(100000, 2)
qrels.train.tsv
- 介绍:训练集的query与doc对应关系,训练集量级为10万。
- 格式:query_id和doc_id。数据来自于搜索点击日志,人工标注query和doc之间具备高相关性,训练集用来训练模型。
1 | if os.path.exists(dict_qrels_train_dir): |
Results:
query_id | doc_id | |
---|---|---|
0 | 1 | 679139 |
1 | 2 | 35343 |
2 | 3 | 781652 |
3 | 4 | 557516 |
4 | 5 | 588014 |
1 | qrels_train.shape |
Results:
(100000, 2)
dev.query.txt
- 介绍:测试集的query,测试集量级为1000。
- 格式:query_id和query,训练集id从1开始编号,测试集id从200001开始编号,query是搜索日志中抽取的查询词。
1 | if os.path.exists(dict_dev_query_dir): |
Results
query_id | query | |
---|---|---|
0 | 200001 | 甲黄酸阿怕替尼片 |
1 | 200002 | 索泰zbox |
2 | 200003 | kfc游戏机 |
3 | 200004 | bunny成兔粮 |
4 | 200005 | 铁线威灵仙 |
1 | dev_query.shape |
Results:
(1000, 2)
3 Text-mining
3.1 Text preprocessing
3.1.1 cut words
1 | import jieba |
Results:
'甲 黄酸 阿怕 替尼片'
1 | def title_cut(x): |
3.1.2 Word2Vec
1 | from gensim.models import Word2Vec |
Results:
[('佳儿', 0.8830252885818481),
('美素', 0.8764686584472656),
('HMO', 0.8655505180358887),
('雅培', 0.8550017476081848),
('适度', 0.8514788746833801),
('蓝臻', 0.8505593538284302),
('白金版', 0.849611759185791),
('版美素', 0.8368095755577087),
('Friso', 0.8177567720413208),
('Enfamil', 0.8171215057373047)]
1 | model.wv.index_to_key[:10] |
Results:
[' ', '新款', '女', '/', '2021', '-', '加厚', '儿童', '秋冬', '外套']
1 | train_w2v_ids = [[model.wv.key_to_index[xx] for xx in x] for x in train_title] |
3.1.3 IDF
1 | from sklearn.feature_extraction.text import TfidfVectorizer |
1 | vocab = idf.get_feature_names() |
Results:
[[534608, 259662, 242520], [385727, 418567, 387725, 406257, 420718]]
1 | corpus_idf = idf.transform(corpus_title) |
1 | idf.idf_, len(idf.idf_) |
Results:
(array([13.12042488, 2.46291458, 8.5771301 , ..., 14.21903717,
14.21903717, 14.21903717]),
640558)
1 | # 将不重要的词语进行过滤 |
1 | drop_token_ids = [model.wv.key_to_index[x] for x in drop_token] |
3.2 无监督Word2Vec 直接构造embeding
1 | def unsuper_w2c_encoding(s, pooling="max"): |
1 | unsuper_w2c_encoding(train_w2v_ids[0]) |
Results:
array([ 0.00627435, 0.00999549, 0.02740174, 0.404633 , 0.4252403 ,
0.23775053, -0.00301973, -0.040391 , 0.12731566, 0.17506774,
0.25548676, -0.02362359, 0.26719984, -0.02683249, 0.07950898,
0.220341 , -0.03015471, -0.00539176, 0.0019598 , -0.00798864,
0.48653737, 0.36375955, 0.00815388, -0.04465045, -0.00626526,
0.19315946, 0.020267 , 0.5370557 , 0.22179876, -0.02791146,
-0.02321572, 0.07029994, -0.05289224, 0.04565464, 0.13785234,
0.20623907, 0.13043119, 0.13404372, 0.03903083, 0.0843261 ,
0.12350243, 0.45837042, -0.02492538, -0.01868924, 0.19881272,
0.18007484, -0.01738105, -0.03000462, 0.20649071, 0.1869723 ,
0.42692277, 0.531309 , 0.06245843, 0.79199183, 0.2779064 ,
0.13139981, 0.39747572, 0.02227024, -0.0458317 , -0.02028935,
-0.02558809, -0.03027141, 0.00923416, 0.01233742, 0.58877105,
-0.01235619, 0.12105425, 0.5268592 , 0.05424006, -0.03668537,
-0.02789669, -0.03252351, 0.02342302, -0.00386677, 0.01726603,
0.3063016 , 0.04099182, 0.02319455, -0.01743875, 0.26839563,
-0.02042119, 0.04482168, 0.1912046 , 0.44685388, 0.56942856,
0.1663401 , 0.43730047, -0.01347114, 0.02290028, 0.32279697,
0.12649736, -0.05318116, 0.12012497, -0.02839875, 0.06174724,
0.0259997 , -0.01499749, -0.06226162, 0.16565983, -0.00632856,
0.0447054 , -0.04246325, 0.11062343, 0.46360213, 0.02929302,
0.11677497, 0.00865052, -0.03610176, 0.16167475, -0.03535878,
0.15723507, -0.01752737, 0.02870348, -0.00388006, 0.21380839,
0.50670934, -0.01522826, 0.16151543, 0.2793154 , 0.15158144,
-0.05517713, -0.01663058, 0.06395861, 0.03442291, 0.03111118,
-0.0300983 , -0.04598322, 0.08044875], dtype=float32)
1 | len(corpus_w2v_ids) |
Results:
1001500
1 | from tqdm import tqdm_notebook |
Results:
0%| | 0/1001500 [00:00<?, ?it/s]
0%| | 0/100000 [00:00<?, ?it/s]
0%| | 0/1000 [00:00<?, ?it/s]
- 初步探索
1 | from sklearn.preprocessing import normalize |
1 | mrr = [] |
0%| | 0/99 [00:00<?, ?it/s]
['美赞臣', '亲舒', '一段'] 领券满减】美赞臣安婴儿A+亲舒 婴儿奶粉1段850克 0-12个月宝宝 [0.73458592]
['现货', '加拿大', '美赞臣', '1', '段', 'EnfamilA', '+', '一段', 'DHA', '奶粉', '765', '克', '超值', '装金装']
1 | np.mean(mrr) |
Results:
0.00013095861707700367
1 | dir_query_embedding = data_dir + 'query_embedding' |
1 | !tar zcvf foo.tar.gz doc_embedding query_embedding |
3.3 Sentence-bert
3.3.1 Negative sample
1 | keyword_corpus_idxs, keyword_idxs = corpus_idf.nonzero() |
Results:
1 | vocab = idf.get_feature_names() |
1 | corpus_idf.shape, max(keyword_corpus_idxs), max(keyword_idxs) |
Results:
1 | ((1001500, 640558), 1001499, 640515) |
1 | dir_train_neg_piar = data_dir + 'train_neg_piar.txt' |
Results:
1 | [[987810], |
1 | idx_keyword_idf = idf.idf_[train_ids[idx - 1]] |
1 | corpus.loc[negative_idx] |
3.3.2 Construct train dataset
1 | train_query.shape[0] |
Results:
1 | 100000 |
1 | from sentence_transformers import InputExample, SentenceTransformer |
Results:
1 | ['<InputExample> label: 1.0, texts: 美赞臣亲舒一段; 领券满减】美赞臣安婴儿A+亲舒 婴儿奶粉1段850克 0-12个月宝宝', |
1 | len(train_examples) |
Results:
1 | 1995000 |
3.3.3 Construct Validation dataset
1 | from sentence_transformers import evaluation |
Results:
1 | 0%| | 0/1001 [00:00<?, ?it/s] |
1 | train_query.shape[0] - 1000, train_query.shape[0] + 1 |
Results:
1 | (99000, 100001) |
1 | idx = 11 |
Results:
1 | (5, 30, 1) |
3.3.4 Sentence BERT
1 | from sentence_transformers import SentenceTransformer, models, util |
Results:
1 | 21000 |
1 | from torch.utils.data import DataLoader |
Results:
1 | Epoch: 0%| | 0/2 [00:00<?, ?it/s] |
load and save
1
2
3
4
5
6
7# save
model.save('./sentence-bert')
# load
from sentence_transformers import SentenceTransformer
model = SentenceTransformer('./sentence-bert/')
3.3.5 Validation & Submission
1 | query_len = train_query.shape[0] |
Results:
1 | SentenceTransformer( |
1 | from sklearn.preprocessing import normalize |
Results:
1 | (1000, 128) (9976, 128) |
1 | cos_sim = util.cos_sim(query_embeddings, corpus_embeddings) |
Results:
1 | 美赞臣亲舒一段 HUJIAN苹果华为手机通用智能手表女运动手环血压心率蓝牙电话拍照多功能定位电子手表学生潮流情侣运动手环 领券满减】美赞臣安婴儿A+亲舒 婴儿奶粉1段850克 0-12个月宝宝 |
3.3.6 Submission
1 | model.eval() |
Results:
SentenceTransformer(
(0): Transformer({'max_seq_length': 50, 'do_lower_case': False}) with Transformer model: BertModel
(1): Pooling({'word_embedding_dimension': 768, 'pooling_mode_cls_token': False, 'pooling_mode_mean_tokens': True, 'pooling_mode_max_tokens': False, 'pooling_mode_mean_sqrt_len_tokens': False})
(2): Dense({'in_features': 768, 'out_features': 128, 'bias': True, 'activation_function': 'torch.nn.modules.activation.Tanh'})
)
1 | query_len = dev_query.shape[0] |
1 | from sklearn.preprocessing import normalize |
Results:
(1000, 128) (1001500, 128)
1 | # result_dir = './results/' |
核验生成的数据是否合规
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67import math
def is_number(s):
if s != s.strip():
return False
try:
f = float(s)
if math.isnan(f) or math.isinf(f):
return False
return True
except ValueError:
return False
# def data_check(file, file_type="doc"):
""" check if a file is UTF8 without BOM,
doc_embedding index starts with 1,
query_embedding index starts with 200001,
the dimension of the embedding is 128.
"""
erro_count = []
error_embeding = []
single_error_embedding = []
for file, file_type in zip(['query_embedding', 'doc_embedding'], ['query', 'doc']):
# file, file_type = "query_embedding", "query"
# file, file_type = "doc_embedding", "doc"
count = 1
id_set = set()
with open(file) as f:
for line in f:
sp_line = line.strip('\n').split("\t")
if len(sp_line) != 2:
print("[Error] Please check your line. The line should be two parts, i.e. index \t embedding")
print("line number: ", count)
index, embedding = sp_line
if not is_number(index):
print("[Error] Please check your id. The id should be int without other char")
print("line number: ", count)
id_set.add(int(index))
embedding_list = embedding.split(',')
if len(embedding_list) != 128:
print("[Error] Please check the dimension of embedding. The dimension is not 128")
print("line number: ", count)
for i, emb in enumerate(embedding_list):
if not is_number(emb):
print("[Error] Please check your embedding. The embedding should be float without other char")
print("line number: ", count)
erro_count.append([index, i])
error_embeding.append(embedding_list)
single_error_embedding.append(emb)
count += 1
if file_type == "doc":
# 1001501
for i in range(1, test_size+1):
if i not in id_set:
print("[Error] The index[{}] of doc_embedding is not found. Please check it.".format(i))
elif file_type == "query":
for i in range(200001, 201001):
if i not in id_set:
print("[Error] The index[{}] of query_embedding is not found. Please check it.".format(i))
print("Check done!\n")Results:
Check done! Check done!
压缩
1
!tar zcvf foo.tar.gz doc_embedding query_embedding
Results:
doc_embedding query_embedding