关于我用AI编写了一个聊天机器人……(11)
本次依旧使用豆包实现代码。
概述
这个聊天机器人主要通过以下步骤实现问答功能:
- 从训练数据文件加载问答对
- 对问题进行预处理和关键词提取
- 计算每个关键词的 IDF(逆文档频率)值
- 接收用户输入并进行处理
- 首先尝试精确匹配已有问题
- 精确匹配失败则使用 TF-IDF 算法进行相似性匹配
- 返回最匹配的答案
下面详细解析各个部分的实现。
核心功能模块解析
1. 文本预处理函数
字符串转小写
string toLower(const string& str) {string result = str;for (string::size_type i = 0; i < result.length(); ++i) {result[i] = tolower(result[i]);}return result;
}
这个函数将输入字符串转换为全小写形式,目的是实现大小写不敏感的匹配,提高关键词匹配的准确性。
关键词提取
vector<string> extractKeywords(const string& text) {vector<string> keywords;string word;for (string::const_iterator it = text.begin(); it != text.end(); ++it) {if (isalnum(*it)) {word += *it;} else if (!word.empty()) {keywords.push_back(toLower(word));word.clear();}}if (!word.empty()) {keywords.push_back(toLower(word));}return keywords;
}
这个函数从文本中提取关键词,规则是:
- 只保留字母和数字(isalnum 检查)
- 其他字符作为分隔符
- 将提取的词转为小写后返回
2. TF-IDF 算法实现
TF-IDF(词频 - 逆文档频率)是一种用于信息检索与数据挖掘的常用加权技术,代码中通过getBestAnswerByTFIDF
函数实现:
string getBestAnswerByTFIDF(const vector<string>& userKeywords,const map<string, vector<string> >& qas,const map<string, vector<string> >& questionKeywords,const map<string, double>& idfValues) {// 实现细节见原代码
}
这个函数的工作流程分为三步:
步骤 1:计算用户问题的 TF-IDF 向量
- 词频 (TF):关键词在用户问题中出现的频率除以问题总词数
- 逆文档频率 (IDF):从预计算的 idfValues 中获取
- TF-IDF:TF 与 IDF 的乘积
步骤 2:计算每个问题的相似度
使用余弦相似度公式计算用户问题与训练数据中每个问题的相似度:
相似度 = 点积(userTFIDF, questionTFIDF) / (||userTFIDF|| × ||questionTFIDF||)
其中点积是两个向量对应元素乘积的和,||x|| 是向量的模长。
步骤 3:找到最佳匹配
- 选取相似度最高的问题
- 如果相似度超过阈值(0.2),返回对应的答案
- 否则返回空字符串表示没有找到匹配
3. 主程序逻辑
main 函数实现了系统的整体流程:
- 数据加载:从 training_data.txt 文件加载问答对
- 数据预处理:提取关键词,计算文档频率和 IDF 值
- 交互循环:
- 接收用户输入
- 先尝试精确匹配
- 精确匹配失败则使用 TF-IDF 进行模糊匹配
- 返回最佳答案或提示无法回答
训练数据格式
系统使用 training_data.txt 文件存储训练数据,格式如下:
Q:问题1
A:回答1
(空行)
Q:问题2
A:回答2第一行
回答2第二行
(空行)
...
- 问题行以 "Q:" 开头
- 回答行以 "A:" 开头
- 回答可以是多行
- 空行表示一个问答对的结束
系统特点与改进方向
特点
- 结合精确匹配和模糊匹配两种方式
- 使用 TF-IDF 算法处理语义相似的问题
改进方向
- 增加停用词过滤(如 "的"、"是" 等无意义词)
- 实现更复杂的文本预处理(如词干提取)
- 调整相似度阈值以优化系统性能
- 增加机器学习模型提高匹配准确性
代码
#include <iostream>
#include <fstream>
#include <string>
#include <map>
#include <vector>
#include <cctype>
#include <cmath>
#include <algorithm>
#include <set>
using namespace std;// 将字符串转换为小写
string toLower(const string& str) {string result = str;for (string::size_type i = 0; i < result.length(); ++i) {result[i] = tolower(result[i]);}return result;
}// 从字符串中提取关键词
vector<string> extractKeywords(const string& text) {vector<string> keywords;string word;for (string::const_iterator it = text.begin(); it != text.end(); ++it) {if (isalnum(*it)) {word += *it;} else if (!word.empty()) {keywords.push_back(toLower(word));word.clear();}}if (!word.empty()) {keywords.push_back(toLower(word));}return keywords;
}// 计算TF-IDF并返回最佳匹配答案
string getBestAnswerByTFIDF(const vector<string>& userKeywords,const map<string, vector<string> >& qas,const map<string, vector<string> >& questionKeywords,const map<string, double>& idfValues) {// 计算用户问题的TF-IDF向量map<string, double> userTFIDF;for (vector<string>::const_iterator kit = userKeywords.begin(); kit != userKeywords.end(); ++kit) {const string& keyword = *kit;// 计算词频(TF)double tf = 0.0;for (vector<string>::const_iterator it = userKeywords.begin(); it != userKeywords.end(); ++it) {if (*it == keyword) tf++;}tf /= userKeywords.size();// 获取IDF值double idf = 0.0;map<string, double>::const_iterator idfIt = idfValues.find(keyword);if (idfIt != idfValues.end()) {idf = idfIt->second;}// 计算TF-IDFuserTFIDF[keyword] = tf * idf;}// 计算每个问题的相似度map<string, double> similarityScores;for (map<string, vector<string> >::const_iterator pit = questionKeywords.begin(); pit != questionKeywords.end(); ++pit) {const string& question = pit->first;const vector<string>& keywords = pit->second;// 计算问题的TF-IDF向量map<string, double> questionTFIDF;for (vector<string>::const_iterator kit = keywords.begin(); kit != keywords.end(); ++kit) {const string& keyword = *kit;// 计算词频(TF)double tf = 0.0;for (vector<string>::const_iterator it = keywords.begin(); it != keywords.end(); ++it) {if (*it == keyword) tf++;}tf /= keywords.size();// 获取IDF值double idf = 0.0;map<string, double>::const_iterator idfIt = idfValues.find(keyword);if (idfIt != idfValues.end()) {idf = idfIt->second;}// 计算TF-IDFquestionTFIDF[keyword] = tf * idf;}// 计算余弦相似度double dotProduct = 0.0;double userNorm = 0.0;double questionNorm = 0.0;// 计算点积和范数for (map<string, double>::const_iterator uit = userTFIDF.begin(); uit != userTFIDF.end(); ++uit) {const string& keyword = uit->first;double userWeight = uit->second;userNorm += userWeight * userWeight;map<string, double>::const_iterator qit = questionTFIDF.find(keyword);if (qit != questionTFIDF.end()) {dotProduct += userWeight * qit->second;}}for (map<string, double>::const_iterator qit = questionTFIDF.begin(); qit != questionTFIDF.end(); ++qit) {double questionWeight = qit->second;questionNorm += questionWeight * questionWeight;}userNorm = sqrt(userNorm);questionNorm = sqrt(questionNorm);// 计算相似度double similarity = 0.0;if (userNorm > 0 && questionNorm > 0) {similarity = dotProduct / (userNorm * questionNorm);}similarityScores[question] = similarity;}// 找到相似度最高的问题string bestQuestion;double maxSimilarity = 0.0;for (map<string, double>::const_iterator it = similarityScores.begin(); it != similarityScores.end(); ++it) {if (it->second > maxSimilarity) {maxSimilarity = it->second;bestQuestion = it->first;}}// 如果相似度足够高,返回对应的答案if (maxSimilarity >= 0.2) { // 相似度阈值map<string, vector<string> >::const_iterator ansIt = qas.find(bestQuestion);if (ansIt != qas.end() && !ansIt->second.empty()) {return ansIt->second[0]; // 假设第一个答案是最佳答案}}return ""; // 没有找到匹配
}int main() {// 存储训练数据map<string, string> exactAnswers; // 精确匹配回答map<string, vector<string> > qas; // 问题-回答映射map<string, vector<string> > questionKeywords; // 问题-关键词映射map<string, int> documentFrequency; // 关键词-文档频率映射// 加载训练数据ifstream trainingFile("training_data.txt");if (trainingFile.is_open()) {string line;string question = "";bool readingAnswer = false;int totalDocuments = 0;while (getline(trainingFile, line)) {// 空行表示一个问答对结束if (line.empty()) {question = "";readingAnswer = false;continue;}// 问题行以Q:开头if (line.substr(0, 2) == "Q:") {question = line.substr(2);readingAnswer = false;totalDocuments++;}// 回答行以A:开头else if (line.substr(0, 2) == "A:") {if (!question.empty()) {string answer = line.substr(2);// 保存精确匹配回答exactAnswers[question] = answer;// 保存问题-回答映射qas[question].push_back(answer);// 提取关键词并保存vector<string> keywords = extractKeywords(question);questionKeywords[question] = keywords;// 更新文档频率set<string> uniqueKeywords;for (vector<string>::const_iterator it = keywords.begin(); it != keywords.end(); ++it) {uniqueKeywords.insert(*it);}for (set<string>::const_iterator it = uniqueKeywords.begin(); it != uniqueKeywords.end(); ++it) {documentFrequency[*it]++;}}readingAnswer = true;}// 多行回答的后续行else if (readingAnswer && !question.empty()) {exactAnswers[question] += "\n" + line;qas[question].back() += "\n" + line;}}trainingFile.close();cout << "已加载 " << exactAnswers.size() << " 条训练数据" << endl;// 计算IDF值map<string, double> idfValues;for (map<string, int>::const_iterator it = documentFrequency.begin(); it != documentFrequency.end(); ++it) {const string& keyword = it->first;int df = it->second;// IDF公式: log(总文档数 / (包含该词的文档数 + 1)) + 1double idf = log((double)totalDocuments / (df + 1)) + 1;idfValues[keyword] = idf;}// 聊天界面cout << "Hello! 输入 'exit' 结束对话。" << endl;string input;while (true) {cout << "You: ";getline(cin, input);if (input == "exit") {cout << "Robot: Goodbye!" << endl;break;}// 精确匹配map<string, string>::const_iterator exactIt = exactAnswers.find(input);if (exactIt != exactAnswers.end()) {cout << "Robot: " << exactIt->second << endl;continue;}// 关键词匹配 (TF-IDF)vector<string> userKeywords = extractKeywords(input);string bestAnswer = getBestAnswerByTFIDF(userKeywords, qas, questionKeywords, idfValues);if (!bestAnswer.empty()) {cout << "Robot: " << bestAnswer << endl;continue;}// 没有找到匹配cout << "Robot: I don't know how to answer this question." << endl;}} else {cout << "无法打开训练文件 training_data.txt" << endl;}return 0;
}
也可以去我主页找相应资源下载
感谢阅读!