如何使用知识图谱和向量数据库实现 Graph RAG -分步教程(下篇)

向量数据库大模型机器学习

使用知识图谱进行相似度搜索

与使用向量空间来查找相似文章不同,我们可以依赖文章所关联的标签来实现相似度搜索。有许多使用标签计算相似度的方法,但在此示例中,我将使用常见的方法:Jaccard 相似度 。我们将再次使用牙龈文章来比较不同的方法。

  
from rdflib importGraph,URIRef  
from rdflib.namespaceimport RDF, RDFS,Namespace, SKOS  
import urllib.parse  
  
# 定义命名空间  
schema =Namespace('http://schema.org/')  
ex =Namespace('http://example.org/')  
rdfs =Namespace('http://www.w3.org/2000/01/rdf-schema#')  
  
# 计算 Jaccard 相似度并返回重叠术语的函数  
def jaccard_similarity(set1, set2):  
    intersection = set1.intersection(set2)  
union= set1.union(set2)  
    similarity = len(intersection)/ len(union)if len(union)!=0else0  
return similarity, intersection  
  
# 加载 RDF 图  
g =Graph()  
g.parse('ontology.ttl', format='turtle')  
  
def get_article_uri(title):  
# 将标题转换为 URI 安全的字符串  
    safe_title = urllib.parse.quote(title.replace(" ","_"))  
returnURIRef(f"http://example.org/article/{safe_title}")  
  
def get_mesh_terms(article_uri):  
    query ="""  
    PREFIX schema:<http://schema.org/>  
    PREFIX ex:<http://example.org/>  
    PREFIX rdfs:<http://www.w3.org/2000/01/rdf-schema#>  
  
    SELECT ?meshTerm  
    WHERE {  
?article schema:about ?meshTerm .  
?meshTerm a ex:MeSHTerm.  
      FILTER (?article =<""" + str(article_uri) + """>)  
}  
"""  
    results = g.query(query)  
    mesh_terms ={str(row['meshTerm'])for row in results}  
return mesh_terms  
  
def find_similar_articles(title):  
    article_uri = get_article_uri(title)  
    mesh_terms_given_article = get_mesh_terms(article_uri)  
  
# 查询所有文章及其 MeSH 术语  
    query ="""  
    PREFIX schema:<http://schema.org/>  
    PREFIX ex:<http://example.org/>  
    PREFIX rdfs:<http://www.w3.org/2000/01/rdf-schema#>  
  
    SELECT ?article ?meshTerm  
    WHERE {  
?article a ex:Article;  
               schema:about ?meshTerm .  
?meshTerm a ex:MeSHTerm.  
}  
"""  
    results = g.query(query)  
  
    mesh_terms_other_articles ={}  
for row in results:  
        article = str(row['article'])  
        mesh_term = str(row['meshTerm'])  
if article notin mesh_terms_other_articles:  
            mesh_terms_other_articles[article]=set()  
        mesh_terms_other_articles[article].add(mesh_term)  
  
# 计算 Jaccard 相似度  
    similarities ={}  
    overlapping_terms ={}  
for article, mesh_terms in mesh_terms_other_articles.items():  
if article != str(article_uri):  
            similarity, overlap = jaccard_similarity(mesh_terms_given_article, mesh_terms)  
            similarities[article]= similarity  
            overlapping_terms[article]= overlap  
  
# 按相似度排序,获取前 5 篇文章  
    top_similar_articles = sorted(similarities.items(), key=lambda x: x[1], reverse=True)[:15]  
  
# 输出结果  
print(f"与 '{title}' 最相似的前 15 篇文章:")  
for article, similarity in top_similar_articles:  
print(f"文章 URI: {article}")  
print(f"Jaccard 相似度: {similarity:.4f}")  
print(f"重叠的 MeSH 术语: {overlapping_terms[article]}")  
print()  
  
# 示例用法  
article_title ="Gingival metastasis as first sign of multiorgan dissemination of epithelioid malignant mesothelioma."  
find_similar_articles(article_title)

结果如下。由于我们再次查询的是牙龈文章,因此它是最相似的文章,这是我们所期望的。其他结果包括:

文章 7 :“股外侧肌的钙化性肌腱炎。三例报告。”
这篇文章讲的是股外侧肌(大腿肌肉)中的钙化性肌腱炎(钙沉积在肌腱中)。这与口腔肿瘤无关。
重叠术语:断层扫描、老年、男性、人类、X 光计算•文章 8 :“前列腺癌患者在前列腺特异性抗原水平变化时,雄激素剥夺治疗的最佳时长。”
这篇文章讨论的是前列腺癌患者应接受雄激素剥夺治疗的时长。虽然这是关于癌症治疗(放疗),但与口腔癌无关。
重叠术语:放疗、老年、男性、人类、辅助治疗•文章 9 :“脑半球不对称的 CT 扫描:失语症恢复的预测因子。”
这篇文章讲的是左右脑半球差异(脑半球不对称)如何预测中风后失语症的恢复情况。
重叠术语:断层扫描、老年、男性、人类、X 光计算


