療知識(shí)圖譜問(wèn)答系統(tǒng):從Neo4j構(gòu)建到意圖識(shí)別實(shí)戰(zhàn))
簡(jiǎn)介基于Python的醫(yī)療知識(shí)圖譜自動(dòng)問(wèn)答系統(tǒng)源碼包適合對(duì)自然語(yǔ)言處理、知識(shí)圖譜和智慧醫(yī)療感興趣的開(kāi)發(fā)者、研究員及畢業(yè)設(shè)計(jì)選題學(xué)生。項(xiàng)目從數(shù)據(jù)預(yù)處理入手覆蓋醫(yī)學(xué)詞匯、病癥、藥物、治療方法等信息的清洗、去重與標(biāo)準(zhǔn)化進(jìn)而借助Neo4j等圖數(shù)據(jù)庫(kù)將實(shí)體和關(guān)系建模構(gòu)建結(jié)構(gòu)化的醫(yī)療知識(shí)圖譜同時(shí)使用NLTK、spaCy等工具完成問(wèn)句的語(yǔ)義分析、實(shí)體識(shí)別與關(guān)系抽取配合Cypher查詢語(yǔ)句實(shí)現(xiàn)問(wèn)題與圖譜節(jié)點(diǎn)的智能匹配最終通過(guò)Flask、Django搭建直觀的問(wèn)答交互界面形成從數(shù)據(jù)到回答的完整閉環(huán)。壓縮包共26個(gè)文件以8個(gè)py源碼腳本、8個(gè)txt詞典與知識(shí)數(shù)據(jù)、5個(gè)xml工程配置、2個(gè)json規(guī)則文件為主大小15.54MB目錄拆分清晰包含數(shù)據(jù)準(zhǔn)備、圖譜構(gòu)建、問(wèn)題分類、答案搜索等模塊并附帶演示圖片方便對(duì)照運(yùn)行結(jié)果。已有184人學(xué)習(xí)下載可作為從零搭建醫(yī)療問(wèn)答系統(tǒng)的完整參考幫助掌握實(shí)體識(shí)別、意圖分類、圖譜查詢、回答生成等關(guān)鍵技術(shù)也為擴(kuò)展其他垂直領(lǐng)域問(wèn)答提供了可復(fù)用的代碼框架。1. 醫(yī)療知識(shí)圖譜自動(dòng)問(wèn)答系統(tǒng)先搞清楚它解決什么問(wèn)題前臺(tái)護(hù)士每天要回答幾十次“頭疼掛什么科”一套基于 Python 的醫(yī)療知識(shí)圖譜自動(dòng)問(wèn)答系統(tǒng)會(huì)先在知識(shí)圖譜上查“頭痛→神經(jīng)內(nèi)科”這條邊再返回科室和下一步建議。它不是大模型不靠猜答案來(lái)自圖中真實(shí)事實(shí)可解釋、可控、可離線運(yùn)行。這類系統(tǒng)由三塊組成醫(yī)療知識(shí)圖譜構(gòu)建、問(wèn)句解析與意圖識(shí)別、查詢與應(yīng)答。標(biāo)題里的“源碼”就是這條鏈路的完整工程形態(tài)。適合正在入門(mén) python、想用知識(shí)圖譜做項(xiàng)目的開(kāi)發(fā)者也適合醫(yī)療信息化團(tuán)隊(duì)拿來(lái)快速搭一套可演示的問(wèn)答原型。2. 圖譜構(gòu)建從表格到Neo4j決定問(wèn)答上限的一步2.1 一份最小可用的醫(yī)療數(shù)據(jù)與關(guān)系設(shè)計(jì)先解決“數(shù)據(jù)從哪來(lái)”。常見(jiàn)做法是收集三類結(jié)構(gòu)化表格疾病-癥狀、疾病-科室、藥品-適應(yīng)癥。很多公開(kāi)醫(yī)療網(wǎng)站的“疾病百科”“就醫(yī)指南”欄目就是這類知識(shí)抓下來(lái)整理成 CSV。這里有個(gè)誤區(qū)覺(jué)得“數(shù)據(jù)量越大系統(tǒng)越聰明”。實(shí)際上圖庫(kù)問(wèn)答的鏈路不是靠規(guī)模撐起來(lái)的100 個(gè)高頻疾病、300 個(gè)癥狀、30 個(gè)科室、50 種常用藥就能讓系統(tǒng)有內(nèi)容可答關(guān)鍵是覆蓋高頻問(wèn)法和高頻詞。真正要花心思的是關(guān)系設(shè)計(jì)。我第一版會(huì)只保留四條關(guān)系邊關(guān)系類型起點(diǎn)實(shí)體終點(diǎn)實(shí)體示例HAS_SYMPTOM疾病癥狀高血壓 - 頭暈DEPARTMENT疾病科室高血壓 - 心血管內(nèi)科TREATS藥品疾病硝苯地平 - 高血壓CHECKS疾病檢查高血壓 - 血壓監(jiān)測(cè)這張表就是知識(shí)圖譜的骨架。實(shí)體類型也不多Disease、Symptom、Department、Drug、Check后面所有問(wèn)答都圍著這五種節(jié)點(diǎn)轉(zhuǎn)。關(guān)系一旦定下來(lái)別急著加“并發(fā)癥”“相關(guān)疾病”這類邊先把主干跑通。2.2 數(shù)據(jù)清洗與三元組生成pandas先把臟數(shù)據(jù)擋在門(mén)外拿到 CSV 之后第一步不是入庫(kù)是清洗。網(wǎng)頁(yè)復(fù)制出來(lái)的表格經(jīng)常帶首尾空格、全角空格、換行符不清洗就會(huì)在 Neo4j 里形成“高血壓”和“高血壓 ”兩個(gè)節(jié)點(diǎn)問(wèn)答匹配時(shí)時(shí)好時(shí)壞非常折磨人。import pandas as pd def clean_entity_name(value): 統(tǒng)一實(shí)體名去掉首尾空格、全角空格、換行符避免匹配時(shí)翻車(chē) if pd.isna(value): return None return str(value).strip().replace( , ).replace(\u3000, ).replace(\n, ) def build_triples(disease_symptom_file, disease_dept_file): ds pd.read_csv(disease_symptom_file, encodingutf-8-sig) dd pd.read_csv(disease_dept_file, encodingutf-8-sig) triples [] for _, row in ds.iterrows(): disease clean_entity_name(row[疾病]) symptom clean_entity_name(row[癥狀]) if disease and symptom: triples.append((Disease, disease, HAS_SYMPTOM, Symptom, symptom)) for _, row in dd.iterrows(): disease clean_entity_name(row[疾病]) dept clean_entity_name(row[科室]) if disease and dept: triples.append((Disease, disease, DEPARTMENT, Department, dept)) # 跨表去重保持首次出現(xiàn)順序 seen set() unique_triples [] for t in triples: if t not in seen: seen.add(t) unique_triples.append(t) return unique_triples編碼用 utf-8-sig 而不是 utf-8是因?yàn)楹芏鄰?Excel 另存出來(lái)的 CSV 帶 BOM用 utf-8 讀第一列會(huì)多一個(gè)不可見(jiàn)字符實(shí)體名匹配直接失敗。clean_entity_name 是我做圖譜項(xiàng)目必寫(xiě)的第一個(gè)函數(shù)它同時(shí)處理了末尾換行和全角空格。參數(shù)說(shuō)明兩個(gè)入?yún)⒎謩e是疾病-癥狀表、疾病-科室表的路徑返回三元組列表元素結(jié)構(gòu)是起點(diǎn)類型起點(diǎn)名關(guān)系類型終點(diǎn)類型終點(diǎn)名。去重放在最后做因?yàn)橥粋€(gè)“疾病-癥狀”對(duì)可能出現(xiàn)在多張來(lái)源表里跨表去重必須統(tǒng)一處理。2.3 用py2neo批量寫(xiě)入Neo4j先建約束再按關(guān)系分組MERGE入庫(kù)順序比入庫(kù)本身更重要。我一般先建唯一約束再用 UNWIND 批量寫(xiě)入。約束保證實(shí)體名唯一它同時(shí)也是查詢索引問(wèn)答階段的匹配速度靠它兜底。from py2neo import Graph from collections import defaultdict graph Graph(bolt://localhost:7687, auth(neo4j, your_password)) # 先建唯一約束舊版Neo4j用ASSERT語(yǔ)法4.4用REQUIRE graph.run(CREATE CONSTRAINT disease_name IF NOT EXISTS FOR (n:Disease) REQUIRE n.name IS UNIQUE) graph.run(CREATE CONSTRAINT symptom_name IF NOT EXISTS FOR (n:Symptom) REQUIRE n.name IS UNIQUE) graph.run(CREATE CONSTRAINT dept_name IF NOT EXISTS FOR (n:Department) REQUIRE n.name IS UNIQUE) graph.run(CREATE CONSTRAINT drug_name IF NOT EXISTS FOR (n:Drug) REQUIRE n.name IS UNIQUE) graph.run(CREATE CONSTRAINT check_name IF NOT EXISTS FOR (n:Check) REQUIRE n.name IS UNIQUE) def import_triples(triples, batch_size500): # 按關(guān)系類型分組避免一個(gè)事務(wù)里混合多種關(guān)系模板 grouped defaultdict(list) for start_type, start_name, rel_type, end_type, end_name in triples: grouped[(start_type, rel_type, end_type)].append({ start: start_name, end: end_name }) for (start_type, rel_type, end_type), group in grouped.items(): for i in range(0, len(group), batch_size): rows group[i:i batch_size] cypher f UNWIND $rows AS row MERGE (s:{start_type} {{name: row.start}}) MERGE (e:{end_type} {{name: row.end}}) MERGE (s)-[:{rel_type}]-(e) graph.run(cypher, rowsrows) print(f關(guān)系 {rel_type} 已寫(xiě)入 {min(i batch_size, len(group))} / {len(group)} 條)三個(gè) MERGE 不等于三個(gè) CREATE。MERGE 在節(jié)點(diǎn)已存在時(shí)只做匹配不重復(fù)創(chuàng)建配合唯一約束能避免二次導(dǎo)入時(shí)報(bào)錯(cuò)。按關(guān)系類型分組是為了讓每個(gè)事務(wù)里的 Cypher 模板一致UNWIND 一批 500 條既快又容易定位失敗批次。類型名和關(guān)系名我直接拼進(jìn)了 Cypher因?yàn)檫@些值來(lái)自代碼里的固定常量不是用戶輸入。如果哪天數(shù)據(jù)表里混入了外部傳入的關(guān)系名必須先做白名單校驗(yàn)再拼不要直接格式化進(jìn)查詢。batch_size 也是可調(diào)參數(shù)本地 Neo4j 可以調(diào)到 1000遠(yuǎn)程實(shí)例建議 200防止單事務(wù)過(guò)大把內(nèi)存打滿。2.4 本體建模的邊界先少后多別讓關(guān)系成為負(fù)擔(dān)這一步其實(shí)就是知識(shí)圖譜構(gòu)建里最常說(shuō)的本體建模定義實(shí)體類型和關(guān)系類型。醫(yī)療場(chǎng)景有個(gè)反直覺(jué)的經(jīng)驗(yàn)——關(guān)系寧少勿多。常見(jiàn)做法是先覆蓋“癥狀-疾病-科室-藥品”四條主干邊等問(wèn)答跑通、確實(shí)回答不了一些問(wèn)題時(shí)再按需補(bǔ)“并發(fā)癥”“檢查”邊。我在早期項(xiàng)目里一口氣加了七種關(guān)系結(jié)果每種問(wèn)法都要重新設(shè)計(jì) Cypher 模板維護(hù)成本成倍上漲。還有一個(gè)容易被忽略的查詢邊界盡量別用可變長(zhǎng)度路徑比如“-[:HAS_SYMPTOM*1..2]-”。在概念演示里它很漂亮但真實(shí)問(wèn)答時(shí)它會(huì)把多條路徑的答案混在一起系統(tǒng)很難向用戶解釋“這個(gè)答案是怎么推出來(lái)的”。醫(yī)療場(chǎng)景要的是可解釋路徑越短越清晰。3. 問(wèn)答引擎意圖識(shí)別、實(shí)體對(duì)齊與Cypher生成讓機(jī)器聽(tīng)懂人話3.1 五類問(wèn)題的意圖識(shí)別先用規(guī)則模板跑通再談模型自動(dòng)問(wèn)答的“自動(dòng)”第一步是判斷用戶在問(wèn)什么。常見(jiàn)問(wèn)題類型有五類問(wèn)癥狀、問(wèn)科室、問(wèn)藥物、問(wèn)病因、問(wèn)預(yù)防。很多人上來(lái)就想用 BERT 分類模型但手里訓(xùn)練樣本可能只有幾百條效果反而不如規(guī)則模板穩(wěn)定。import re INTENT_MAP { query_symptom: [什么癥狀, 有哪些癥狀, 癥狀表現(xiàn), 會(huì)有什么表現(xiàn)], query_department: [掛什么科, 什么科室, 哪個(gè)科室, 應(yīng)該看什么科], query_drug: [吃什么藥, 用什么藥, 藥物, 怎么用藥], query_cause: [病因, 什么原因, 怎么會(huì)得, 誘發(fā)因素], query_prevent: [怎么預(yù)防, 如何預(yù)防, 預(yù)防措施, 注意事項(xiàng)], } def classify_intent(question: str) - str: for intent, keywords in INTENT_MAP.items(): if any(kw in question for kw in keywords): return intent return query_general沒(méi)有用正則表達(dá)式只做子串匹配原因是醫(yī)療問(wèn)句的變體實(shí)在太多正則寫(xiě)起來(lái)又長(zhǎng)又脆。每類意圖維護(hù)一個(gè)關(guān)鍵詞數(shù)組新增問(wèn)法就加一行字符串代價(jià)最低。query_general 是兜底意圖表示系統(tǒng)沒(méi)聽(tīng)明白后面會(huì)走回退邏輯。這個(gè)函數(shù)是純字符串匹配中文分詞不參與跑得極快。3.2 實(shí)體抽取與別名對(duì)齊jieba加自定義詞典命中“頭疼頭痛”問(wèn)句進(jìn)來(lái)之后要找出里面的疾病名、癥狀名。最常用的做法是 jieba 分詞加自定義詞典。醫(yī)療詞有兩個(gè)特點(diǎn)詞長(zhǎng)不穩(wěn)定“神經(jīng)內(nèi)科”在默認(rèn)詞典里會(huì)被切成“神經(jīng)/內(nèi)科”經(jīng)常會(huì)切錯(cuò)別名多“頭疼”和“頭痛”在知識(shí)圖譜里可能是兩個(gè)寫(xiě)法。import jieba # 自定義詞典提高醫(yī)療實(shí)體分詞穩(wěn)定度 for word in [神經(jīng)內(nèi)科, 心血管內(nèi)科, 高血壓, 糖尿病, 腦梗死, 冠心病]: jieba.add_word(word) # 別名表口語(yǔ)/別名 - 圖譜標(biāo)準(zhǔn)名 ALIAS_MAP { 頭疼: 頭痛, 血壓高: 高血壓, 血糖高: 糖尿病, 腦袋疼: 頭痛, 胸口悶: 胸悶, } def extract_entities(question: str) - list: tokens jieba.lcut(question) hits [] for token in tokens: token ALIAS_MAP.get(token, token) # 先做別名歸一 for entity_type, entity_names in ENTITY_NAMES.items(): if token in entity_names: hits.append((entity_type, token)) break return hitsENTITY_NAMES 是“實(shí)體類型 - 標(biāo)準(zhǔn)名集合”可以直接從圖譜里讀出來(lái)保持和庫(kù)內(nèi)完全一致。這里的關(guān)鍵是把別名映射放在分詞之后、匹配標(biāo)準(zhǔn)名之前。順序錯(cuò)了比如先匹配再歸一結(jié)果就是“頭疼”在標(biāo)準(zhǔn)名集合里查不到直接被丟棄。參數(shù)說(shuō)明jieba.add_word 會(huì)提升該詞在后續(xù)切分中的權(quán)重但它不是百分百?gòu)?qiáng)制極端情況下還是可能切錯(cuò)。更保險(xiǎn)的做法是把自定義詞典寫(xiě)進(jìn) jieba 的 user_dict 文件并把這個(gè)文件放進(jìn)項(xiàng)目倉(cāng)庫(kù)里換環(huán)境時(shí)不用重新寫(xiě)一遍。3.3 相似度兜底分詞沒(méi)命中系統(tǒng)還能不能救回來(lái)分詞畢竟有邊界。用戶說(shuō)“血壓偏高”圖譜里只有“高血壓”jieb 切出來(lái)是“血壓”“偏高”實(shí)體抽取直接撲空。這種時(shí)候要有一個(gè)模糊匹配兜底常見(jiàn)做法是 difflib 的 SequenceMatcher。from difflib import SequenceMatcher def fuzzy_entity_lookup(name: str, entity_list: list, min_ratio: float 0.6): 在實(shí)體名列表里找最相似的候選低于min_ratio則不返回 best_match, best_ratio None, 0.0 for entity in entity_list: ratio SequenceMatcher(None, name, entity).ratio() if ratio best_ratio: best_match, best_ratio entity, ratio return best_match if best_ratio min_ratio else Nonemin_ratio 默認(rèn) 0.6這個(gè)值是血淚經(jīng)驗(yàn)調(diào)出來(lái)的。設(shè)太低比如 0.4“小兒肺炎”會(huì)匹配到“肺炎”給出的答案不準(zhǔn)設(shè)太高比如 0.85別名基本救不回來(lái)。0.6 是在實(shí)體長(zhǎng)度普遍為 2 到 6 個(gè)漢字時(shí)的折中值。要注意“血壓偏高”這句由兩個(gè) token 組成fuzzy_entity_lookup 只能處理單個(gè) token所以我會(huì)先把“血壓偏高”整體拼回去做一次查詢?cè)倏紤] token 級(jí)匹配。順序是完整句子先試一次分詞結(jié)果再試一次最后才是單 token 模糊。這一層兜底的意義是讓系統(tǒng)回答“不確定”時(shí)有個(gè)合理的近似答案而不是直接報(bào)錯(cuò)。醫(yī)療場(chǎng)景寧可給近似條目并提示“為您找到相近的是”也不該空手而歸。3.4 生成Cypher查詢模板拼接與參數(shù)化一個(gè)都不能少意圖和實(shí)體都齊了下一步是把它們翻譯成圖查詢。這里的模板要按意圖和實(shí)體類型同時(shí)匹配比如“高血壓有什么癥狀”和“頭痛掛什么科”兩個(gè)問(wèn)題的起點(diǎn)實(shí)體類型、關(guān)系邊都不一樣。def build_cypher(intent: str, entity_type: str, entity_name: str): if intent query_symptom: cypher ( fMATCH (d:{entity_type} {{name: $name}}) f-[:HAS_SYMPTOM]-(s:Symptom) RETURN s.name AS answer ) elif intent query_department: cypher ( fMATCH (d:{entity_type} {{name: $name}}) f-[:DEPARTMENT]-(dept:Department) RETURN dept.name AS answer ) elif intent query_drug: cypher ( fMATCH (d:{entity_type} {{name: $name}}) f-[:TREATS]-(drug:Drug) RETURN drug.name AS answer ) else: return None, None return cypher, {name: entity_name} def query_answer(cypher: str, params: dict): if not cypher: return [抱歉這個(gè)問(wèn)題我暫時(shí)回答不了] records graph.run(cypher, parametersparams).data() return [record[answer] for record in records] or [知識(shí)圖譜里暫未查到相關(guān)記錄](méi)實(shí)體名通過(guò) $name 參數(shù)傳入而不是直接拼進(jìn) Cypher。參數(shù)化在這里有兩個(gè)作用一是避免實(shí)體名里帶單引號(hào)、雙引號(hào)時(shí)把查詢語(yǔ)法破壞掉二是防止注入雖然問(wèn)答場(chǎng)景風(fēng)險(xiǎn)有限但工程上保留這個(gè)習(xí)慣沒(méi)有壞處。entity_type 依然走白名單拼裝來(lái)源是 2.2 節(jié)三元組里的固定類型不受用戶輸入影響。到這里一個(gè)完整問(wèn)答請(qǐng)求已經(jīng)能夠閉環(huán)問(wèn)句進(jìn)來(lái)識(shí)別意圖抽取實(shí)體拼 Cypher查庫(kù)返回答案列表。完整系統(tǒng)通常會(huì)把 query_answer 包成一個(gè) Flask 接口前端 POST 一句 json 就能拿到答案。接口本身是殼問(wèn)答核心全在前面幾個(gè)函數(shù)里。4. 五個(gè)高頻踩坑記錄現(xiàn)象、原因、解決4.1 “頭疼”查得到“頭痛”查不到現(xiàn)象用戶輸入“頭疼”返回“知識(shí)圖譜里暫未查到相關(guān)記錄”輸入“頭痛”卻有完整答案。原因圖譜標(biāo)準(zhǔn)名只收了“頭痛”問(wèn)句里的“頭疼”沒(méi)進(jìn)別名表實(shí)體抽取階段就丟了。解決先建別名表把口語(yǔ)說(shuō)法、方言、縮寫(xiě)統(tǒng)一映射到標(biāo)準(zhǔn)名映射發(fā)生在實(shí)體匹配之前。別只做兩三個(gè)別名實(shí)際收集時(shí)用搜索詞的聯(lián)想詞去補(bǔ)表比如“血壓高”“血壓偏高”“HBP”都要收斂到“高血壓”。4.2 圖譜越大越慢約束沒(méi)建、索引沒(méi)有現(xiàn)象幾百條數(shù)據(jù)時(shí)秒回到幾千條后查詢明顯變慢有的 Cypher 要跑幾秒鐘。原因?qū)霑r(shí)沒(méi)建唯一約束也沒(méi)建屬性索引Neo4j 只能按 label 掃描所有節(jié)點(diǎn)做 name 匹配。解決導(dǎo)入前執(zhí)行 2.3 節(jié)那幾條 CREATE CONSTRAINT 語(yǔ)句它同時(shí)創(chuàng)建索引。索引要建在查詢的匹配字段上也就是 name 屬性。這個(gè)問(wèn)題在數(shù)據(jù)量過(guò)萬(wàn)后會(huì)更明顯項(xiàng)目初期就要建別等卡了再補(bǔ)。4.3 換臺(tái)電腦跑不起來(lái)py2neo與Neo4j版本不匹配現(xiàn)象代碼在自己機(jī)器上跑得好好的換臺(tái)電腦裝好 Neo4j啟動(dòng) Python 腳本直接報(bào)認(rèn)證失敗或者提示 Graph.run 方法不存在。原因py2neo 4.x 和 5.x 的部分 API 不兼容Neo4j 5.x 對(duì)舊版驅(qū)動(dòng)支持也有限兩個(gè)版本對(duì)不上就會(huì)出這類玄學(xué)問(wèn)題。解決項(xiàng)目根目錄放 requirements.txt鎖死 py2neo 與 neo4j 驅(qū)動(dòng)版本同時(shí)把 Neo4j 版本寫(xiě)進(jìn) README。我一般也會(huì)順手寫(xiě)一個(gè)連接測(cè)試腳本啟動(dòng)后先跑一句 RETURN 1確認(rèn)環(huán)境通了再跑導(dǎo)入和問(wèn)答。4.4 問(wèn)“病因”返回“科室”意圖模板太粗現(xiàn)象用戶問(wèn)“高血壓是什么原因造成的”系統(tǒng)返回“心血管內(nèi)科”。原因INTENT_MAP 里 query_cause 只有“病因”“什么原因”這句恰好沒(méi)觸發(fā)整體落進(jìn)了 query_department 的關(guān)鍵詞“什么”。解決不要只擴(kuò)充關(guān)鍵詞要建一個(gè)意圖自檢集每個(gè)意圖至少準(zhǔn)備 15 個(gè)變體問(wèn)句跑一遍自動(dòng)測(cè)試看哪些問(wèn)句被分錯(cuò)了類。規(guī)則模板的優(yōu)勢(shì)就是修起來(lái)快但也要有測(cè)試集兜住回歸否則改一個(gè)詞可能帶崩另一類問(wèn)題。4.5 導(dǎo)入時(shí)報(bào)ConstraintValidationFailed重復(fù)實(shí)體沒(méi)有先合并現(xiàn)象第二次運(yùn)行導(dǎo)入腳本圖庫(kù)報(bào)“Node already exists with label Disease and property name”。原因唯一約束建立后CREATE 語(yǔ)句試圖新建重名節(jié)點(diǎn)會(huì)直接拋錯(cuò)。解決所有節(jié)點(diǎn)寫(xiě)入統(tǒng)一用 MERGE它存在就匹配、不存在才創(chuàng)建。另外 2.2 節(jié)的三元組去重也不是可有可無(wú)同一對(duì)“疾病-癥狀”可能在多張來(lái)源表里重復(fù)出現(xiàn)導(dǎo)入前去重能減少一半這類報(bào)錯(cuò)。5. 進(jìn)階從能問(wèn)答到答得可靠先過(guò)評(píng)估、歸一化、鏈指三關(guān)5.1 建一份測(cè)試問(wèn)句集算準(zhǔn)召評(píng)價(jià)問(wèn)答系統(tǒng)不能靠拍腦袋。常見(jiàn)做法是準(zhǔn)備 100 條測(cè)試問(wèn)句每條標(biāo)注標(biāo)準(zhǔn)實(shí)體、標(biāo)準(zhǔn)答案然后跑一個(gè)簡(jiǎn)單評(píng)估腳本def evaluate(test_set, answer_func): total_recall total_precision 0.0 n 0 for case in test_set: answers set(answer_func(case[question])) gold set(case[gold_answers]) if not gold: continue total_recall len(answers gold) / len(gold) total_precision (len(answers gold) / len(answers)) if answers else 0 n 1 return total_recall / n, total_precision / n召回率衡量“該給的答案漏沒(méi)漏”精確率衡量“給的答案準(zhǔn)不準(zhǔn)”。醫(yī)療問(wèn)答里我一般更看重召回答不全比答錯(cuò)好補(bǔ)救但精確率低于 0.7 時(shí)就要檢查實(shí)體對(duì)齊和意圖模板了。5.2 術(shù)語(yǔ)歸一化把“血壓高”“HBP”都收斂到“高血壓”別名表做到后期會(huì)成為一張標(biāo)準(zhǔn)術(shù)語(yǔ)表這是醫(yī)療問(wèn)答從“能用”到“好用”的分水嶺。做法是給每個(gè)標(biāo)準(zhǔn)實(shí)體維護(hù)一個(gè)別名數(shù)組在問(wèn)句預(yù)處理階段統(tǒng)一替換。這一步值得提前投入因?yàn)楹罄m(xù)做實(shí)體鏈指、接口拓展都依賴一套穩(wěn)定的標(biāo)準(zhǔn)名。我見(jiàn)過(guò)團(tuán)隊(duì)在這里偷懶結(jié)果換個(gè)數(shù)據(jù)源別名表推倒重來(lái)代價(jià)遠(yuǎn)大于一開(kāi)始就建詞表。5.3 實(shí)體鏈指處理“它”“這個(gè)病”的指代連續(xù)對(duì)話里用戶會(huì)說(shuō)“它”“這個(gè)病”實(shí)體抽取抓不到。簡(jiǎn)單做法是保存上一輪命中的實(shí)體當(dāng)前問(wèn)句出現(xiàn)代詞時(shí)替換回去def resolve_coreference(question, last_entity): if not last_entity: return question return question.replace(它, last_entity).replace(這個(gè)病, last_entity)只適用于單輪指代。多輪對(duì)話的指代消解要上模型醫(yī)療場(chǎng)景里結(jié)論必須保守指代不確定時(shí)寧可多問(wèn)一句“您說(shuō)的是哪個(gè)病”也不要自作聰明。我最早搭這個(gè)系統(tǒng)時(shí)數(shù)據(jù)表里有一行疾病名后面帶了個(gè)換行符結(jié)果實(shí)體匹配時(shí)好時(shí)壞查了一晚上才發(fā)現(xiàn)是清洗環(huán)節(jié)漏了換行符。后來(lái)我把所有實(shí)體名統(tǒng)一走 clean_entity_name順序也固定為先洗數(shù)據(jù)、再建約束、最后寫(xiě)問(wèn)答邏輯。這個(gè)順序值得你記下來(lái)。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取