FAQ Python + NLTK 搭建-NLTK模型(二)

背景

上一篇文章中介绍了如何将word和md导出为Json, 此事是第一步,有了Json才能给NLTK模型进行匹配,调试,训练。

设计

NLTK的设计分为一下几个步骤:

  1. 读取faq.json数据集
  2. 构建问题字典
  3. 文本预处理,分句,分词,去除通用词,词干提取
  4. 匹配问题函数,使用BM25召回算法,使用最高分进行问题回复
  5. 返回配置的回复
  6. 封装CRUD数据库,使用脚本将大Json写入到DB里,对DB操作大大提高查询性能
  7. 封装Python对外 Public API,供业务层使用
  8. 部署本机环境
  9. 编写一个简单的html进行测试

1.文本预处理,将文本拆分为句子子列表

def split_sentences(text: str) -> List[str]:
    """将文本拆分为句子列表"""
    return re.split(r'[.!?\n]+', text)

2.对文本进行预处理,包括分词,停用词删除

def preprocess_text(text: str) -> List[str]:
    """对文本进行预处理,包括分词和停用词删除"""
    tokens = word_tokenize(text.lower())
    stop_words = set(stopwords.words('english'))
    return [token for token in tokens if token not in stop_words and token.isalnum() and token not in {'p', 'ol', 'li', 'img', 'https', 'strong'}]

3.使用snowballStemmer 对单词进行词干提取

def stem_words(words: List[str]) -> List[str]:
    """使用 SnowballStemmer 对单词进行词干提取"""
    stemmer = SnowballStemmer("english")
    return [stemmer.stem(word) for word in words]

4.匹配问题,使用BM25相关新计算得分

def bm25_score(query: List[str], doc: List[str], question_list: List[dict], k1=1.5, b=0.75) -> float:
    """
    计算BM25相关性得分
    """
    score = 0
    doc_length = len(doc)  # 计算文档长度一次
    term_freq = {term: doc.count(term) for term in query}  # 预计算词频
    for term in query:
        tf = term_freq[term]
        doc_containing_term = sum(1 for q in question_list for q_text in [q['question']] if term in q_text)
        if doc_containing_term >= len(question_list):
            idf = 0
        else:
            idf = log((len(question_list) - doc_containing_term + 0.5) / (doc_containing_term + 0.5))
        score += idf * (tf * (k1 + 1)) / (tf + k1 * (1 - b + b * doc_length / 300))  # 使用预计算的文档长度
    return score

5. 匹配问题逻辑

匹配问题并返回答案,业务API

  • 优先匹配keyWords,
  • 当keyWords匹配结果是None,则使用question匹配最高分
  • 当前两个步骤都没匹配到,则最终匹配answer中的关键词,这里就会存在较大误差
  • 最终任何一个都没匹配到则返回None
def match_question(query: str, q_platform: str) -> Tuple[str, str]:
    """
    匹配问题并返回答案
    """
    query_words = preprocess_text(query)
    query_words = stem_words(query_words)
    question_arr = get_question_dict(q_platform)
    best_score = 0
    best_answer = None
    
    for questions in question_arr:
        keywords = questions['keywords']
        answer = questions['answer']
        answer_words = preprocess_text(keywords)
        answer_words = stem_words(answer_words)
        answer_words = [word for word in answer_words if word.isalnum()]
                    #print(f"answer_words: {answer_words}\n\n")
        score = bm25_score(query_words, answer_words, question_arr)
        if score > best_score and answer:  # 只在找到更好的分数时更新
            best_score = score
            best_answer = answer

    if best_answer is None:
        for questions in question_arr:
            keywords = questions['question']
            answer = questions['answer']
            answer_words = preprocess_text(keywords)
            answer_words = stem_words(answer_words)
            score = bm25_score(query_words, answer_words, question_arr)

            if score == 0:
                if all(word in answer_words for word in query_words):
                    best_answer = answer
                    break
            elif score > best_score and answer:
                best_score = score
                best_answer = answer

    if best_answer is None:
        for questions in question_arr:
            keywords = questions['answer']
            answer = questions['answer']
            answer_words = preprocess_text(answer)
            answer_words = stem_words(answer_words)
                    # 移除符号字符串
            answer_words = [word for word in answer_words if word.isalnum()]
                    #print(f"answer_words: {answer_words}\n\n")
            score = bm25_score(query_words, answer_words, question_arr)

            if score > best_score and answer:  # 只在找到更好的分数时更新
                best_score = score
                best_answer = answer
    
    return best_answer or "Sorry, I couldn't find a suitable answer."