这个方法最好的地方是,由于我们计算相似度的方式,我们可以看到为什么 其他文章是相似的 —— 我们清楚地看到每篇文章与牙龈文章比较时,重叠的术语,即哪些术语在牙龈文章和其他比较文章中是共同的。

可解释性的缺点是:我们可以看到,这些文章似乎并不是最相似的,考虑到之前的结果。它们都共享三个术语(老年、男性和人类),这些术语可能不像治疗方案或口腔肿瘤那么相关。你可以通过使用基于术语在语料库中出现频率的加权方法——如术语频率-逆文档频率(TF-IDF) ,来重新计算,这可能会改善结果。你也可以在进行相似度计算时选择最相关的标记术语,以便对结果进行更多的控制。

使用知识图谱计算术语相似度的最大缺点是计算量 —— 这次计算大约花了 30 分钟。

使用知识图谱进行 RAG

我们还可以仅使用知识图谱来进行 RAG 的检索部分。我们已经从上面的语义搜索中保存了一些关于口腔肿瘤的文章列表。为了实现 RAG,我们只需将这些文章发送给 LLM,并要求它总结结果。

首先,我们将每篇文章的标题和摘要合并成一个大的文本块,称为 combined\_text

  
# 合并标题和摘要的函数  
def combine_abstracts(top_3_articles):  
    combined_text ="".join(  
[f"Title: {data['title']} Abstract: {data['abstract']}"for article_uri, data in top_3_articles]  
)  
return combined_text  
  
# 合并前三篇文章的摘要  
combined_text = combine_abstracts(top_3_articles)  
print(combined_text)

然后我们设置一个客户端,这样就可以将这个文本直接发送给 LLM:

  
import openai  
  
# 设置你的 OpenAI API 密钥  
api_key ="YOUR API KEY"  
openai.api_key = api_key

接下来,我们给 LLM 提供上下文和提示:

  
def generate_summary(combined_text):  
    response = openai.Completion.create(  
        model="gpt-3.5-turbo-instruct",  
        prompt=f"Summarize the key information here in bullet points. Make it understandable to someone without a medical degree:\n\n{combined_text}",  
        max_tokens=1000,  
        temperature=0.3  
)  
  
# 获取原始文本输出  
    raw_summary = response.choices[0].text.strip()  
  
# 将文本按行拆分并清理空白字符  
    lines = raw_summary.split('\n')  
    lines =[line.strip()for line in lines if line.strip()]  
  
# 将行重新合并并保持换行  
    formatted_summary ='\n'.join(lines)  
  
return formatted_summary  
  
# 生成并打印摘要  
summary = generate_summary(combined_text)  
print(summary)

结果如下:

  
-一名14岁男孩在前上颌发现了一个牙龈肿瘤,已被切除并通过光学和电子显微镜研究  
-该肿瘤由肌上皮细胞组成,并且呈现恶性特征  
-电子显微镜显示该肿瘤起源于唾液腺  
-这是唯一一个确认存在恶性特征的肌上皮瘤案例  
-进行了一项可行性研究,旨在提高在高发区域对口腔癌和癌前病变的早期检测  
-烟草商参与了分发传单,邀请吸烟者接受全科医生的免费检查  
-研究包括了93名患者,27%被转诊到专科医生  
-63.6%的被转诊患者实际上看了专家,15.3%被确认有癌前病变  
-一项研究发现口腔鳞状细胞癌(OSCC)中,胚胎致死异常视觉蛋白HuR的表达与环氧化酶 COX-2的表达相关  
-胶质胞质中的HuR表达与 COX-2表达、淋巴结及远处转移相关  
-抑制HuR表达导致口腔癌细胞中 COX-2表达减少。

这些结果看起来不错,即它是对从语义搜索返回的三篇文章的一个很好的总结。 使用仅基于知识图谱(KG)的 RAG 应用程序的响应质量,取决于你的 KG 检索相关文档的能力。 如本例所示,如果提示足够简单,比如“总结这里的关键信息”,那么困难的部分是检索(为 LLM 提供正确的文章作为上下文),而生成响应并不难。

第 3 步:使用向量化的知识图谱进行数据检索测试

现在我们要将两者结合起来。我们将在数据库中的每篇文章中添加 URI,然后在 Weaviate 中创建一个新集合,将文章的标题、摘要、关联的 MeSH 术语以及 URI 进行向量化。URI 是文章的唯一标识符,并且是我们与知识图谱连接的方式。

