療知識圖譜問答系統(tǒng):從圖譜構(gòu)建到Neo4j查詢的完整鏈路解析)
從接觸醫(yī)療知識圖譜問答系統(tǒng)這個方向開始我陸陸續(xù)續(xù)看過不少開源項目但這個倉庫給我的印象特別深。它沒有堆砌復(fù)雜的模型而是用一套完整但門檻不高的工程鏈路把醫(yī)學(xué)知識結(jié)構(gòu)化、自然語言問句理解、圖數(shù)據(jù)庫查詢這三件事串了起來。這篇文章就是我對 QASystemOnMedicalKG-master 的探索記錄包含代碼閱讀思路、知識圖譜構(gòu)建細(xì)節(jié)、問答鏈路拆解、復(fù)現(xiàn)時踩過的坑以及我基于這套系統(tǒng)做的改進(jìn)實驗。如果你想快速入門醫(yī)療垂直領(lǐng)域的問答系統(tǒng)或者正在糾結(jié)知識圖譜到底怎么落地這篇記錄應(yīng)該能給你一個比較完整的參考。我先把話說在前面這個項目不是那種開箱即用、放上生產(chǎn)環(huán)境就能直接服役的成品它是很好的教學(xué)樣板和學(xué)習(xí)骨架。你需要理解它的設(shè)計邏輯再根據(jù)業(yè)務(wù)數(shù)據(jù)去做二次改造。下文我會按照我自己的閱讀順序來展開——先看整體架構(gòu)再看圖譜構(gòu)建然后是問答引擎核心最后聊查詢性能和踩坑經(jīng)驗。1. 先摸清楚項目到底在做什么三大模塊與一條主鏈路1.1 從目錄結(jié)構(gòu)反推系統(tǒng)架構(gòu)拿到一個開源項目我一般不會急著去跑代碼而是先看目錄結(jié)構(gòu)和入口腳本。這套系統(tǒng)最直觀的劃分是三個部分?jǐn)?shù)據(jù)準(zhǔn)備、圖譜構(gòu)建與導(dǎo)入、問答服務(wù)。數(shù)據(jù)準(zhǔn)備負(fù)責(zé)把原始的醫(yī)學(xué)資料整理成結(jié)構(gòu)化三元組圖譜構(gòu)建部分負(fù)責(zé)把這些三元組清洗、去重、關(guān)聯(lián)最終導(dǎo)入圖數(shù)據(jù)庫問答服務(wù)則接收用戶輸入的中文問句完成意圖識別、實體抽取、查詢語句生成和答案返回。這種分層非常清晰也符合大多數(shù)知識圖譜問答項目的基本套路。我建議你在看任何類似項目時都先問自己一個問題這個項目的數(shù)據(jù)源頭是什么數(shù)據(jù)是怎么變成圖的用戶輸入是怎么被路由到圖查詢的把這三個問題的答案找出來整個項目的主干脈絡(luò)就清楚了。1.2 數(shù)據(jù)是醫(yī)療問答系統(tǒng)的血液醫(yī)療知識圖譜問答系統(tǒng)最核心的資產(chǎn)不是算法而是數(shù)據(jù)。這個項目里的數(shù)據(jù)主要由疾病、癥狀、藥物、食物、檢查項目等實體類型以及它們之間的語義關(guān)系組成。比如“高血壓”和“頭暈”之間是“表現(xiàn)為”的關(guān)系“糖尿病”和“胰島素”之間是“推薦用藥”的關(guān)系。為什么要用知識圖譜而不是傳統(tǒng)的表格因為醫(yī)療知識本身是網(wǎng)狀結(jié)構(gòu)。一個疾病關(guān)聯(lián)多種癥狀一種癥狀又可能指向多種疾病一種藥物有適用癥也可能有禁忌癥。這種多對多、跨層級、帶語義標(biāo)簽的關(guān)系用二維表表達(dá)非常吃力但用圖來表達(dá)就非常自然。圖數(shù)據(jù)庫里節(jié)點是實體邊是關(guān)系用戶可以順著某條路徑去檢索“從疾病出發(fā)經(jīng)過癥狀節(jié)點再到達(dá)相關(guān)檢查項目”這類多跳問題。這個項目把這一整套流程都串了起來。我在探索時的體會是它的價值不在某個單點算法有多強而在讓你看到一條完整的、可以被復(fù)刻的落地鏈路。這也是我推薦從它入手的原因。2. 知識圖譜構(gòu)建疾病、癥狀、藥物之間如何被結(jié)構(gòu)化2.1 實體類型與關(guān)系類型的設(shè)計先聊實體類型。這套系統(tǒng)把醫(yī)療領(lǐng)域信息抽象成六個左右的實體類別包括疾病、癥狀、藥物、食物、檢查項目、身體部位等。每種實體在圖中都有對應(yīng)的節(jié)點標(biāo)簽節(jié)點屬性一般是名稱、別名、所屬科室、常見程度等。關(guān)系類型則圍繞疾病展開比如疾病和癥狀之間的“表現(xiàn)為”、疾病和藥物之間的“推薦用藥”、疾病和檢查項目之間的“適合檢查”、疾病和食物之間的“宜吃”和“忌吃”等。這個設(shè)計看起來不復(fù)雜但它是后續(xù)所有問答邏輯的基礎(chǔ)。因為問答系統(tǒng)生成查詢語句時本質(zhì)上就是把用戶問句里的實體和意圖映射為圖上的節(jié)點和關(guān)系。比如用戶問“感冒應(yīng)該吃什么藥”系統(tǒng)要做兩件事識別出“感冒”這個疾病實體再判斷出意圖是“推薦用藥”然后生成一條從疾病節(jié)點到藥物節(jié)點的查詢路徑。如果關(guān)系類型設(shè)計得不夠明確這一步就會非常難受。我特別想強調(diào)命名規(guī)范的重要性。這個項目里關(guān)系類型大多采用統(tǒng)一的動詞短語比如“HAS_SYMPTOM”“RECOMMEND_DRUG”。我建議你在自己的項目里也保持這種清晰的命名習(xí)慣因為在寫問答模板時這些關(guān)系名會被直接拼進(jìn)查詢語句命名越一致模板維護(hù)成本越低。2.2 數(shù)據(jù)清洗與導(dǎo)入Neo4j的細(xì)節(jié)圖譜構(gòu)建里最花時間的往往不是設(shè)計而是清洗。原始數(shù)據(jù)可能來自醫(yī)療百科、結(jié)構(gòu)化表格或人工整理基本都會包含重復(fù)實體、同義名稱、空屬性、亂碼字符等問題。比如同一個藥物在不同數(shù)據(jù)源里可能寫作“阿司匹林”和“乙酰水楊酸”如果不做歸一化圖譜里就會出現(xiàn)兩個孤立的節(jié)點查詢時用戶說哪個都只能查到一半。我當(dāng)時復(fù)現(xiàn)這個項目時數(shù)據(jù)處理部分主要做了三類操作。第一是去重按實體名稱歸一化后合并重復(fù)節(jié)點。第二是補全關(guān)系只保留兩個節(jié)點都存在的三元組避免產(chǎn)生懸掛節(jié)點。第三是字段清洗把科室、藥品劑量等屬性中的空格、換行、特殊符號去掉統(tǒng)一格式。導(dǎo)入Neo4j這一步也有講究。這個項目的數(shù)據(jù)規(guī)模不大常規(guī)做法是用Cypher的LOAD CSV或者直接通過Python驅(qū)動批量寫入。批量寫入時要注意把節(jié)點先創(chuàng)建完再創(chuàng)建邊否則會出現(xiàn)“關(guān)系引用了不存在的節(jié)點”這類錯誤。如果數(shù)據(jù)量更大比如幾十萬以上節(jié)點就得考慮使用Neo4j Admin Import這樣的離線導(dǎo)入工具把數(shù)據(jù)轉(zhuǎn)成CSV頭文件再導(dǎo)入速度快好幾個數(shù)量級。3. 問答引擎核心鏈路從自然語言到Cypher查詢3.1 中文問句的識別與實體鏈接問答鏈路是整個系統(tǒng)最讓人興奮的部分。用戶輸入是一句口語化的中文問句比如“高血壓病人平時吃什么降壓藥比較好”而系統(tǒng)內(nèi)部需要把它轉(zhuǎn)成一條可執(zhí)行的圖查詢。這個轉(zhuǎn)換過程大體分為兩步先抽取實體再判斷意圖。實體抽取這一層項目采用了詞典與分詞工具結(jié)合的方式。思路是準(zhǔn)備一個包含疾病名、癥狀名、藥物名的自定義詞典在分詞階段就盡量讓專業(yè)術(shù)語完整切分。比如“高血壓”這個詞如果按普通分詞可能被切出“血壓”有了詞典約束才能把它作為一個完整實體返回。我實測下來這個方案對常見的疾病名、藥物名覆蓋效果不錯遇到不認(rèn)識的專有名詞就會失效所以后期又補充了基于規(guī)則的回退機制當(dāng)詞典匹配不到時嘗試從原文中提取包含“疾病”“藥物”等后綴詞的片段再和圖譜里的節(jié)點模糊匹配。實體鏈接則是把抽取出的實體名映射到圖譜中的具體節(jié)點。這里有個隱藏問題一個實體可能有多個名字。我在改進(jìn)時加了一張同義詞映射表把凡是節(jié)點屬性中包含別名的表述都指向主節(jié)點例如“高血壓”和“essential hypertension”這樣的中外文表述。就這一個改動讓問答命中率提升了不止一點點。3.2 模板解析的多個分支意圖識別的作用是決定用戶到底要問什么。這個項目把意圖劃分為若干類常見的包括疾病癥狀查詢、疾病治療用藥查詢、疾病宜吃忌吃查詢、疾病檢查項目查詢、科室查詢等。每個意圖類型對應(yīng)至少一個查詢模板模板里預(yù)留實體槽位比如“{疾病}吃什么藥”對應(yīng)“推薦用藥”。模板匹配的具體思路是把問句和預(yù)定義好的句式規(guī)則做比對。這里不是靠深度學(xué)習(xí)模型而是靠規(guī)則加關(guān)鍵詞權(quán)重。比如句子中出現(xiàn)了“癥狀”“表現(xiàn)”“什么感覺”等詞系統(tǒng)判斷意圖偏向癥狀查詢出現(xiàn)“吃什么藥”“治療”“用藥”等詞則偏向用藥查詢。這種做法的優(yōu)點是實現(xiàn)簡單、完全可控缺點是覆蓋不全換一種沒見過的問法就可能誤判。我在自己的改進(jìn)實驗里給每個意圖類型都增加了正例和反例句子通過小規(guī)模分類模型做增強效果比純規(guī)則穩(wěn)定了一些。3.3 一個完整問答示例我拿“糖尿病有哪些常見并發(fā)癥”這個問句走了一遍完整鏈路。分詞階段自定義詞典把“糖尿病”和“并發(fā)癥”都識別為詞條實體抽取階段系統(tǒng)從圖譜中找到疾病節(jié)點“糖尿病”意圖判斷階段句子中出現(xiàn)“并發(fā)癥”這個關(guān)鍵詞系統(tǒng)把它歸類到并發(fā)癥查詢模板緊接著模板實例化生成一條查詢語句從糖尿病節(jié)點出發(fā)沿著表示并發(fā)關(guān)系的邊找到相關(guān)聯(lián)的疾病節(jié)點返回這些節(jié)點的名稱。這條查詢在數(shù)據(jù)庫里執(zhí)行后返回的結(jié)果可能是“糖尿病腎病”“糖尿病視網(wǎng)膜病變”等。系統(tǒng)再把節(jié)點名稱包裝成一句自然語言答案返回給用戶。整個過程從輸入到響應(yīng)在本地環(huán)境上大概只需要幾百毫秒。這個速度對于教學(xué)演示完全夠用也說明在數(shù)據(jù)量可控時規(guī)則加圖查詢的方案遠(yuǎn)比想象中高效。4. 圖數(shù)據(jù)庫的選型觀察與查詢性能體驗4.1 為什么不能直接用關(guān)系型數(shù)據(jù)庫很多人會問醫(yī)療數(shù)據(jù)實體不算多用MySQL也能存為什么非要引入圖數(shù)據(jù)庫我自己探索后的理解是圖數(shù)據(jù)庫的核心優(yōu)勢不在基礎(chǔ)查詢而在多跳關(guān)聯(lián)和路徑檢索。醫(yī)療領(lǐng)域最常問的問題之一就是“這種癥狀可能是哪些疾病引起的”這個查詢本質(zhì)上是從癥狀節(jié)點出發(fā)反查所有連到它的疾病節(jié)點可能還要繼續(xù)跳到對應(yīng)的檢查項目或用藥建議。這種查詢?nèi)绻藐P(guān)系表實現(xiàn)通常需要設(shè)計多張關(guān)聯(lián)表寫一串JOIN語句而且關(guān)系層級越深SQL越復(fù)雜。在圖數(shù)據(jù)庫里這只是一個沿邊遍歷的操作。這套項目使用的是圖數(shù)據(jù)庫領(lǐng)域最主流的Neo4j它的查詢語言Cypher對路徑遍歷的表達(dá)非常直接比如匹配一條關(guān)系鏈就是簡單地描述節(jié)點和關(guān)系的連接模式。我個人的觀察是對于關(guān)系類型固定、查詢模式清晰的垂直領(lǐng)域圖數(shù)據(jù)庫的思維模型天然更匹配。它逼迫你把業(yè)務(wù)關(guān)系建模成圖這個建模過程本身就是對領(lǐng)域知識的一次梳理。4.2 索引、參數(shù)化與查詢延遲實際跑通問答之后我開始關(guān)注性能問題。在這個項目的小規(guī)模圖譜上直接查詢幾乎沒有壓力但不代表可以忽略索引。醫(yī)療圖譜雖然在單個實體類型上節(jié)點數(shù)量有限但如果以后擴(kuò)展到幾十萬實體、上百萬關(guān)系沒有索引的查詢性能會迅速惡化。我在驗證時給疾病名、癥狀名、藥物名的名稱屬性都加了索引。加索引前某些模糊匹配查詢能明顯感覺到延遲加索引后即便是全庫掃描類的匹配也順滑很多。這個經(jīng)驗可以通用到所有圖數(shù)據(jù)庫項目先確認(rèn)哪些實體屬性會被當(dāng)成查詢?nèi)肟谠籴槍@些屬性建立索引千萬不要等用戶來吐槽慢。另外我還建議查詢語句里盡量使用參數(shù)化寫法不要把所有實體值硬拼到查詢字符串里。參數(shù)化不僅能讓數(shù)據(jù)庫做執(zhí)行計劃緩存更重要的是能避免查詢注入和特殊字符引發(fā)的解析錯誤。這一點很多做知識圖譜問答的初學(xué)者容易忽略覺得數(shù)據(jù)是自己導(dǎo)進(jìn)去的沒必要防注入。但等到實體名里出現(xiàn)引號、括號、特殊符號時你就知道參數(shù)化有多重要了。5. 復(fù)現(xiàn)過程中踩過的坑與排查思路5.1 CSV導(dǎo)入時報錯的根因第一次按文檔復(fù)現(xiàn)時我在數(shù)據(jù)導(dǎo)入環(huán)節(jié)就卡住了。現(xiàn)象是CSV文件里明明有數(shù)據(jù)但導(dǎo)入報錯說找不到字段或者節(jié)點沖突。排查鏈路從文件編碼開始最后定位到兩個問題一個是文件是含BOM的UTF-8編碼解析時把首列列名帶上了不可見字符另一個是同一實體在CSV里出現(xiàn)了多行重復(fù)創(chuàng)建后觸發(fā)了唯一性沖突。這兩個問題在真實項目中非常典型。我的解決方法是先統(tǒng)一文件編碼為無BOM的UTF-8再在寫入前對實體名做一次歸一化校驗保證同一主鍵只寫入一次。如果你想避開同類問題這里有個實用經(jīng)驗寫導(dǎo)入腳本之前先單獨把數(shù)據(jù)讀出來看一眼列名和數(shù)據(jù)類型不要默認(rèn)源文件干凈。5.2 分詞與實體匹配失敗問答鏈路里我遇到最影響體驗的問題是分詞。比如“阿司匹林腸溶片”這種帶劑型的藥物名標(biāo)準(zhǔn)詞典可能只收錄“阿司匹林”分詞后“腸溶片”變成普通詞導(dǎo)致實體抽取只能抽到半個名字。更麻煩的是“小兒感冒顆?!边@類包含修飾語的長詞條詞典不維護(hù)完整詞表的話幾乎百分百切錯。我當(dāng)時的排錯思路是把問答模塊單獨做日志輸出把每個問句的分詞結(jié)果、候選實體和最終匹配節(jié)點全部打印出來。當(dāng)答案錯誤時往回看分詞結(jié)果基本就能定位是詞典問題、實體鏈接問題還是模板問題。用這個辦法我一次性發(fā)現(xiàn)了二十多個需要補充進(jìn)自定義詞典的藥物和疾病詞條。后續(xù)迭代時我建議你把分詞結(jié)果和匹配節(jié)點也加入監(jiān)控日志這比單看最終答案好不好用要高效得多。5.3 模板覆蓋不足的應(yīng)對規(guī)則模板的天然短板是覆蓋不全。這個項目內(nèi)置了一些問法模板但換成口語化表達(dá)就可能匹配失敗。比如“血壓高的人該注意什么”這句話系統(tǒng)可能無法直接判斷它等價于“高血壓的注意事項”。最初我選擇不斷加模板后來發(fā)現(xiàn)模板越加越多維護(hù)成本急劇上升。后來我換了一個思路先對問句做標(biāo)準(zhǔn)化改寫把口語說法映射成核心句式再做意圖判斷。比如“血壓高”“血壓偏高”先歸一化為“高血壓”“該注意什么”“要當(dāng)心什么”歸一化為“注意事項”。這樣模板數(shù)量不需要膨脹意圖識別準(zhǔn)確率反而提高了。這個經(jīng)驗我覺得很值得分享在垂直領(lǐng)域問答里與其堆模板不如先做問句改寫。6. 項目能怎么改進(jìn)把Demo做成可用產(chǎn)品的方向6.1 同義詞與實體鏈接增強在探索這個項目的基礎(chǔ)上我做了一輪增強實驗核心是補全同義詞映射。醫(yī)療領(lǐng)域的同義詞非常多包括中文別名、英文縮寫、商品名和通用名。比如不同品牌的降壓藥商品名可能完全不同但本質(zhì)成分是同一種藥。如果不做同義詞歸一用戶在問答時就會頻繁遭遇“查不到”。我的實現(xiàn)方式是在圖譜中給實體節(jié)點增加“別名”屬性在實體鏈接階段先按標(biāo)準(zhǔn)名精準(zhǔn)匹配匹配不到再按別名模糊匹配。這一層補充并不復(fù)雜但對問答體驗的提升非常明顯。如果你也打算在醫(yī)療垂直領(lǐng)域做產(chǎn)品建議在一開始就把同義詞表設(shè)計成獨立可維護(hù)的數(shù)據(jù)文件讓醫(yī)學(xué)人員可以持續(xù)補充。6.2 從單輪到多輪與澄清原始項目的交互基本限定在單輪問答用戶問一句系統(tǒng)答一句如果實體有歧義會直接選第一個。這在真實醫(yī)療場景里風(fēng)險很大。一個“心臟疼”的用戶可能指的是心絞痛也可能指的是胃食管反流引起的胸痛系統(tǒng)不應(yīng)該直接給答案而應(yīng)該反問澄清。我在這套系統(tǒng)上增加了一個很輕量的澄清機制當(dāng)候選實體超過一個并且各候選實體之間的答案差異較大時先把候選實體列給用戶讓用戶選擇后再繼續(xù)查詢。雖然這只是多輪交互的雛形但已經(jīng)把體驗從“命令式查詢”往前推了一大步。醫(yī)療問答涉及生命安全寧可多問一句也不要武斷給結(jié)論。6.3 評估方式與個人體會最后聊聊評估。這類項目的效果不能只看幾個例子跑通了就算完。我建議整理一份一百到兩百條的真實問句集覆蓋各個意圖類型人工標(biāo)注正確答案再批量跑系統(tǒng)統(tǒng)計準(zhǔn)確率和無答案率。我對原系統(tǒng)做了一輪實測統(tǒng)計結(jié)果顯示在模板覆蓋范圍內(nèi)準(zhǔn)確率很高但出了覆蓋范圍無答案率明顯上升。這正是規(guī)則系統(tǒng)的分水嶺。改進(jìn)之后我用同義詞、問句改寫和澄清機制把無答案率往下壓了不少。整個過程讓我對醫(yī)療知識圖譜問答有了更深的理解這類系統(tǒng)的天花板其實不取決于算法多先進(jìn)而取決于知識庫的完整度和業(yè)務(wù)規(guī)則的合理設(shè)計。如果后續(xù)要進(jìn)入生產(chǎn)環(huán)境我會考慮引入更強的實體識別模型同時保留規(guī)則系統(tǒng)作為兜底讓兩種方案互相補位。如果你正在研究這個項目我的建議很直接先別急著換模型把知識圖譜的數(shù)據(jù)質(zhì)量打磨到位把提問模板吃透再用真實問句做一輪完整的評估。這個項目最好的地方就是給了你一個完整的基線讓你能在上面快速驗證自己的想法。等這一輪跑完你對醫(yī)療問答里那些“看似簡單但藏滿細(xì)節(jié)”的問題心里就有底了。