面向文本分析的交互式主题建模
目录
面向文本分析的交互式主题建模 1
一、绪论 2
1.1 本课题的研究背景和意义 2
1.1.1 主题模型的发展及研究现状 2
1.1.2 目前存在的问题 3
1.1.3 本课题的研究意义 3
1.2 研究内容和主要工作 3
1.3 本文的组织结构 3
二、核心算法 3
2.1 文本预处理 4
2.2 大型语料库的内存优化 6
2.3 UMAP 数据降维&可视化 7
三、系统设计与实现 9
3.1 系统介绍及流程图 9
3.2 后端实现过程 10
3.2.1 框架介绍 10
3.2.2 数据库 ORM 映射类 10
3.2.3 Pydantic 模型类 12
3.2.4 TextPreprocessing 文本预处理类 12
3.2.5 SSNMFTopicModel 非负矩阵分解类 12
3.2.6 TopicModelTrainingTask 训练任务类 12
3.2.7 Web API 接口 13
3.3 前端实现过程 16
3.3.1 框架介绍 16
3.3.2 介绍界面 16
3.3.3 Bilibili 视频评论爬虫界面 17
3.3.4 语料库查看/选择界面 18
3.3.5 侧边栏主控制界面 19
3.3.6 右侧训练状态 Tab 19
3.3.7 词云图 Tab 19
3.3.8 NMF 迭代误差折线图 Tab 20
3.3.9 主题聚类可视化 Tab 20
3.3.10 文档详细信息抽屉 22
3.3.11 主题详细信息抽屉 23
3.3.12 用户交互 23
3.3.13 新文档主题分布预测 29
四、算法定量分析及比较 29
4.1 性能及收敛速度 30
4.2 多次运行的一致性 30
五、使用案例 31
5.1 Bilibili 视频评论数据 31
5.2 外卖用户评价数据 39
5.3 新冠病毒新闻数据 44
六、总结及展望 47
一、绪论
1.1 本课题的研究背景和意义
近年来,随着网络以及社交媒体的不断发展及活跃,互联网上每天都会产生大量的文本信息。在这些海量的文本数据中,对其进行分析并提取出有价值的内容,并由此指导用户的决策过程一直是研究的热点领域。在解决这些问题的各种方法中,主题建模方法(从文档语料库中发现语义上具有意义的主题)在数据挖掘/机器学习和可视化分析领域得到了广泛的应用。顾名思义,这是一个自动识别文本对象中存在的主题并派生文本语料库显示的隐藏模式的过程。
主题建模不同于使用正则表达式或基于字典的关键字搜索技术的基于规则的文本挖掘方法。这是一种无监督的方法,用于查找和观察大型文本集中的一堆单词(称为“主题”)。主题可以定义为“语料库中共同出现的术语的重复模式”。
主题模型有着广泛的应用场景,特别是对于文档聚类,组织大量文本数据,从非结构化文本中检索信息以及选择功能非常有用。例如,《纽约时报》正在使用主题模型来提升其用户–文章推荐引擎;各种专业人士正在针对招聘行业使用主题模型,他们旨在提取职位描述的潜在特征并将其映射到合适的候选人。
在本节接下来的内容中,我们将回顾主题模型的发展及研究现状,提出目前存在的问题,以及本课题的研究意义。
1.1.1 主题模型的发展及研究现状
早在上世纪 80 年代,研究者们常常使用 TF-IDF 方法来进行文档的检索和信息的提取。在此方法中,先选择一个基于词或者词组的基本词汇,然后对于语料库中的每个文档,分别统计其中每个单词的出现次数。经过合适的正则化后,再将词的频率计数和反向文档频率计数相比较,得到一个词数乘以文档数的向量 X。向量的每个行中都包含着语料库中每个文档的 TF-IDF 值。
但是整个 TF-IDF 算法是建立在一个假设之上的:一个单词出现的文本频数越小,它区别不同类别文本的能力就越大。这个假设很多时候是不正确的,尤其是在引入 IDF 的过程中,单纯地认为文本频率小的单词就越重要,文本频率大的单词就越无用,显然这并不是完全正确的。其也不能有效地反映单词的重要程度和特征词的分布情况,因此精度有限。
上世纪 90 年代,Deerwester S 等人提出了潜在语义分析(LSA),它是一种用于知识获取和展示的计算理论和方法,出发点就是文本中的词与词之间存在某种联系,即存在某种潜在的语义结构。Thomas Hofmann 于 1999 年提出了概率潜在语义分析(pLSA),并在文中描述了 pLSA 与 LSA 的区别,即 LSA 主要基于奇异值分解(SVD),而 pLSA 则依赖混合分解。他随后进行了一系列实证研究,并讨论了 pLSA 在自动文档索引中的应用。他的实证结果表明 pLSA 相对于 LSA 的表现有明显进步。
年,潜在语义分析被 Jerome Bellegarda 提出使用在自然语言处理中。2003 年 Andrew Y. Ng 等人在论文中提出用于 pLSA 的 aspect model 具有严重的过度拟合问题,他们提出了隐含狄利克雷分布(LDA),这可以看作是结合了贝叶斯思想的 pLSA。LDA 是目前使用的最常见的主题模型。
年,针对当前主题模型可以为文档构建可解释的向量表示,而词向量在句法规则方面十分有效,Christopher E Moody 提出了lda2vec,一个学习密集单词向量的模型,将上述两类模型的优点结合。他们的方法很容易融入现有的自动微分框架,并允许科学家使用无监督文档表示,同时学习单词向量和它们之间的线性关系。
1.1.2 目前存在的问题
主题模型是一种无监督学习模型,其结果的好坏取决于所选的模型参数和训练集,并且具有很高的不确定性。用户通常无法在训练过程中对模型的结果进行修正,特别是一些专业领域,用户无法向主题模型提供一些领域知识来提高主题建模的质量。已有一些研究提出,通过对主题模型添加约束的形式来解决这一问题。Hu 等人提出了交互式主题模型,然而主题建模的结果十分不直观,从中寻找不恰当的结果并添加合适的约束非常耗时、费力。同时现在基于概率图模型的主题建模算法结果往往具有不确定性,很难引入用户的交互反馈操作,比如让用户交互调整建模的中间结果和参数,从而得到更优的主题分析结果。
1.1.3 本课题的研究意义
本课题的研究意义是对当前主题建模算法进行优化改进,解决算法结果的不确定性和用户交互反馈引入的困难性这 2 个问题,将可视化分析技术与主题模型相结合,提供有效的交互手段,让人们充分参与到分析主题模型的结果中来,利用人的认知能力,从数据中挖掘有效信息,达到基于用户驱动的文本主题模型交互优化。
1.2 研究内容和主要工作
本课题的研究主要内容是在国内外交互式主题建模的研究基础上,设计一个面向文本分析的交互式主题建模可视化分析系统。本文转载自http://www.biyezuopin.vip/onews.asp?id=15157系统使用非负矩阵分解(NMF)来作为主题建模的主要算法,并参考了 Jaegul Choo 等人提出的 UTOPIAN 主题建模可视化分析系统,对其中的半监督非负矩阵分解(SS-NMF)算法和多种用户交互方式进行了实现及改进。
本系统采用 Web 技术,后端算法和接口使用 Python 语言和 FastAPI Web 框架编写,前端界面使用 React&Ant Design 框架编写。主要工作可分为以下几个部分:
语料库的爬取、收集和整理,并写入数据库。
后端主题建模相关算法的编写。
后端 Web API 接口的编写。
前端界面的设计。
前端与后端的对接,调用后端数据接口,并将数据可视化。
前端用户交互逻辑的编写。
from typing import List
from fastapi import Depends, FastAPI, BackgroundTasks, Response, Cookie, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy.orm import Session
from typing import Optional, Dict
import crud, models, schemas
from database import SessionLocal
from TopicModelTrainingTask import *
import uuid
app = FastAPI()
# 配置跨域问题
origins = [
"http://localhost:8000",
"http://localhost:3000"
]
# 全局主题模型训练对象
topic_model_training_tasks: Dict[str, TopicModelTrainingTask] = {}
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 数据库连接依赖
def get_db():
try:
db = SessionLocal()
yield db
finally:
db.close()
@app.get(
"/bilibilivideos/",
response_model=List[schemas.BilibiliVideo],
tags=['bilibili视频评论语料库数据接口']
)
def get_bilibili_videos(db: Session = Depends(get_db)):
db_videos = crud.get_bilibili_videos(db)
return db_videos
@app.get(
"/bilibilivideos/{vid}/",
response_model=schemas.BilibiliVideo,
tags=['bilibili视频评论语料库数据接口']
)
def get_bilibili_videos_by_vid(vid: int, db: Session = Depends(get_db)):
db_video = crud.get_bilibili_videos_by_vid(vid, db)
return db_video
@app.get(
"/bilibilivideos/{vid}/comments/",
response_model=List[schemas.BilibiliVideoComment],
tags=['bilibili视频评论语料库数据接口']
)
def get_bilibili_video_comments_by_vid(vid: int, db: Session = Depends(get_db)):
db_comments = crud.get_bilibili_video_comments_by_vid(vid, db)
return db_comments
@app.get(
"/onlineShoppingReviews/",
response_model=List[schemas.OnlineShoppingReview],
tags=['电商购物评价语料库数据接口']
)
def get_online_shopping_reviews(db: Session = Depends(get_db)):
db_reviews = crud.get_online_shopping_reviews(db)
return db_reviews
@app.get(
"/takeawayReviews/",
response_model=List[schemas.TakeawayReview],
tags=['外卖评价语料库数据接口']
)
def get_takeaway_reviews(db: Session = Depends(get_db)):
db_reviews = crud.get_takeaway_reviews(db)
return db_reviews
@app.get(
"/chineseLyrics/",
response_model=List[schemas.ChineseLyrics],
tags=['中文歌歌词语料库数据接口']
)
def get_chinese_lyrics(db: Session = Depends(get_db)):
db_reviews = crud.get_chinese_lyrics(db)
return db_reviews
@app.get(
"/COVID19News/",
response_model=List[schemas.COVID19News],
tags=['新冠病毒新闻语料库数据接口']
)
def get_COVID19_news(db: Session = Depends(get_db)):
db_reviews = crud.get_COVID19_news(db)
return db_reviews
# 为每个用户设置uuid标识符,并存入Cookie
def create_user_uuid_and_set_cookie(response: Response):
user_uuid = str(uuid.uuid1())
response.set_cookie(key="user_uuid", value=user_uuid)
return user_uuid
@app.post(
"/calculate/preprocessing/",
tags=["计算模块数据接口"]
)
async def run_text_preprocessing_task(text_preprocessing_params: TextPreprocessingParams,
background_task: BackgroundTasks,
response: Response,
user_uuid: str = Cookie(None)):
if not user_uuid:
user_uuid = create_user_uuid_and_set_cookie(response)
global topic_model_training_tasks
task = TopicModelTrainingTask(text_preprocessing_params)
topic_model_training_tasks[user_uuid] = task
background_task.add_task(task.preprocessing)
return {"message": "已添加文本预处理后台任务"}
@app.post(
"/calculate/nmftraining/",
tags=["计算模块数据接口"]
)
async def run_nmf_training_and_tsne_task(nmf_training_params: NMFTrainingParams,
background_task: BackgroundTasks,
response: Response,
user_uuid: str = Cookie(None)):
if not user_uuid:
user_uuid = create_user_uuid_and_set_cookie(response)
global topic_model_training_tasks
if user_uuid and user_uuid in topic_model_training_tasks:
task = topic_model_training_tasks[user_uuid]
if task.text_preprocessing_progress.status_code == 2:
background_task.add_task(task.nmf_training, nmf_training_params)
return {"message": "已添加NMF主题模型训练后台任务"}
else:
raise HTTPException(status_code=404, detail="请先进行文本预处理任务!")
else:
raise HTTPException(status_code=404, detail="未找到相应的训练对象!")
@app.get(
"/calculate/nmftraining/keywordsearch/",
response_model=KeywordSearchResult,
tags=["用户交互"]
)
async def search_keyword(search_text: str,
response: Response,
user_uuid: str = Cookie(None)):
if not user_uuid:
user_uuid = create_user_uuid_and_set_cookie(response)
global topic_model_training_tasks
if user_uuid and user_uuid in topic_model_training_tasks:
task = topic_model_training_tasks[user_uuid]
if task.text_preprocessing_progress.status_code == 2:
search_text = search_text.lower()
bag_words = task.text_preprocessing.bagWords
keyword_search_result = KeywordSearchResult()
for wi, word in enumerate(bag_words):
if search_text in word:
keyword_search_result.word_id_list.append(wi)
keyword_search_result.word_list.append(word)
return keyword_search_result
else:
raise HTTPException(status_code=404, detail="请先进行文本预处理任务!")
else:
raise HTTPException(status_code=404, detail="未找到相应的训练对象!")
@app.post(
"/calculate/nmftraining/topickeywordoptimization/",
tags=["用户交互"]
)
async def run_topic_keyword_optimization_task(tko_params: TopicKeywordOptimizationParams,
background_task: BackgroundTasks,
response: Response,
user_uuid: str = Cookie(None)):
if not user_uuid:
user_uuid = create_user_uuid_and_set_cookie(response)
global topic_model_training_tasks
if user_uuid and user_uuid in topic_model_training_tasks:
task = topic_model_training_tasks[user_uuid]
if task.nmf_training_progress.status_code == 2:
background_task.add_task(task.topic_keyword_optimization, tko_params)
return {"message": "已添加主题关键词优化后台任务"}
else:
raise HTTPException(status_code=404, detail="请先等待NMF训练结束!")
else:
raise HTTPException(status_code=404, detail="未找到相应的训练对象!")
@app.post(
"/calculate/nmftraining/topicsplit/",
tags=["用户交互"]
)
async def run_topic_split_task(ts_params: TopicSplitParams,
background_task: BackgroundTasks,
response: Response,
user_uuid: str = Cookie(None)):
if not user_uuid:
user_uuid = create_user_uuid_and_set_cookie(response)
global topic_model_training_tasks
if user_uuid and user_uuid in topic_model_training_tasks:
task = topic_model_training_tasks[user_uuid]
if task.nmf_training_progress.status_code == 2:
background_task.add_task(task.topic_split, ts_params)
return {"message": "已添加主题拆分后台任务"}
else:
raise HTTPException(status_code=404, detail="请先等待NMF训练结束!")
else:
raise HTTPException(status_code=404, detail="未找到相应的训练对象!")
@app.post(
"/calculate/nmftraining/topicmerge/",
tags=["用户交互"]
)
async def run_topic_merge_task(tm_params: TopicMergeParams,
background_task: BackgroundTasks,
response: Response,
user_uuid: str = Cookie(None)):
if not user_uuid:
user_uuid = create_user_uuid_and_set_cookie(response)
global topic_model_training_tasks
if user_uuid and user_uuid in topic_model_training_tasks:
task = topic_model_training_tasks[user_uuid]
if task.nmf_training_progress.status_code == 2:
background_task.add_task(task.topic_merge, tm_params)
return {"message": "已添加主题合并后台任务"}
else:
raise HTTPException(status_code=404, detail="请先等待NMF训练结束!")
else:
raise HTTPException(status_code=404, detail="未找到相应的训练对象!")
@app.post(
"/calculate/nmftraining/keywordinducedtopiccreate/",
tags=["用户交互"]
)
async def run_keyword_induced_topic_create_task(kitc_params: KeywordInducedTopicCreateParams,
background_task: BackgroundTasks,
response: Response,
user_uuid: str = Cookie(None)):
if not user_uuid:
user_uuid = create_user_uuid_and_set_cookie(response)
global topic_model_training_tasks
if user_uuid and user_uuid in topic_model_training_tasks:
task = topic_model_training_tasks[user_uuid]
if task.nmf_training_progress.status_code == 2:
background_task.add_task(task.keyword_induced_topic_create, kitc_params)
return {"message": "已添加关键词诱导主题创建后台任务"}
else:
raise HTTPException(status_code=404, detail="请先等待NMF训练结束!")
else:
raise HTTPException(status_code=404, detail="未找到相应的训练对象!")
@app.post(
"/calculate/nmftraining/documentinducedtopiccreate/",
tags=["用户交互"]
)
async def run_document_induced_topic_create_task(ditc_params: DocumentInducedTopicCreateParams,
background_task: BackgroundTasks,
response: Response,
user_uuid: str = Cookie(None)):
if not user_uuid:
user_uuid = create_user_uuid_and_set_cookie(response)
global topic_model_training_tasks
if user_uuid and user_uuid in topic_model_training_tasks:
task = topic_model_training_tasks[user_uuid]
if task.nmf_training_progress.status_code == 2:
background_task.add_task(task.document_induced_topic_create, ditc_params)
return {"message": "已添加文档诱导主题创建后台任务"}
else:
raise HTTPException(status_code=404, detail="请先等待NMF训练结束!")
else:
raise HTTPException(status_code=404, detail="未找到相应的训练对象!")
@app.get(
"/calculate/nmftraining/newdoctopicdistributionpredict/",
tags=["主题预测"]
)
async def run_new_document_topic_distribution_predict_task(
new_doc_text: str,
background_task: BackgroundTasks,
response: Response,
user_uuid: str = Cookie(None)):
if not user_uuid:
user_uuid = create_user_uuid_and_set_cookie(response)
global topic_model_training_tasks
if user_uuid and user_uuid in topic_model_training_tasks:
task = topic_model_training_tasks[user_uuid]
if task.nmf_training_progress.status_code == 2:
background_task.add_task(
task.new_doc_topic_distribution_predict,
new_doc_text
)
return {"message": "已添加新文档主题分布预测后台任务"}
else:
raise HTTPException(status_code=404, detail="请先等待NMF训练结束!")
else:
raise HTTPException(status_code=404, detail="未找到相应的训练对象!")
@app.get(
"/calculate/preprocessing/progress/",
tags=["计算模块数据接口"],
response_model=TextPreprocessingProgress
)
def get_text_preprocessing_task_progress(user_uuid: str = Cookie(None)):
global topic_model_training_tasks
if (not user_uuid) or (user_uuid not in topic_model_training_tasks):
raise HTTPException(status_code=404, detail="未找到相应的训练对象!")
return topic_model_training_tasks[user_uuid].text_preprocessing_progress
@app.get(
"/calculate/nmftraining/progress/",
tags=["计算模块数据接口"],
response_model=NMFTrainingProgress
)
def get_nmf_training_task_progress(user_uuid: str = Cookie(None)):
global topic_model_training_tasks
if (not user_uuid) or (user_uuid not in topic_model_training_tasks):
raise HTTPException(status_code=404, detail="未找到相应的训练对象!")
task = topic_model_training_tasks[user_uuid]
if task.text_preprocessing_progress.status_code == 2:
return task.nmf_training_progress
else:
raise HTTPException(status_code=404, detail="请先进行文本预处理任务!")
@app.get(
"/calculate/umap/progress/",
tags=["计算模块数据接口"],
response_model=UMAPProgress
)
def get_umap_task_progress(user_uuid: str = Cookie(None)):
global topic_model_training_tasks
if (not user_uuid) or (user_uuid not in topic_model_training_tasks):
raise HTTPException(status_code=404, detail="未找到相应的训练对象!")
task = topic_model_training_tasks[user_uuid]
if task.nmf_training_progress.status_code == 2:
return task.umap_progress
else:
raise HTTPException(status_code=404, detail="请先等待NMF训练结束!")
@app.get(
"/calculate/nmftraining/predict/progress/",
tags=["计算模块数据接口"],
response_model=NewDocTopicDistributionPredictProgress
)
def get_new_doc_topic_distribution_predict_task_progress(user_uuid: str = Cookie(None)):
global topic_model_training_tasks
if (not user_uuid) or (user_uuid not in topic_model_training_tasks):
raise HTTPException(status_code=404, detail="未找到相应的训练对象!")
task = topic_model_training_tasks[user_uuid]
if task.nmf_training_progress.status_code == 2:
return task.topic_distribution_predict_progress
else:
raise HTTPException(status_code=404, detail="请先等待NMF训练结束!")
@app.get(
"/calculate/details/document/",
tags=["计算模块数据接口"],
response_model=DocumentDetails
)
def get_document_details(doc_id: int, user_uuid: str = Cookie(None)):
global topic_model_training_tasks
if (not user_uuid) or (user_uuid not in topic_model_training_tasks):
raise HTTPException(status_code=404, detail="未找到相应的训练对象!")
task = topic_model_training_tasks[user_uuid]
if task.nmf_training_progress.status_code == 2:
return task.get_document_details(doc_id)
else:
raise HTTPException(status_code=404, detail="请先等待NMF训练结束!")
@app.get(
"/calculate/details/topic/",
tags=["计算模块数据接口"],
response_model=TopicDetails
)
def get_topic_details(topic_id: int, user_uuid: str = Cookie(None)):
global topic_model_training_tasks
if (not user_uuid) or (user_uuid not in topic_model_training_tasks):
raise HTTPException(status_code=404, detail="未找到相应的训练对象!")
task = topic_model_training_tasks[user_uuid]
if task.nmf_training_progress.status_code == 2:
return task.get_topic_details(topic_id)
else:
raise HTTPException(status_code=404, detail="请先等待NMF训练结束!")
@app.get(
"/calculate/userinteraction/info/keyword/",
tags=["计算模块数据接口"],
response_model=TopicKeywordInfo
)
def get_topic_keyword_info(topic_id: int, user_uuid: str = Cookie(None)):
global topic_model_training_tasks
if (not user_uuid) or (user_uuid not in topic_model_training_tasks):
raise HTTPException(status_code=404, detail="未找到相应的训练对象!")
task = topic_model_training_tasks[user_uuid]
if task.nmf_training_progress.status_code == 2:
return task.get_topic_keyword_info(topic_id)
else:
raise HTTPException(status_code=404, detail="请先等待NMF训练结束!")
@app.get(
"/calculate/userinteraction/info/topicmergekeyword/",
tags=["计算模块数据接口"],
response_model=TopicMergeKeywordInfo
)
def get_topic_merge_keyword_info(topic1_id: int, topic2_id: int, user_uuid: str = Cookie(None)):
global topic_model_training_tasks
if (not user_uuid) or (user_uuid not in topic_model_training_tasks):
raise HTTPException(status_code=404, detail="未找到相应的训练对象!")
task = topic_model_training_tasks[user_uuid]
if task.nmf_training_progress.status_code == 2:
return task.get_topic_merge_keyword_info(topic1_id, topic2_id)
else:
raise HTTPException(status_code=404, detail="请先等待NMF训练结束!")