首先,我们在数据中为 URI 添加一个新列:

  
# 创建有效 URI 的函数  
def create_valid_uri(base_uri, text):  
if pd.isna(text):  
returnNone  
# 对文本进行编码,使其适用于 URI  
    sanitized_text = urllib.parse.quote(text.strip().replace(' ','_').replace('"','').replace('<','').replace('>','').replace("'","_"))  
returnURIRef(f"{base_uri}/{sanitized_text}")  
  
# 为 DataFrame 添加一列新的文章 URI  
df['Article_URI']= df['Title'].apply(lambda title: create_valid_uri("http://example.org/article", title))

接下来,我们为新集合创建一个新 schema,包含附加字段:

  
class_obj ={  
# 类定义  
"class":"articles_with_abstracts_and_URIs",  
  
# 属性定义  
"properties":[  
{  
"name":"title",  
"dataType":["text"],  
},  
{  
"name":"abstractText",  
"dataType":["text"],  
},  
{  
"name":"meshMajor",  
"dataType":["text"],  
},  
{  
"name":"Article_URI",  
"dataType":["text"],  
},  
],  
  
# 指定向量化器  
"vectorizer":"text2vec-openai",  
  
# 模块设置  
"moduleConfig":{  
"text2vec-openai":{  
"vectorizeClassName":True,  
"model":"ada",  
"modelVersion":"002",  
"type":"text"  
},  
"qna-openai":{  
"model":"gpt-3.5-turbo-instruct"  
},  
"generative-openai":{  
"model":"gpt-3.5-turbo"  
}  
},  
}

将该 schema 推送到向量数据库中:

  
client.schema.create\_class(class\_obj)

现在我们将数据向量化到新的集合中:

  
import logging  
import numpy as np  
  
# 配置日志记录  
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s')  
  
# 将无穷大替换为 NaN,然后再填充 NaN 值  
df.replace([np.inf,-np.inf], np.nan, inplace=True)  
df.fillna('', inplace=True)  
  
# 将列转换为字符串类型  
df['Title']= df['Title'].astype(str)  
df['abstractText']= df['abstractText'].astype(str)  
df['meshMajor']= df['meshMajor'].astype(str)  
df['Article_URI']= df['Article_URI'].astype(str)  
  
# 记录数据类型  
logging.info(f"Title column type: {df['Title'].dtype}")  
logging.info(f"abstractText column type: {df['abstractText'].dtype}")  
logging.info(f"meshMajor column type: {df['meshMajor'].dtype}")  
logging.info(f"Article_URI column type: {df['Article_URI'].dtype}")  
  
# 批量写入数据  
with client.batch(  
    batch_size=10,# 指定批量大小  
    num_workers=2,# 并行化处理  
)as batch:  
for index, row in df.iterrows():  
try:  
            question_object ={  
"title": row.Title,  
"abstractText": row.abstractText,  
"meshMajor": row.meshMajor,  
"article_URI": row.Article_URI,  
}  
            batch.add_data_object(  
                question_object,  
                class_name="articles_with_abstracts_and_URIs",  
                uuid=generate_uuid5(question_object)  
)  
exceptExceptionas e:  
            logging.error(f"Error processing row {index}: {e}")

使用向量化的知识图谱进行语义搜索

现在我们可以像之前一样,在向量数据库上进行语义搜索,但这次有更多的可解释性和对结果的控制。

  
response =(  
    client.query  
.get("articles_with_abstracts_and_URIs",["title","abstractText","meshMajor","article_URI"])  
.with_additional(["id"])  
.with_near_text({"concepts":["mouth neoplasms"]})  
.with_limit(10)  
.do()  
)  
  
print(json.dumps(response, indent=4))

返回的结果如下:

文章 1 :“牙龈转移作为上皮型恶性间皮瘤多器官扩散的首个征兆。”•文章 10 :“血管中心性中面部淋巴瘤:一名老年男子的诊断挑战。”
这篇文章讲述的是如何诊断一名男子的鼻癌。•文章 11 :“下颌伪癌性增生。”
这篇文章对我来说很难理解,但我认为它讨论的是伪癌性增生如何看起来像癌症(因此名字中有‘伪’),但它实际上是非癌性的。虽然它确实是关于下颌的,但它被标记为“口腔肿瘤”MeSH 术语。

这些结果是否比知识图谱(KG)或仅使用向量数据库的结果要好,还是更差,很难判断。从理论上讲,结果应该更好,因为现在每篇文章的 MeSH 术语也与文章一起向量化了。然而,我们并没有真正地向量化知识图谱。例如,MeSH 术语之间的关系并没有包含在向量数据库中。

