hnsw_retrieval.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. import json
  2. import pickle
  3. import requests
  4. import numpy as np
  5. from fuzzywuzzy import fuzz
  6. from sentence_transformers import util
  7. from pprint import pprint
  8. import config
  9. from formula_process import formula_recognize
  10. from comprehensive_score import Comprehensive_Score
  11. from physical_quantity_extract import physical_quantity_extract
  12. class HNSW():
  13. def __init__(self, data_process, logger=None):
  14. # 配置初始数据
  15. self.mongo_coll = config.mongo_coll
  16. self.vector_dim = config.vector_dim
  17. self.hnsw_retrieve_url = config.hnsw_retrieve_url
  18. self.dim_classify_url = config.dim_classify_url
  19. self.knowledge_tagging_url = config.knowledge_tagging_url
  20. # 日志采集
  21. self.logger = logger
  22. self.log_msg = config.log_msg
  23. # 数据预处理实例化
  24. self.dpp = data_process
  25. # 语义相似度实例化
  26. self.cph_score = Comprehensive_Score(config.dev_mode)
  27. # 难度数值化定义
  28. self.difficulty_transfer = {"容易": 0.2, "较易": 0.4, "一般": 0.6, "较难": 0.8, "困难": 1.0}
  29. # 加载公式处理数据模型(词袋模型/原始向量/原始数据)
  30. with open(config.bow_model_path, "rb") as bm:
  31. self.bow_model = pickle.load(bm)
  32. self.bow_vector = np.load(config.bow_vector_path)
  33. with open(config.formula_data_path, 'r', encoding='utf8', errors='ignore') as f:
  34. self.formula_id_list = json.load(f)
  35. # 图片搜索查重功能
  36. def img_retrieve(self, retrieve_text, post_url, similar, topic_num):
  37. try:
  38. if post_url is not None:
  39. # 日志采集
  40. if self.logger is not None:
  41. self.logger.info(self.log_msg.format(id="图片搜索查重",
  42. type="{}图片搜索查重post".format(topic_num),
  43. message=retrieve_text))
  44. img_dict = dict(img_url=retrieve_text, img_threshold=similar, img_max_num=40)
  45. img_res = requests.post(post_url, json=img_dict, timeout=30).json()
  46. # 日志采集
  47. if self.logger is not None:
  48. self.logger.info(self.log_msg.format(id="图片搜索查重",
  49. type="{}图片搜索查重success".format(topic_num),
  50. message=img_res))
  51. return img_res
  52. except Exception as e:
  53. # 日志采集
  54. if self.logger is not None:
  55. self.logger.error(self.log_msg.format(id="图片搜索查重",
  56. type="{}图片搜索查重error".format(topic_num),
  57. message=retrieve_text))
  58. return []
  59. # 公式搜索查重功能
  60. def formula_retrieve(self, formula_string, similar):
  61. # 调用清洗分词函数
  62. formula_string = '$' + formula_string + '$'
  63. formula_string = self.dpp.content_clear_func(formula_string)
  64. # 公式识别
  65. formula_list = formula_recognize(formula_string)
  66. if len(formula_list) == 0:
  67. return []
  68. # 日志采集
  69. if self.logger is not None:
  70. self.logger.info(self.log_msg.format(id="formula_retrieve",
  71. type=formula_string,
  72. message=formula_list))
  73. try:
  74. # 使用词袋模型计算句向量
  75. bow_vec = self.bow_model.transform([formula_list[0]]).toarray().astype("float32")
  76. # 并行计算余弦相似度
  77. formula_cos = np.array(util.cos_sim(bow_vec, self.bow_vector))
  78. # 获取余弦值大于等于0.8的数据索引
  79. cos_list = np.where(formula_cos[0] >= similar)[0]
  80. except:
  81. return []
  82. if len(cos_list) == 0:
  83. return []
  84. # 根据阈值获取满足条件的题目id
  85. res_list = []
  86. # formula_threshold = similar
  87. formula_threshold = 0.7
  88. for idx in cos_list:
  89. fuzz_score = fuzz.ratio(formula_list[0], self.formula_id_list[idx][0]) / 100
  90. if fuzz_score >= formula_threshold:
  91. # res_list.append([self.formula_id_list[idx][1], fuzz_score])
  92. # 对余弦相似度进行折算
  93. cosine_score = formula_cos[0][idx]
  94. if 0.95 <= cosine_score < 0.98:
  95. cosine_score = cosine_score * 0.98
  96. elif cosine_score < 0.95:
  97. cosine_score = cosine_score * 0.95
  98. # 余弦相似度折算后阈值判断
  99. if cosine_score < similar:
  100. continue
  101. res_list.append([self.formula_id_list[idx][1], int(cosine_score * 100) / 100])
  102. # 根据分数对题目id排序并返回前50个
  103. res_sort_list = sorted(res_list, key=lambda x: x[1], reverse=True)[:80]
  104. formula_res_list = []
  105. fid_set = set()
  106. for ele in res_sort_list:
  107. for fid in ele[0]:
  108. if fid in fid_set:
  109. continue
  110. fid_set.add(fid)
  111. formula_res_list.append([fid, ele[1]])
  112. return formula_res_list[:50]
  113. def api_post(self, post_url, post_data, log_info):
  114. try:
  115. post_result = requests.post(post_url, json=post_data, timeout=10).json()
  116. except Exception as e:
  117. post_result = []
  118. # 日志采集
  119. if self.logger is not None:
  120. self.logger.error(self.log_msg.format(id="{}error".format(log_info[0]),
  121. type="当前题目{}error".format(log_info[0]),
  122. message=log_info[1]))
  123. return post_result
  124. # HNSW查(支持多学科混合查重)
  125. def retrieve(self, retrieve_list, post_url, similar, scale, doc_flag):
  126. """
  127. return:
  128. [
  129. {
  130. 'semantics': [[20232015, 1.0, {'quesType': 1.0, 'knowledge': 1.0, 'physical_scene': 1.0, 'solving_type': 1.0, 'difficulty': 1.0, 'physical_quantity': 1.0}]],
  131. 'text': [[20232015, 0.97]],
  132. 'image': [],
  133. 'label': {'knowledge': ['串并联电路的辨别'], 'physical_scene': ['串并联电路的辨别'], 'solving_type': ['规律理解'], 'difficulty': 0.6, 'physical_quantity': ['电流']},
  134. 'topic_num': 1
  135. },
  136. ...
  137. ]
  138. """
  139. # 计算retrieve_list的vec值
  140. # 调用清洗分词函数和句向量计算函数
  141. sent_vec_list, cont_clear_list = self.dpp(retrieve_list, is_retrieve=True)
  142. # HNSW查重
  143. retrieve_res_list = []
  144. for i,sent_vec in enumerate(sent_vec_list):
  145. # 初始化返回数据类型
  146. # retrieve_value_dict = dict(synthese=[], semantics=[], text=[], image=[])
  147. retrieve_value_dict = dict(semantics=[], text=[], image=[])
  148. # 获取题目序号
  149. topic_num = retrieve_list[i]["topic_num"] if "topic_num" in retrieve_list[i] else 1
  150. # 图片搜索查重功能
  151. if doc_flag is True:
  152. retrieve_value_dict["image"] = self.img_retrieve(retrieve_list[i]["stem"], post_url, similar, topic_num)
  153. else:
  154. retrieve_value_dict["image"] = []
  155. """
  156. 文本相似度特殊处理
  157. """
  158. # 判断句向量维度
  159. if sent_vec.size != self.vector_dim:
  160. retrieve_res_list.append(retrieve_value_dict)
  161. continue
  162. # 调用hnsw接口检索数据
  163. query_labels = self.api_post(self.hnsw_retrieve_url, sent_vec.tolist(), ["HNSW检索", cont_clear_list[i]])
  164. if len(query_labels) == 0:
  165. retrieve_res_list.append(retrieve_value_dict)
  166. continue
  167. # 批量读取数据库
  168. mongo_find_dict = {"id": {"$in": query_labels}}
  169. query_dataset = self.mongo_coll.find(mongo_find_dict)
  170. ####################################### 语义相似度借靠 #######################################
  171. query_data = dict()
  172. ####################################### 语义相似度借靠 #######################################
  173. # 返回大于阈值的结果
  174. for label_data in query_dataset:
  175. if "sentence_vec" not in label_data:
  176. continue
  177. # 计算余弦相似度得分
  178. label_vec = pickle.loads(label_data["sentence_vec"])
  179. if label_vec.size != self.vector_dim:
  180. continue
  181. cosine_score = util.cos_sim(sent_vec, label_vec)[0][0]
  182. # 阈值判断
  183. if cosine_score < similar:
  184. continue
  185. # 计算编辑距离得分
  186. fuzz_score = fuzz.ratio(cont_clear_list[i], label_data["content_clear"]) / 100
  187. # 进行编辑距离得分验证,若小于设定分则过滤
  188. if fuzz_score >= similar:
  189. retrieve_value = [label_data["id"], fuzz_score]
  190. retrieve_value_dict["text"].append(retrieve_value)
  191. ####################################### 语义相似度借靠 #######################################
  192. max_mark_score = 0
  193. if cosine_score >= 0.9 and cosine_score > max_mark_score:
  194. max_mark_score = cosine_score
  195. query_data = label_data
  196. ####################################### 语义相似度借靠 #######################################
  197. """
  198. 语义相似度特殊处理
  199. """
  200. # 标签字典初始化
  201. label_dict = dict() # todo: 可以加一个高相似的替换, 不走模型预测
  202. # 知识点LLM标注
  203. label_dict["knowledge"] = query_data["knowledge"] if query_data else []
  204. ####################################### 知识点标注接口调用 #######################################
  205. # knowledge_post_data = {"sentence": cont_clear_list[i]}
  206. # label_dict["knowledge"] = self.api_post(self.knowledge_tagging_url, knowledge_post_data, ["知识点标注", cont_clear_list[i]])
  207. ####################################### 知识点标注接口调用 #######################################
  208. tagging_id_list = [self.cph_score.knowledge2id[ele] for ele in label_dict["knowledge"] \
  209. if ele in self.cph_score.knowledge2id]
  210. # 题型数据获取
  211. label_dict["quesType"] = retrieve_list[i].get("quesType", "选择题")
  212. # 多维分类api调用
  213. dim_post_data = {"sentence": cont_clear_list[i], "quesType": label_dict["quesType"]}
  214. dim_classify_dict = self.api_post(self.dim_classify_url, dim_post_data, ["多维分类", cont_clear_list[i]])
  215. if len(dim_classify_dict) == 0:
  216. dim_classify_dict = {"solving_type": ["规律理解"], "difficulty": 0.6}
  217. # 求解类型模型分类
  218. label_dict["solving_type"] = dim_classify_dict["solving_type"]
  219. # 难度模型分类
  220. label_dict["difficulty"] = dim_classify_dict["difficulty"]
  221. # 物理量规则提取
  222. label_dict["physical_quantity"] = physical_quantity_extract(cont_clear_list[i])
  223. # LLM标注知识点题目获取题库对应相似知识点题目数据
  224. knowledge_id_list = []
  225. if len(tagging_id_list) > 0:
  226. ####################################### encode_base_value设置 #######################################
  227. # 考试院: 10000, 风向标: 10
  228. encode_base_value = 10000 if config.dev_mode == "ksy" else 10
  229. ####################################### encode_base_value设置 #######################################
  230. for ele in tagging_id_list:
  231. init_id = int(ele / encode_base_value) * encode_base_value
  232. init_id_list = self.cph_score.init_id2max_id.get(str(init_id), [])
  233. knowledge_id_list.extend(init_id_list)
  234. knowledge_query_dataset = None
  235. if len(knowledge_id_list) > 0:
  236. mongo_find_dict = {"knowledge_id": {"$in": knowledge_id_list}}
  237. knowledge_query_dataset = self.mongo_coll.find(mongo_find_dict)
  238. # 返回大于阈值的结果
  239. if knowledge_query_dataset:
  240. # 待查重难度数值转换
  241. if label_dict["difficulty"] in self.difficulty_transfer:
  242. label_dict["difficulty"] = self.difficulty_transfer[label_dict["difficulty"]]
  243. for refer_data in knowledge_query_dataset:
  244. # 题库数据难度数值转换
  245. if refer_data["difficulty"] in self.difficulty_transfer:
  246. refer_data["difficulty"] = self.difficulty_transfer[refer_data["difficulty"]]
  247. sum_score, score_dict = self.cph_score(label_dict, refer_data, scale)
  248. if sum_score < similar:
  249. continue
  250. retrieve_value = [refer_data["id"], sum_score, score_dict]
  251. retrieve_value_dict["semantics"].append(retrieve_value)
  252. # 将组合结果按照score降序排序
  253. retrieve_sort_dict = {k: sorted(value, key=lambda x: x[1], reverse=True)
  254. for k,value in retrieve_value_dict.items()}
  255. # 加入题目序号
  256. retrieve_sort_dict["label"] = label_dict
  257. retrieve_sort_dict["topic_num"] = topic_num
  258. # 以字典形式返回最终查重结果
  259. retrieve_res_list.append(retrieve_sort_dict)
  260. return retrieve_res_list
  261. if __name__ == "__main__":
  262. # 获取mongodb数据
  263. mongo_coll = config.mongo_coll
  264. from data_preprocessing import DataPreProcessing
  265. hnsw = HNSW(DataPreProcessing())
  266. test_data = []
  267. for idx in [201511100736265]:
  268. test_data.append(mongo_coll.find_one({"id": idx}))
  269. res = hnsw.retrieve(test_data, '', 0.8, False)
  270. pprint(res[0]["semantics"])
  271. # # 公式搜索查重功能
  272. # formula_string = "ρ蜡=0.9*10^3Kg/m^3"
  273. # formula_string = "p蜡=0.9*10^3Kq/m^3"
  274. # print(hnsw.formula_retrieve(formula_string, 0.8))