6.封装一些CRUD DB方法,用来进行1级问题,2级问题,3级问题检索

def get_answer_by_id(question_ids: str, platform: str) -> List[Dict[str, Union[str, int]]]:  # 修改参数类型为str,返回类型为List[Dict[str, Union[str, int]]]
    """
    通过问题ID返回答案
    """

    question_id_list = question_ids.split(',')  

    # 从DB查询

    answers = DB.sqlManager.get_faq_by_id(question_id_list,None, int(platform_auto(platform)))

    if answers:
        return answers

    return answers

#获取一级标题
def get_top_level_questions(platform: str) -> List[Tuple[int, str, str]]:
    
    top_level_questions = DB.sqlManager.get_faq_by_id(None, [0], int(platform_auto(platform)))  # Ensure parent_id=0 is passed
    
    return top_level_questions

#获取id下的子问题
def get_subQuestions_by_id(p_id: int, platform: str) -> List[Tuple[int, int, str, str]]:
  
    matching_questions = DB.sqlManager.get_faq_by_id(None,[p_id], int(platform_auto(platform)))

    return matching_questions

def upsert_faq_from_json(faq_data):
    return DB.sqlManager.upsert_faq_from_json(faq_data)

7.封装Python sqlManager.py

  • 建表,要对id,parentId, platform建立表索引,提高查询性能
class FAQ(Base):
    __tablename__ = 'faqs'
    id = Column(Integer, primary_key=True, index=True)
    parentId = Column(Integer, index=True)  # Added index for performance
    question = Column(String(255))
    answer = Column(Text)
    keywords = Column(Text)
    isLast = Column(Integer)
    platform = Column(Integer, index=True)  # Added index for performance
    type = Column(Integer)

    def to_dict(self):
        return {
            'id': self.id,
            'parentId': self.parentId,
            'question': self.question,
            'answer': self.answer,
            'keywords': self.keywords,
            'isLast': self.isLast,
            'platform': self.platform,
            'type': self.type
        }

7.1封装的CRUD要加锁,保证数据的原子性

# 增加数据
def add_faq_from_json(json_data):
    session = Session()
    try:
        faqs = [FAQ(**item) for item in json_data]
        session.bulk_save_objects(faqs)
        session.commit()
    except SQLAlchemyError as e:
        session.rollback()
        print(f"Error: {e}")
    finally:
        session.close()

部署本机Python后台服务

新建一个run.py, 用来运行后台服务

  • 封装查询API:
app = Flask(__name__)
CORS(app)
@app.route('/v1/faq', methods=['GET'])
def handle_faq_request():
    user_query = request.args.get('query')
    platform = request.args.get('platform')
    if user_query:
        try:
            answer = kami_ntlk.match_question(user_query, platform)
            return jsonify({'code': 200, 'data': answer})
        except Exception as e:
            return jsonify({'code': 502, 'data': 'An error occurred: {}'.format(str(e))}), 400
    else:
        return jsonify({'code': 502, 'data': 'No query parameter provided.'}), 400
  • 使用WGIServer创建服务
if __name__ == '__main__':
# Debug
    #app.run(host='0.0.0.0', port=8000, debug=False)
# Production
    http_server = WSGIServer(('', 8000), app)
    http_server.serve_forever()
  • 运行
    -- python run.py
    -- python -m http.server 8000 --bind 0.0.0.0 开启本机服务

写一个简单的Html进行测试:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,864评论 6 494
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,175评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,401评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,170评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,276评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,364评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,401评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,179评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,604评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,902评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,070评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,751评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,380评论 3 319
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,077评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,312评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,924评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,957评论 2 351