有一个好处是,MeSH 术语向量化后 ,我们可以立即获得一些可解释性 —— 例如,文章 11 也被标记为口腔肿瘤。
但将向量数据库与知识图谱连接的真正好处是,我们可以应用任何我们想要的来自知识图谱的过滤器。记得之前我们在数据中添加了“发布日期”作为字段吗?现在我们可以基于这个字段进行筛选。假设我们想找出2020年5月1日之后发布的口腔肿瘤相关的文章

  
from rdflib importGraph,Namespace,URIRef,Literal  
from rdflib.namespaceimport RDF, RDFS, XSD  
  
# 定义命名空间  
schema =Namespace('http://schema.org/')  
ex =Namespace('http://example.org/')  
rdfs =Namespace('http://www.w3.org/2000/01/rdf-schema#')  
xsd =Namespace('http://www.w3.org/2001/XMLSchema#')  
  
def get_articles_after_date(graph, article_uris, date_cutoff):  
# 创建一个字典来存储每个 URI 的结果  
    results_dict ={}  
  
# 使用文章 URI 列表和日期过滤器定义 SPARQL 查询  
    uris_str =" ".join(f"<{uri}>"for uri in article_uris)  
    query = f"""  
    PREFIX schema:<http://schema.org/>  
    PREFIX ex:<http://example.org/>  
    PREFIX rdfs:<http://www.w3.org/2000/01/rdf-schema#>  
    PREFIX xsd:<http://www.w3.org/2001/XMLSchema#>  
  
    SELECT ?article ?title ?datePublished  
    WHERE {{  
      VALUES ?article {{{uris_str}}}  
  
?article a ex:Article;  
               schema:name ?title ;  
               schema:datePublished ?datePublished .  
  
      FILTER (?datePublished >"{date_cutoff}"^^xsd:date)  
}}  
"""  
  
# 执行查询  
    results = graph.query(query)  
  
# 提取每篇文章的详细信息  
for row in results:  
        article_uri = str(row['article'])  
        results_dict[article_uri]={  
'title': str(row['title']),  
'date_published': str(row['datePublished'])  
}  
  
return results_dict  
  
date_cutoff ="2023-01-01"  
articles_after_date = get_articles_after_date(g, article_uris, date_cutoff)  
  
# 输出结果  
for uri, details in articles_after_date.items():  
print(f"Article URI: {uri}")  
print(f"Title: {details['title']}")  
print(f"Date Published: {details['date_published']}")  
print()

最初的查询返回了 10 个结果(我们设置了最大值为 10),但这些文章中只有 6 篇是在 2023年1月1日之后发布的。以下是结果:

picture.image

使用向量化的知识图谱进行相似度搜索

我们可以像之前对牙龈文章(文章 1)进行的搜索一样,在这个新集合中进行相似度搜索:

  
response =(  
    client.query  
.get("articles_with_abstracts_and_URIs",["title","abstractText","meshMajor","article_URI"])  
.with_near_object({  
"id":"37b695c4-5b80-5f44-a710-e84abb46bc22"  
})  
.with_limit(50)  
.with_additional(["distance"])  
.do()  
)  
  
print(json.dumps(response, indent=2))

返回结果如下:

•文章 3:“下颌转移性神经母细胞瘤。病例报告。”•文章 4:“针对烟草使用者的口腔恶性病变筛查可行性研究。”•文章 12:“弥漫性肺内恶性间皮瘤伪装为间质性肺病:一种独特的间皮瘤变种。” 这篇文章讲的是五名男性患者患有一种看起来很像其他肺病——间质性肺病的间皮瘤。

由于我们已经向量化了 MeSH 标记的术语,我们可以看到每篇文章相关的标签。虽然它们在某些方面可能相似,但并不是关于口腔肿瘤的文章。假设我们想找到与牙龈文章相似的文章,但特别是关于口腔肿瘤的文章。现在,我们可以结合之前在这些结果中使用知识图谱进行的 SPARQL 过滤。

“口腔肿瘤”的同义词和下位概念的 MeSH URI 已经保存,但我们需要为从向量搜索中返回的 50 篇文章获取 URI:

  
# 假设 response 是包含你文章的数据结构  
article_uris =[URIRef(article["article_URI"])for article in response["data"]["Get"]["Articles_with_abstracts_and_URIs"]]

现在我们可以像之前使用知识图谱进行语义搜索时一样,基于标签对结果进行排序。

  
from rdflib importURIRef  
  
# 使用文章 URI 过滤器构造 SPARQL 查询  
query ="""  
PREFIX schema:<http://schema.org/>  
PREFIX ex:<http://example.org/>  
  
SELECT ?article ?title ?abstract?datePublished ?access ?meshTerm  
WHERE {  
?article a ex:Article;  
           schema:name ?title ;  
           schema:description ?abstract;  
           schema:datePublished ?datePublished ;  
           ex:access ?access ;  
           schema:about ?meshTerm .  
  
?meshTerm a ex:MeSHTerm.  
  
# 过滤器,只包含 URI 列表中的文章  
  FILTER (?article IN (%s))  
}  
"""  
  
# 将 URIRef 列表转换为适用于 SPARQL 的字符串  
article_uris_string =", ".join([f"<{str(uri)}>"for uri in article_uris])  
  
# 将文章 URI 插入查询中  
query = query % article_uris_string  
  
# 用于存储文章及其相关 MeSH 术语的字典  
article_data ={}  
  
# 针对每个 MeSH 术语运行查询  
for mesh_term in mesh_terms:  
    results = g.query(query, initBindings={'meshTerm': mesh_term})  
  
# 处理结果  
for row in results:  
        article_uri = row['article']  
  
if article_uri notin article_data:  
            article_data[article_uri]={  
'title': row['title'],  
'abstract': row['abstract'],  
'datePublished': row['datePublished'],  
'access': row['access'],  
'meshTerms':set()  
}  
  
# 将 MeSH 术语添加到该文章的术语集  
        article_data[article_uri]['meshTerms'].add(str(row['meshTerm']))  
  
# 按照匹配的 MeSH 术语数量对文章进行排名  
ranked_articles = sorted(  
    article_data.items(),  
    key=lambda item: len(item[1]['meshTerms']),  
    reverse=True  
)  
  
# 输出结果  
for article_uri, data in ranked_articles:  
print(f"Title: {data['title']}")  
print(f"Abstract: {data['abstract']}")  
print("MeSH Terms:")  
for mesh_term in data['meshTerms']:  
print(f"  - {mesh_term}")  
print()

在向量数据库最初返回的 50 篇文章中,只有 5 篇被标记为“口腔肿瘤”或相关概念。

文章 2 :“起源于小唾液腺的肌上皮瘤。光学和电子显微镜研究。”
标记为:牙龈肿瘤、唾液腺肿瘤•文章 4 :“针对烟草使用者的口腔恶性病变筛查可行性研究。”
标记为:口腔肿瘤•文章 13 :“起源于牙龈沟的表皮样癌。”
这篇文章描述了一个牙龈癌(牙龈肿瘤)的案例。
标记为:牙龈肿瘤•文章 1 :“牙龈转移作为上皮型恶性间皮瘤多器官扩散的首个征兆。”
标记为:牙龈肿瘤•文章 14 :“转移至耳下腺淋巴结:CT 和 MR 成像结果。”
这篇文章讲的是耳下腺(主要唾液腺)中的肿瘤。
标记为:耳下腺肿瘤

最后,假设我们要将这些相似的文章推荐给用户,但我们只想推荐该用户有权限访问的文章。假设我们知道该用户只能访问标记为访问等级 3、5 和 7 的文章。我们可以在知识图谱中使用类似的 SPARQL 查询来应用过滤器:

  
from rdflib importGraph,Namespace,URIRef,Literal  
from rdflib.namespaceimport RDF, RDFS, XSD, SKOS  
  
# 假设你的 RDF 图(g)已加载  
  
# 定义命名空间  
schema =Namespace('http://schema.org/')  
ex =Namespace('http://example.org/')  
rdfs =Namespace('http://www.w3.org/2000/01/rdf-schema#')  
  
def filter_articles_by_access(graph, article_uris, access_values):  
# 使用动态 VALUES 子句构造 SPARQL 查询  
    uris_str =" ".join(f"<{uri}>"for uri in article_uris)  
    query = f"""  
    PREFIX schema:<http://schema.org/>  
    PREFIX ex:<http://example.org/>  
    PREFIX rdfs:<http://www.w3.org/2000/01/rdf-schema#>  
  
    SELECT ?article ?title ?abstract?datePublished ?access ?meshTermLabel  
    WHERE {{  
      VALUES ?article {{{uris_str}}}  
  
?article a ex:Article;  
               schema:name ?title ;  
               schema:description ?abstract;  
               schema:datePublished ?datePublished ;  
               ex:access ?access ;  
               schema:about ?meshTerm .  
?meshTerm rdfs:label ?meshTermLabel .  
  
      FILTER (?access IN ({", ".join(map(str, access_values))}))  
}}  
"""  
  
# 执行查询  
    results = graph.query(query)  
  
# 提取每篇文章的详细信息  
    results_dict ={}  
for row in results:  
        article_uri = str(row['article'])  
if article_uri notin results_dict:  
            results_dict[article_uri]={  
'title': str(row['title']),  
'abstract': str(row['abstract']),  
'date_published': str(row['datePublished']),  
'access': str(row['access']),  
'mesh_terms':[]  
}  
        results_dict[article_uri]['mesh_terms'].append(str(row['meshTermLabel']))  
  
return results_dict  
  
access_values =[3,5,7]  
filtered_articles = filter_articles_by_access(g, ranked_article_uris, access_values)  
  
# 输出结果  
for uri, details in filtered_articles.items():  
print(f"Article URI: {uri}")  
print(f"Title: {details['title']}")  
print(f"Abstract: {details['abstract']}")  
print(f"Date Published: {details['date_published']}")  
print(f"Access: {details['access']}")  
print()

有一篇文章用户无法访问。其余四篇文章如下:

文章 2 :“起源于小唾液腺的肌上皮瘤。光学和电子显微镜研究。”
标记为:牙龈肿瘤、唾液腺肿瘤。访问级别:5•文章 4 :“针对烟草使用者的口腔恶性病变筛查可行性研究。”
标记为:口腔肿瘤。访问级别:7•文章 1 :“牙龈转移作为上皮型恶性间皮瘤多器官扩散的首个征兆。”
标记为:牙龈肿瘤。访问级别:3•文章 14 :“转移至耳下腺淋巴结:CT 和 MR 成像结果。”
这篇文章讲的是耳下腺(主要唾液腺)中的肿瘤。
标记为:耳下腺肿瘤。访问级别:3

使用向量化的知识图谱进行 RAG

最后,让我们看看当我们将向量数据库与知识图谱结合使用时,RAG(检索增强生成)是如何工作的。提醒一下,你可以直接对向量数据库运行 RAG,并将其发送给 LLM 以生成响应:

  
response =(  
    client.query  
.get("Articles_with_abstracts_and_URIs",["title","abstractText",'article_URI','meshMajor'])  
.with_near_text({"concepts":["therapies for mouth neoplasms"]})  
.with_limit(3)  
.with_generate(grouped_task="Summarize the key information here in bullet points. Make it understandable to someone without a medical degree.")  
.do()  
)  
  
print(response["data"]["Get"]["Articles_with_abstracts_and_URIs"][0]["_additional"]["generate"]["groupedResult"])

在这个示例中,我使用了搜索词 ‘therapies for mouth neoplasms’,并且与之前一样使用了提示:

"Summarize the key information here in bullet points. Make it understandable to someone without a medical degree." 我们只返回了排名前三的文章来生成此响应。以下是结果:

  
-口腔内恶性间皮瘤转移是罕见的,平均生存期为9-12个月。  
-2012年8月至2013年9月期间,13名患者接受了新辅助化疗和根治性胸膜切除术,随后进行放疗。  
-到2014年1月,11名患者仍然存活,中位生存期为11个月,而8名患者发生复发,2名患者在手术后8个月和9个月死亡。  
-一名68岁的男子发现其牙龈肿块为恶性间皮瘤的转移性病灶,导致多器官复发。  
-对新生长病变进行活检非常重要,即使是在不常见的部位,尤其是当患者有间皮瘤病史时。  
-新辅助放化疗对局部晚期直肠癌可能有效,但一些患者可能反应不佳。  
-基因改变可能与直肠癌新辅助治疗的敏感性或耐药性相关。  
-在直肠癌肿瘤中发现了1p、8p、17p和18q的丧失,以及1q和13q的获得。  
-特定染色体区域的改变与新辅助治疗的反应相关。  
-肿瘤细胞的细胞遗传学特征可能会影响直肠癌放化疗的反应。  
-调强放射治疗对鼻咽癌在局部控制和总生存期方面取得了良好的长期效果。  
-急性毒性包括口腔炎、皮炎和口干,大多数患者的毒性等级为0-2。  
-晚期毒性主要包括口干,但随着时间的推移有所改善。  
-远处转移仍然是治疗失败的主要原因,突显了更有效的系统治疗的需求。

作为一个测试,我们可以准确地查看选择了哪三篇文章:

  
# 提取文章的 URI  
article_uris =[article["article_URI"]for article in response["data"]["Get"]["Articles_with_abstracts_and_URIs"]]  
  
# 过滤响应中仅包含给定 URI 的文章的函数  
def filter_articles_by_uri(response, article_uris):  
    filtered_articles =[]  
  
    articles = response['data']['Get']['Articles_with_abstracts_and_URIs']  
for article in articles:  
if article['article_URI']in article_uris:  
            filtered_articles.append(article)  
  
return filtered_articles  
  
# 过滤响应  
filtered_articles = filter_articles_by_uri(response, article_uris)  
  
# 输出过滤后的文章  
print("Filtered articles:")  
for article in filtered_articles:  
print(f"Title: {article['title']}")  
print(f"URI: {article['article_URI']}")  
print(f"Abstract: {article['abstractText']}")  
print(f"MeshMajor: {article['meshMajor']}")  
print("---")

有趣的是,第一篇文章是关于牙龈肿瘤的,这是口腔肿瘤的一个子集,但第二篇文章是关于直肠癌的,第三篇是关于鼻咽癌的。它们讨论的是癌症的治疗方法,只是不是我搜索的那种癌症。令人担忧的是,提示词是“口腔肿瘤的治疗方法”,但是结果中包含了其他类型癌症的治疗信息。这有时被称为“上下文污染”(context poisoning)——无关或误导性的信息被注入到提示词中,从而导致 LLM 返回误导性的回答。

我们可以使用知识图谱(KG)来解决这个上下文污染问题。以下是一个图示,展示了向量数据库和知识图谱如何协同工作,以实现更好的 RAG 实现:

picture.image

首先,我们在向量数据库上使用相同的提示词进行语义搜索:口腔癌的治疗方法。这次我将限制条目数增加到 20 篇,因为我们会筛选掉一些不相关的文章。

  
response =(  
    client.query  
.get("articles_with_abstracts_and_URIs",["title","abstractText","meshMajor","article_URI"])  
.with_additional(["id"])  
.with_near_text({"concepts":["therapies for mouth neoplasms"]})  
.with_limit(20)  
.do()  
)  
  
# 提取文章的 URI  
article_uris =[article["article_URI"]for article in response["data"]["Get"]["Articles_with_abstracts_and_URIs"]]  
  
# 打印提取的文章 URI  
print("Extracted article URIs:")  
for uri in article_uris:  
print(uri)

接下来,我们使用之前相同的排序技术,基于与“口腔肿瘤”相关的概念进行排序:

  
from rdflib importURIRef  
  
# 使用文章 URI 过滤器构造 SPARQL 查询  
query ="""  
PREFIX schema:<http://schema.org/>  
PREFIX ex:<http://example.org/>  
  
SELECT ?article ?title ?abstract?datePublished ?access ?meshTerm  
WHERE {  
?article a ex:Article;  
           schema:name ?title ;  
           schema:description ?abstract;  
           schema:datePublished ?datePublished ;  
           ex:access ?access ;  
           schema:about ?meshTerm .  
  
?meshTerm a ex:MeSHTerm.  
  
# 过滤器,只包含 URI 列表中的文章  
  FILTER (?article IN (%s))  
}  
"""  
  
# 将 URIRef 列表转换为适用于 SPARQL 的字符串  
article_uris_string =", ".join([f"<{str(uri)}>"for uri in article_uris])  
  
# 将文章 URI 插入查询中  
query = query % article_uris_string  
  
# 用于存储文章及其相关 MeSH 术语的字典  
article_data ={}  
  
# 针对每个 MeSH 术语运行查询  
for mesh_term in mesh_terms:  
    results = g.query(query, initBindings={'meshTerm': mesh_term})  
  
# 处理结果  
for row in results:  
        article_uri = row['article']  
  
if article_uri notin article_data:  
            article_data[article_uri]={  
'title': row['title'],  
'abstract': row['abstract'],  
'datePublished': row['datePublished'],  
'access': row['access'],  
'meshTerms':set()  
}  
  
# 将 MeSH 术语添加到该文章的术语集  
        article_data[article_uri]['meshTerms'].add(str(row['meshTerm']))  
  
# 按照匹配的 MeSH 术语数量对文章进行排名  
ranked_articles = sorted(  
    article_data.items(),  
    key=lambda item: len(item[1]['meshTerms']),  
    reverse=True  
)  
  
# 输出结果  
for article_uri, data in ranked_articles:  
print(f"Title: {data['title']}")  
print(f"Abstract: {data['abstract']}")  
print("MeSH Terms:")  
for mesh_term in data['meshTerms']:  
print(f"  - {mesh_term}")  
print()

只有三篇文章被标记为“口腔肿瘤”相关术语:

文章 4 :“针对烟草使用者的口腔恶性病变筛查可行性研究。”
标记为:口腔肿瘤•文章 15 :“光动力治疗实验性研究:化学诱导的口腭粘膜癌前病变和鳞状细胞癌的治疗。”
这篇文章讨论了在大鼠中测试的一种癌症治疗方法(光动力治疗)。
标记为:口腭肿瘤•文章 1 :“牙龈转移作为上皮型恶性间皮瘤多器官扩散的首个征兆。”
标记为:牙龈肿瘤

我们将这些文章发送到 LLM,看看结果是否有所改善:

  
# 过滤响应  
filtered_articles = filter_articles_by_uri(response, matching_articles)  
  
# 将标题和摘要合并成一段文本的函数  
def combine_abstracts(filtered_articles):  
    combined_text ="\n\n".join(  
[f"Title: {article['title']}\nAbstract: {article['abstractText']}"for article in filtered_articles]  
)  
return combined_text  
  
# 合并过滤后的文章摘要  
combined_text = combine_abstracts(filtered_articles)  
  
# 生成并打印摘要  
summary = generate_summary(combined_text)  
print(summary)

结果如下:

  
-口腔癌很常见,通常在晚期才被发现  
-进行了一项可行性研究,旨在提高在高风险地区对口腔癌和癌前病变的早期检测  
-烟草商参与了分发传单,邀请吸烟者接受全科医生的免费检查  
-研究包括了93名患者,其中27%被转诊到专家  
-63.6%的转诊患者实际就诊了专家,其中15.3%被诊断为癌前病变  
-光动力治疗(PDT)作为一种实验性癌症治疗方法,在大鼠的化学诱导的癌前病变和口腭鳞状细胞癌中进行研究  
- PDT 使用Photofrin和两种不同的激活波长进行治疗,514.5 nm 波长组的治疗效果较好  
-恶性间皮瘤的牙龈转移非常罕见,且生存率较低  
-一项病例研究显示,患者出现牙龈肿块作为恶性间皮瘤多器官复发的首个征兆,强调了对所有新生长病变进行活检的重要性,即使在不常见的解剖部位也应如此。

我们可以明显看到改进 —— 这些结果不再是关于直肠癌或鼻咽肿瘤的。看起来这确实是三篇被选中的文章的相对准确的总结,都是关于口腔肿瘤的治疗方法。

结论

1. 向量数据库的优势

向量数据库非常适合快速启动搜索、相似度(推荐)和 RAG 应用程序,并且几乎不需要额外的开销。它在处理带有无结构数据的结构化数据时表现尤为出色。例如,在这个期刊文章的例子中,如果我们有文章摘要,它能够很好地帮助我们获取相关信息。

注意:如果没有文章摘要作为数据集的一部分,效果可能就会大打折扣。

2. 知识图谱的优势

知识图谱在提高准确性和控制方面非常有用。如果你希望确保输入到搜索应用程序中的数据是“正确的”(这里的“正确”指的是符合你需求的标准),那么知识图谱将是不可或缺的。知识图谱对于搜索和相似度非常有效,但它满足需求的程度取决于:

•元数据的丰富度•标签的质量

标签质量的好坏也因使用场景的不同而有所区别 —— 例如,构建推荐引擎和搜索引擎时,分类法的构建与应用方式可能不同。

3. 向量数据库 + 知识图谱的最佳组合

将知识图谱用于过滤向量数据库中的结果,是取得最佳效果的做法。这一点并不令人惊讶 —— 因为我在使用知识图谱来过滤掉我认为无关或误导性的结果,从而使结果更加符合我的需求。

重点:并不是说知识图谱本身会直接改善结果,而是它提供了控制输出的能力,使得结果能够得到优化。


总结:

•向量数据库适合快速启动应用程序,处理无结构数据时表现优越。•知识图谱通过确保数据的准确性和提供更高的控制力,帮助提高搜索结果的质量。•向量数据库和知识图谱的结合,能够过滤无关信息,从而得到更加精准的搜索结果。

更多信息

山行AI希望本文对你有所帮助,更多信息请查看:https://medium.com/data-science/how-to-implement-graph-rag-using-knowledge-graphs-and-vector-databases-60bb69a22759,感谢点赞、转发!

References

[1] 这里:https://github.com/SteveHedden/kg\_llm
[2]Graph RAG:https://www.ontotext.com/knowledgehub/fundamentals/what-is-graph-rag/
[3]GraphRAG:https://microsoft.github.io/graphrag/
[4]GRAG:https://arxiv.org/abs/2405.16506
[5]语义 RAG:https://www.poolparty.biz/graph-retrieval-augmented-generation
[6]越来越多:https://arxiv.org/pdf/2311.07509
[7]PubMed 仓库:https://www.kaggle.com/datasets/owaiskhan9654/pubmed-multilabel-text-classification
[8]CC0 公共领域:https://creativecommons.org/publicdomain/zero/1.0/
[9]Weaviate 的官方快速入门教程:https://weaviate.io/developers/weaviate/current/getting-started/quickstart.html
[10]文章: https://towardsdatascience.com/semantic-search-with-weaviate-b4fa8a982e5d

0
0
0
0
关于作者
关于作者

文章

0

获赞

0

收藏

0

相关资源
CV 技术在视频创作中的应用
本次演讲将介绍在拍摄、编辑等场景,我们如何利用 AI 技术赋能创作者;以及基于这些场景,字节跳动积累的领先技术能力。
相关产品
评论
未登录
看完啦,登录分享一下感受吧~
暂无评论