第二章:自然语言处理 (NLP) 核心技术:AI机器人理解世界的钥匙
自然语言处理(NLP)是人工智能领域的一个重要分支,它赋予了计算机理解、解释、生成和操纵人类语言的能力。对于会聊天的AI机器人而言,NLP是其理解用户意图、分析用户情绪、生成自然回复以及从海量文本中提取知识的核心。在本章中,我们将从最基础的文本预处理开始,层层深入,剖析各种NLP核心技术,揭示它们如何让AI机器人能够“听懂”人类的语言。
2.1 文本预处理:原始语言到可计算形式的转化
原始的用户输入文本通常是“脏乱”的:可能包含各种标点符号、特殊字符、大小写混杂、词语之间没有明确边界等。在将这些文本喂给AI模型进行分析之前,必须进行一系列的预处理步骤,将其转化为机器能够理解和处理的标准化、结构化形式。这一过程是所有NLP任务的基础。
2.1.1 分词 (Tokenization):语言单位的切割
分词是将连续的文本序列切分成有意义的语言单位(称为“词元”或“Token”)的过程。这些词元可以是单词、词组、标点符号,甚至是单个字符。分词是NLP的第一个关键步骤,其准确性直接影响后续所有任务的效果。
-
概念与重要性
- 概念: 分词就是把一句话或一段文字,按照一定的规则,切分成一个个独立的、有意义的最小语言单位。在英语等以空格分隔单词的语言中,分词相对简单;但在中文等没有天然分隔符的语言中,分词是一个复杂且关键的挑战。
- 重要性:
- 意义单元: 计算机无法直接理解人类语言的含义,它需要将语言分解为可计算的最小语义单元。
- 特征提取: 词元是后续特征提取(如词袋模型、TF-IDF、词嵌入)的基础。没有正确的分词,就无法构建有效的文本特征。
- 上下文理解: 某些高级NLP模型(如基于Transformer的模型)虽然能处理字符序列,但分词仍然是它们的预处理层,将文本映射到模型词汇表中的ID。
- 消除歧义: 某些多义词需要根据上下文进行正确分词,才能消除歧义。
-
中英文分词的差异与挑战
-
英文分词:
- 特点: 英文单词之间通常有空格作为自然分隔符,这使得基于空格的分词成为可能。
- 挑战: 仍然存在一些挑战,例如:
- 连字符词:
high-quality
应该是一个词还是两个词? - 缩写与所有格:
don't
,I'm
,users'
应该如何分词? - 标点符号:
.
,
!
等是应该作为独立词元还是与单词合并? - 数字和特殊符号:
12.5
,$100
,[email protected]
。
- 连字符词:
import re # 导入正则表达式模块 def simple_english_tokenize(text: str) -> list[str]: # 定义一个简单的英文分词函数 """ 使用正则表达式进行英文分词。 将文本转换为小写,并分割为字母序列和数字,忽略其他标点。 """ # r'\b\w+\b' 匹配单词边界的字母数字串 # 或者更简单的:re.findall(r'[a-z]+|\d+', text.lower()) words = re.findall(r'[a-z]+', text.lower()) # 将文本转小写,并用正则表达式匹配所有连续的字母,返回列表 return words # 返回分词结果 english_text = "Hello, world! I'm learning Python NLP. It's awesome! My score is 98.5." # 英文文本 tokens = simple_english_tokenize(english_text) # 调用分词函数 print(f"英文分词结果: { tokens}") # 打印分词结果
实际的英文分词库(如NLTK、SpaCy)会更复杂,能够处理上述挑战,并提供词性标注、命名实体识别等功能。
-
中文分词 (Chinese Word Segmentation, CWS):
- 特点: 中文句子由连续的汉字组成,词语之间没有空格作为分隔符。一个词语可能由一个、两个或多个汉字组成。例如,“上海市”是一个词,“上海”和“市”也是词。
- 挑战:
- 词典覆盖率问题: 新词(如网络流行语、专业术语、人名地名)不断涌现,现有词典无法完全覆盖。
- 歧义消解:
- 交集型歧义: “发展中国家”可以被切分为“发展/中国/家”或“发展/中国家”。
- 组合型歧义: “北京大学”是“北京/大学”还是“北京大学”一个词。
- 未登录词识别: 词典中没有的词语,如人名、地名、机构名等专有名词。
# 中文文本没有天然的空格分隔 chinese_text_no_spaces = "我爱北京天安门" # 中文文本 # print(chinese_text_no_spaces.split(' ')) # 这将无法正确分词
-
-
中文分词的常见方法
-
基于词典的分词 (Dictionary-Based Tokenization)
也称为字符串匹配分词,是最直观和基础的方法。它依赖于一个预先构建好的词典,通过匹配文本中最长的词或按照特定规则(如最大匹配法)从词典中查找词语。- 最大匹配法 (Maximum Matching Method, MM):
- 正向最大匹配 (Forward Maximum Matching, FMM): 从文本的起始位置开始,每次取词典中最长的词进行匹配。
- 逆向最大匹配 (Backward Maximum Matching, BMM): 从文本的末尾位置开始,每次取词典中最长的词进行匹配。
- 双向最大匹配 (Bidirectional Maximum Matching, BiMM): 同时进行FMM和BMM,然后比较结果,选择分词数量最少、或者词典匹配度最高的那个结果。通常,BMM在中文分词中表现略优于FMM。
def custom_fmm_tokenizer(text: str, lexicon: set) -> list[str]: # 定义一个自定义的正向最大匹配分词函数 """ 实现简单的正向最大匹配 (FMM) 分词算法。 :param text: 待分词的中文文本。 # 参数text的中文解释 :param lexicon: 包含词语的集合(作为词典)。 # 参数lexicon的中文解释 :return: 分词后的词语列表。 # 返回值的中文解释 """ tokens = [] # 存储分词结果的列表 text_len = len(text) # 获取文本长度 max_word_len = max(len(word) for word in lexicon) if lexicon else 1 # 获取词典中最长词的长度,如果词典为空则为1 i = 0 # 当前处理的文本起始位置 while i < text_len: # 循环直到文本处理完毕 found = False # 标记是否找到词 # 尝试从最长词开始匹配 for j in range(min(i + max_word_len, text_len), i, -1): # 从当前位置向后截取长度递减的子串,最长为max_word_len word = text[i:j] # 截取子串 if word in lexicon: # 如果子串在词典中 tokens.append(word) # 将词语添加到结果列表 i = j # 更新当前处理位置到词语的末尾 found = True # 标记已找到词 break # 跳出内层循环,处理下一个词 if not found: # 如果在词典中没有找到匹配的词 tokens.append(text[i]) # 将当前字符作为一个词元(未登录词处理) i += 1 # 移动到下一个字符 return tokens # 返回分词结果 # 示例词典 (小型) custom_chinese_lexicon = { # 定义一个自定义的中文词典 "北京", "北京大学", "大学", "学生", "热爱", "祖国", "我", "爱", "天安门", "上海", "浦东", "开发", "浦东开发", "研究", "研究生", "生命", "科学", "生命科学" # 包含多个词语,有重叠词 } text_fmm_1 = "我热爱北京大学的学生。" # 测试文本1 tokens_fmm_1 = custom_fmm_tokenizer(text_fmm_1, custom_chinese_lexicon) # 调用FMM分词 print(f"FMM分词 (文本1): { tokens_fmm_1}") # 打印分词结果 (可能分出“北京/大学”而不是“北京大学”) text_fmm_2 = "浦东开发与生命科学研究。" # 测试文本2 tokens_fmm_2 = custom_fmm_tokenizer(text_fmm_2, custom_chinese_lexicon) # 调用FMM分词 print(f"FMM分词 (文本2): { tokens_fmm_2}") # 打印分词结果
基于词典的分词方法简单高效,但对词典的依赖性强,无法处理未登录词和歧义消解能力有限。
- 最大匹配法 (Maximum Matching Method, MM):
-
基于统计模型的分词 (Statistical Model-Based Tokenization)
这种方法将分词视为一个序列标注问题,通过统计文本中词语出现的频率以及词语之间的搭配概率来判断切分点。常见的模型包括:- 隐马尔可夫模型 (Hidden Markov Model, HMM): 将每个汉字视为一个状态,状态有“词首B”、“词中M”、“词尾E”、“单字S”四种,通过Viterbi算法找到最可能的状态序列,从而确定分词结果。
- 条件随机场 (Conditional Random Field, CRF): 比HMM更先进,能够考虑更长的上下文信息,并避免HMM的“独立性假设”。在早期的中文分词中表现优异。
- 字标注法: 将分词问题转化为对每个汉字进行标注(如B、M、E、S),然后通过机器学习模型(如HMM、CRF、或更复杂的神经网络)进行序列标注。
这些方法通常需要大量的标注语料进行训练。
-
基于深度学习的分词 (Deep Learning-Based Tokenization)
现代中文分词通常采用深度学习模型,如Bi-LSTM-CRF、或基于Transformer的模型。这些模型能够自动学习文本特征和词语边界,在处理未登录词和歧义消解方面表现更出色,并且能够结合预训练语言模型(如BERT、ERNIE)的强大语义理解能力。
-
-
实际Python库应用:
jieba
(结巴分词)
jieba
是一个功能强大的中文分词工具,结合了基于词典和基于统计的方法,并支持自定义词典。它是中文NLP领域的“Hello World”级工具。-
安装
jieba
:pip install jieba # 安装jieba库
-
基本分词模式:
jieba
提供三种分词模式:- 精确模式 (Default): 试图将句子最精确地切开,适合文本分析。
- 全模式: 把句子中所有可以成词的词语都扫描出来, 速度快,但会有冗余。
- 搜索引擎模式: 在精确模式的基础上,对长词再次切分,提高召回率,适合搜索引擎。
import jieba # 导入jieba库 text_jieba = "我爱北京天安门,上海浦东开发开放。" # 中文文本 print("--- 精确模式 ---") # 打印提示 seg_list_exact = jieba.cut(text_jieba, cut_all=False) # 精确模式分词 (cut_all=False是默认值) print("分词结果:", "/ ".join(seg_list_exact)) # 打印分词结果,用斜杠连接 print("\n--- 全模式 ---") # 打印提示 seg_list_full = jieba.cut(text_jieba, cut_all=True) # 全模式分词 print("分词结果:", "/ ".join(seg_list_full)) # 打印分词结果 print("\n--- 搜索引擎模式 ---") # 打印提示 seg_list_search = jieba.cut_for_search(text_jieba) # 搜索引擎模式分词 print("分词结果:", "/ ".join(seg_list_search)) # 打印分词结果
-
自定义词典
jieba
支持加载用户自定义的词典,以提高分词的准确性,特别是对于专业领域词汇、人名、地名、新词等。-
创建词典文件:
user_dict.txt
格式:词语 词频 词性
(词频和词性可选,词频越大越倾向于被分出来,词性通常不影响分词结果)人工智能 10 n AI机器人 20 n 健康智荐 15 n 深度学习 12 n 情绪洞察 18 n
-
加载词典:
import jieba # 导入jieba库 import os # 导入os模块 # 创建一个临时的用户词典文件用于演示 user_dict_path = "user_dict.txt" # 用户词典文件路径 with open(user_dict_path, "w", encoding="utf-8") as f: # 以写入模式打开文件 f.write("人工智能 10 n\n") # 写入词语、词频、词性 f.write("AI机器人 20 n\n") # 写入词语、词频、词性 f.write("健康智荐 15 n\n") # 写入词语、词频、词性 f.write("情绪洞察 18 n\n") # 写入词语、词频、词性 f.write("睡眠分析 12 n\n") # 写入词语、词频、词性 f.write("自然语言处理 18 n\n") # 写入词语、词频、词性 print(f"创建了用户词典文件: { user_dict_path}") # 打印创建提示 jieba.load_userdict(user_dict_path) # 加载用户自定义词典 text_with_new_words = "AI机器人会分析你的情绪和进行健康智荐,其中涉及到人工智能和自然语言处理技术。" # 包含新词的文本 print("\n--- 加载自定义词典后的精确模式分词 ---") # 打印提示 seg_list_custom = jieba.cut(text_with_new_words, cut_all=False) # 精确模式分词 print("分词结果:", "/ ".join(seg_list_custom)) # 打印分词结果 # 演示动态添加词语 jieba.add_word("情绪分析器") # 动态添加一个词语 jieba.add_word("对话管理") # 动态添加一个词语 print("\n--- 动态添加词语后的分词 ---") # 打印提示 text_dynamic_add = "情绪分析器是对话管理的重要组成部分。" # 包含动态添加词语的文本 seg_list_dynamic = jieba.cut(text_dynamic_add, cut_all=False) # 精确模式分词 print("分词结果:", "/ ".join(seg_list_dynamic)) # 打印分词结果 # 清理临时文件 os.remove(user_dict_path) # 删除用户词典文件 print(f"删除了用户词典文件: { user_dict_path}") # 打印删除提示
-
-
关键词提取 (TF-IDF 和 TextRank)
jieba
除了分词,还提供了基于TF-IDF和TextRank算法的关键词提取功能。-
TF-IDF 关键词提取:
jieba.analyse.extract_tags(sentence, topK=20, withWeight=False, allowPOS=())
topK
: 返回关键词的数量。withWeight
: 是否返回关键词的权重。allowPOS
: 允许的词性(如只提取名词)。
-
TextRank 关键词提取:
TextRank是一种基于图的排序算法,类似于PageRank,通过词语之间的共现关系构建图,然后计算每个词语的重要性。
jieba.analyse.textrank(sentence, topK=20, withWeight=False, allowPOS=('ns', 'n', 'vn', 'v'))
allowPOS
: TextRank通常允许更多的词性来构建图。
import jieba.analyse # 导入jieba.analyse模块,用于关键词提取 text_for_keywords = "AI机器人可以分析用户的情绪状态,并根据健康数据提供个性化的健康推荐。它通过自然语言处理技术理解用户输入,并结合深度学习模型进行情绪洞察和健康智荐。" # 用于关键词提取的文本 print("\n--- TF-IDF 关键词提取 ---") # 打印提示 # 提取top 5关键词,并显示权重 keywords_tfidf = jieba.analyse.extract_tags(text_for_keywords, topK=5, withWeight=True) # 使用TF-IDF提取前5个关键词,并返回权重 for word, weight in keywords_tfidf: # 遍历关键词和权重 print(f"'{ word}': { weight:.4f}") # 打印关键词和权重 print("\n--- TextRank 关键词提取 ---") # 打印提示 # 提取top 5关键词,并显示权重 keywords_textrank = jieba.analyse.textrank(text_for_keywords, topK=5, withWeight=True) # 使用TextRank提取前5个关键词,并返回权重 for word, weight in keywords_textrank: # 遍历关键词和权重 print(f"'{ word}': { weight:.4f}") # 打印关键词和权重
关键词提取在AI机器人中非常有用,例如:
- 内容摘要: 快速理解用户消息的核心内容。
- 信息检索: 根据用户查询提取关键词,用于从知识库中查找相关信息。
- 意图识别辅助: 某些关键词直接指示了用户意图。
-
-
-
分词在AI机器人中的应用
在我们的AI机器人中,分词是所有文本处理模块的第一步:- 情绪分析: 用户输入的句子首先被分词,然后分析每个词或词组的情绪极性。
- 意图识别: 分词后的词语可以作为特征,输入到分类模型中识别用户意图。
- 命名实体识别: 识别分词结果中的人名、地名、组织名等特定实体。
- 健康数据提取: 从用户描述中提取具体的健康指标数值和名称(例如,“我睡了八小时”需要识别“睡”和“八小时”)。
2.1.2 文本正则化与清洗 (Text Normalization & Cleaning):去噪与标准化
文本清洗是为了去除文本中的噪音和不一致性,使其标准化,从而提高后续NLP任务的准确性和效率。这包括去除不必要的字符、统一文本格式等。
-
去除标点符号
标点符号在某些NLP任务中可能有用(如情绪分析中的感叹号表示强烈情感),但在其他任务中(如词袋模型构建)可能被视为噪音。通常,我们会根据具体任务需求来决定是保留还是去除。import re # 导入re模块 def remove_punctuation(text: str) -> str: # 定义去除标点符号的函数 """ 使用正则表达式去除文本中的所有标点符号。 :param text: 输入文本。 # 参数text的中文解释 :return: 去除标点后的文本。 # 返回值的中文解释 """ # re.sub(pattern, repl, string) 替换匹配的模式 # r'[^\w\s]' 匹配任何不是单词字符(字母、数字、下划线)和空白字符的字符 # 它会移除所有标点符号和特殊符号,只保留字母、数字、下划线和空格 cleaned_text = re.sub(r'[^\w\s]', '', text) # 使用正则表达式替换掉非字母、数字、下划线的字符为空字符串 return cleaned_text # 返回清洗后的文本 punctuated_text = "Hello, world! How are you doing? I'm fine. 这是一个测试,带有各种标点符号!@#¥%……&*()。" # 带有标点符号的文本 cleaned_no_punct = remove_punctuation(punctuated_text) # 调用去除标点符号函数 print(f"去除标点符号后: { cleaned_no_punct}") # 打印去除标点后的文本
对于中文,除了常见标点,还要考虑全角和半角标点。上述
[^\w\s]
也能处理大部分,但更精确的中文字符处理可能需要针对Unicode范围。 -
大小写转换 (针对英文)
在英文NLP中,将所有文本转换为小写(或大写)是常见的标准化步骤,以避免将Apple
和apple
视为两个不同的词。def to_lowercase(text: str) -> str: # 定义转换为小写的函数 """ 将输入文本转换为小写。 :param text: 输入文本。 # 参数text的中文解释 :return: 小写化的文本。 # 返回值的中文解释 """ return text.lower() # 返回文本的小写形式 mixed_case_text = "This Is A Mixed Case Text for NLP. Python." # 混合大小写文本 lowercase_text = to_lowercase(mixed_case_text) # 调用转换为小写函数 print(f"转换为小写后: { lowercase_text}") # 打印转换为小写后的文本
对于中文,由于没有大小写概念,此步骤通常不适用。但如果文本中夹杂英文,仍然有必要。
-
去除数字 (可选)
在某些任务中,数字可能不携带语义信息,可以去除。但在健康推荐等场景,数字(如睡眠时间、步数、体重)是关键信息,不能去除。def remove_numbers(text: str) -> str: # 定义去除数字的函数 """ 使用正则表达式去除文本中的所有数字。 :param text: 输入文本。 # 参数text的中文解释 :return: 去除数字后的文本。 # 返回值的中文解释 """ # r'\d+' 匹配一个或多个数字 cleaned_text = re.sub(r'\d+', '', text) # 使用正则表达式替换所有数字为空字符串 return cleaned_text # 返回清洗后的文本 text_with_numbers = "我的电话是13812345678,今年25岁,体重65.5公斤。" # 包含数字的文本 cleaned_no_numbers = remove_numbers(text_with_numbers) # 调用去除数字函数 print(f"去除数字后: { cleaned_no_numbers}") # 打印去除数字后的文本
-
去除特殊字符、HTML标签
用户输入可能包含表情符号、乱码字符、或者从网页复制粘贴带来的HTML标签。这些都需要清理。def remove_special_chars_and_html(text: str) -> str: # 定义去除特殊字符和HTML标签的函数 """ 去除文本中的特殊字符和HTML标签。 :param text: 输入文本。 # 参数text的中文解释 :return: 清理后的文本。 # 返回值的中文解释 """ # 1. 移除HTML标签 clean_html = re.sub(r'<.*?>', '', text) # 使用正则表达式移除所有HTML标签 # 2. 移除常见的非字母数字中文字符 (这里稍微放宽,保留一些常见的如中文顿号) # 实际可能需要更复杂的规则或白名单 # ^[\u4e00-\u9fa5a-zA-Z0-9\s,.,。?!!?]+$ 匹配中文、英文、数字、空格和一些常用标点 # [^\u4e00-\u9fa5a-zA-Z0-9\s,.,。?!!?] 匹配除了这些字符以外的所有字符 clean_special_chars = re.sub(r'[^\u4e00-\u9fa5a-zA-Z0-9\s,.,。?!!?]', '', clean_html) # 移除其他非指定的中文字符、英文、数字、空格和常用标点 # 移除多个连续空格,替换为单个空格 clean_spaces = re.sub(r'\s+', ' ', clean_special_chars).strip() # 替换多个连续空格为一个,并去除首尾空白 return clean_spaces # 返回清理后的文本 dirty_text = "<html><body>我今天心情<p style='color:red'>很棒</p>!😊🎉 电话:123-4567。</body></html>" # 包含HTML标签和特殊字符的文本 cleaned_text = remove_special_chars_and_html(dirty_text) # 调用清洗函数 print(f"去除特殊字符和HTML后: { cleaned_text}") # 打印清理后的文本
-
去除停用词 (Stop Words)
停用词是语言中频繁出现但通常不携带太多语义信息或情感极性的词语(如“的”、“是”、“了”、“and”、“the”)。去除停用词可以减少特征维度,提高处理效率,并有时能提升模型性能。def remove_stopwords(tokens: list[str], stopwords: set) -> list[str]: # 定义去除停用词的函数 """ 从分词后的词语列表中去除停用词。 :param tokens: 分词后的词语列表。 # 参数tokens的中文解释 :param stopwords: 停用词集合。 # 参数stopwords的中文解释 :return: 去除停用词后的词语列表。 # 返回值的中文解释 """ filtered_tokens = [word for word in tokens if word not in stopwords] # 遍历词语列表,如果词语不在停用词集合中,则保留 return filtered_tokens # 返回过滤后的词语列表 # 简化的中文停用词列表 (实际会更长) chinese_stopwords = { "的", "是", "了", "和", "在", "我", "你", "他", "她", "它", "们", "啊", "呀", "哦", "嗯", "吧", "呢", "吗", "怎么样", "什么"} # 中文停用词集合 raw_tokens_cn = ["我", "今天", "的", "心情", "是", "非常", "好", "的", "啊"] # 原始中文分词结果 filtered_tokens_cn = remove_stopwords(raw_tokens_cn, chinese_stopwords) # 调用去除停用词函数 print(f"去除中文停用词后: { filtered_tokens_cn}") # 打印过滤后的中文分词结果 # 简化的英文停用词列表 english_stopwords = { "a", "an", "the", "is", "are", "was", "were", "and", "or", "but", "i", "you", "he", "she", "it", "they", "me", "him", "her", "us", "them"} # 英文停用词集合 raw_tokens_en = ["i", "am", "a", "good", "student", "and", "i", "like", "it"] # 原始英文分词结果 filtered_tokens_en = remove_stopwords(raw_tokens_en, english_stopwords) # 调用去除停用词函数 print(f"去除英文停用词后: { filtered_tokens_en}") # 打印过滤后的英文分词结果
停用词列表通常来自公开数据集,也可以根据具体领域和任务进行定制。例如,在情感分析中,“不”字通常是重要的否定词,不应被移除。
-
拼写纠正 (Spell Correction) (简述)
用户输入中可能包含拼写错误或打字错误。拼写纠正旨在将这些错误词语纠正为正确的形式。- 原理: 通常基于编辑距离(如Levenshtein距离)、N-gram语言模型、或基于深度学习的序列到序列模型。
- 在AI机器人中的应用: 提高用户输入的理解准确性,改善用户体验。
- 复杂性: 中文拼写纠正涉及到同音字、形近字、拼音输入错误等,比英文复杂。
由于拼写纠正本身是一个复杂的NLP子任务,这里仅做概念性介绍,不提供具体的从零实现代码,实际会使用第三方库。
2.1.3 词形还原与词干提取 (Lemmatization & Stemming):词语形态的统一
在英文等屈折语中,同一个词可能因为时态、单复数、词性等变化而呈现出不同的形态(如 run
, running
, ran
;cat
, cats
)。词形还原和词干提取旨在将这些不同形态的词语还原到它们的“基本形式”,从而减少特征空间,提高模型泛化能力。
-
概念与区别
- 词干提取 (Stemming):
- 概念: 简单粗暴地切除词语的后缀(或前缀),得到词的“词干”。词干不一定是有效的词语,仅仅是一个前缀。
- 特点: 速度快,但精确度较低,可能产生非词典词(Porter Stemmer)。
- 例如:
running
->runn
,cats
->cat
。
- 词形还原 (Lemmatization):
- 概念: 将词语还原为其“词元”(Lemma),即词的规范化形式。词形还原通常需要结合词典和词性信息,结果保证是词典中的有效词。
- 特点: 速度慢(需要查找词典和考虑词性),但精确度高,结果是有意义的词语。
- 例如:
running
->run
,ran
->run
,better
->good
。
- 词干提取 (Stemming):
-
词干提取算法 (Python
NLTK
库)
NLTK
(Natural Language Toolkit) 是Python中最著名的NLP库之一,提供了丰富的文本处理工具,包括词干提取器。-
安装 NLTK:
pip install nltk # 安装NLTK库
-
下载 NLTK 数据:
在使用NLTK的一些功能前,可能需要下载对应的数据集,例如停用词、词形还原词典等。import nltk # 导入nltk库 # nltk.download('punkt') # 分词器模型 # nltk.download('stopwords') # 停用词列表 # nltk.download('wordnet') # WordNet词典,用于词形还原 # nltk.download('averaged_perceptron_tagger') # 词性标注器
(请在Python环境中运行
nltk.download()
命令来下载所需数据,这里不直接提供下载代码以避免潜在的网络问题。) -
Porter Stemmer: 最早和最常见的英文词干提取算法之一。
from nltk.stem import PorterStemmer # 从nltk.stem导入PorterStemmer porter = PorterStemmer() # 创建PorterStemmer实例 words_to_stem = ["running", "runs", "ran", "universal", "universally", "caring", "cars"] # 待词干提取的词语列表 stemmed_words = [porter.stem(word) for word in words_to_stem] # 对每个词语进行词干提取 print(f"Porter词干提取结果: { stemmed_words}") # 打印词干提取结果
-
Snowball Stemmer: 改进型的Porter Stemmer,支持多种语言。
from nltk.stem import SnowballStemmer # 从nltk.stem导入SnowballStemmer # 英文Snowball Stemmer snowball_en = SnowballStemmer("english") # 创建英文Snowball Stemmer实例 stemmed_words_snowball_en = [snowball_en.stem(word) for word in words_to_stem] # 对每个词语进行词干提取 print(f"Snowball (英文) 词干提取结果: { stemmed_words_snowball_en}") # 打印词干提取结果 # 考虑多语言支持 (尽管AI机器人是中文的,了解其多语言特性很重要) # snowball_es = SnowballStemmer("spanish") # 创建西班牙文Snowball Stemmer实例 # stemmed_words_snowball_es = [snowball_es.stem("corriendo"), snowball_es.stem("corrió")] # 对西班牙文词语进行词干提取 # print(f"Snowball (西班牙文) 词干提取结果: {stemmed_words_snowball_es}") # 打印结果
-
-
词形还原 (Python
NLTK
库)
词形还原通常使用WordNet词典。from nltk.stem import WordNetLemmatizer # 从nltk.stem导入WordNetLemmatizer from nltk.corpus import wordnet # 从nltk.corpus导入wordnet (需要下载WordNet数据) # 确保已下载 'wordnet' 和 'averaged_perceptron_tagger' # nltk.download('wordnet') # nltk.download('averaged_perceptron_tagger') lemmatizer = WordNetLemmatizer() # 创建WordNetLemmatizer实例 words_to_lemmatize = ["running", "runs", "ran", "better", "good", "cats", "geese", "mice"] # 待词形还原的词语列表 # 词形还原通常需要词性 (POS) 作为参数以获得更准确的结果 # 如果不指定词性,默认是名词 (n) print("--- 词形还原 (不指定词性) ---") # 打印提示 lemmatized_words_default = [lemmatizer.lemmatize(word) for word in words_to_lemmatize] # 不指定词性进行词形还原 print(f"结果: { lemmatized_words_default}") # 打印结果 # 词性标注辅助函数 (需要下载 'averaged_perceptron_tagger') def get_wordnet_pos(tag): # 定义获取WordNet词性的辅助函数 """将NLTK的POS标签转换为WordNet的POS标签。""" if tag.startswith('J'): # 如果标签以J开头(形容词) return wordnet.ADJ # 返回WordNet形容词 elif tag.startswith('V'): # 如果标签以V开头(动词) return wordnet.VERB # 返回WordNet动词 elif tag.startswith('N'): # 如果标签以N开头(名词) return wordnet.NOUN # 返回WordNet名词 elif tag.startswith('R'): # 如果标签以R开头(副词) return wordnet.ADV # 返回WordNet副词 else: # 其他情况 return wordnet.NOUN # 默认返回名词 (通常为None或'n') # 演示带词性的词形还原 (需要先对文本进行词性标注) from nltk import word_tokenize, pos_tag # 从nltk导入word_tokenize和pos_tag text_pos_lemma = "The cats were running quickly, and they felt much better." # 带有不同词性的文本 tokens_pos = word_tokenize(text_pos_lemma) # 英文分词 tagged_tokens = pos_tag(tokens_pos) # 对分词结果进行词性标注 print("\n--- 词形还原 (带词性) ---") # 打印提示 lemmatized_words_with_pos = [] # 存储带词性词形还原结果的列表 for word, tag in tagged_tokens: # 遍历词语和词性标签 pos = get_wordnet_pos(tag) # 获取WordNet词性 if pos: # 如果获取到词性 lemmatized_words_with_pos.append(lemmatizer.lemmatize(word, pos=pos)) # 带词性进行词形还原 else: # 如果没有获取到词性 lemmatized_words_with_pos.append(lemmatizer.lemmatize(word)) # 不带词性进行词形还原 (默认名词) print(f"原始带词性: { tagged_tokens}") # 打印原始带词性标注的词语 print(f"还原结果: { lemmatized_words_with_pos}") # 打印词形还原结果
在AI机器人中,词形还原(而非词干提取)通常是更优选择,因为它保留了词的语义完整性,对于后续的语义理解、意图识别和情绪分析更有帮助。
-
中文文本的特殊性
中文是典型的分析语(isolating language),词语通常没有词形变化。因此,中文NLP中通常不需要进行词干提取或词形还原。- 然而,中文文本处理仍然可能需要进行繁简转换(例如,将“繁体字”转换为“简体字”),以及一些异体字或同义词的规范化。
- 一些中文分词库(如
jieba
)会自带一些词语的规范化处理,但更深入的同义词/近义词归一化则属于更高级的语义理解范畴。
在AI机器人中,文本预处理是一个串联的流水线:
用户输入文本 -> (去除HTML/特殊字符) -> (大小写转换) -> 分词 -> (去除停用词) -> (词形还原/词干提取) -> 清洁、标准化词元列表
这个最终的词元列表将作为后续特征提取和模型输入的干净数据。
2.1.4 文本表示:将文本转化为数值向量
计算机无法直接处理文本,它们只能理解数字。因此,我们需要将清洗和分词后的文本转化为数值表示形式,这些数值向量能够捕捉文本的特征和语义信息。这一过程称为文本向量化或特征工程。
-
独热编码 (One-Hot Encoding)
- 概念: 独热编码是一种简单的文本表示方法。它为词汇表中的每个唯一词语创建一个维度。对于一个词语,它的向量表示在这个词语对应的维度上为1,其他所有维度为0。
- 优点: 简单易懂,实现方便。
- 缺点:
- 维度灾难: 词汇表越大,向量维度越高,导致特征空间巨大且稀疏。
- 语义信息缺失: 独热编码无法捕捉词语之间的语义关系(例如,“国王”和“女王”的向量距离与“国王”和“香蕉”的向量距离相同)。
- 无法处理未登录词: 对于词汇表中不存在的词语,无法表示。
def one_hot_encode_word(word: str, vocab: list[str]) -> list[int]: # 定义独热编码单词的函数 """ 对单个词语进行独热编码。 :param word: 待编码的词语。 # 参数word的中文解释 :param vocab: 词汇表(所有唯一词语的列表)。 # 参数vocab的中文解释 :return: 独热编码向量。 # 返回值的中文解释 """ word_vector = [0] * len(vocab) # 初始化一个全零向量,长度为词汇表大小 try: # 尝试找到词语在词汇表中的索引 idx = vocab.index(word) # 获取词语在词汇表中的索引 word_vector[idx] = 1 # 将对应索引位置的值设为1 except ValueError: # 如果词语不在词汇表中 print(f"警告: 词语 '{ word}' 不在词汇表中,无法进行独热编码。") # 打印警告 return word_vector # 返回独热编码向量 def one_hot_encode_document(tokens: list[str], vocab: list[str]) -> list[int]: # 定义独热编码文档的函数 """ 对一个文档(词语列表)进行独热编码,表示文档中是否存在某个词。 :param tokens: 文档的词语列表。 # 参数tokens的中文解释 :param vocab: 词汇表。 # 参数vocab的中文解释 :return: 文档的独热编码向量。 # 返回值的中文解释 """ doc_vector = [0] * len(vocab) # 初始化一个全零向量 for word in tokens: # 遍历文档中的每个词语 try: # 尝试找到词语在词汇表中的索引 idx = vocab.index(word) # 获取词语在词汇表中的索引 doc_vector[idx] = 1 # 如果词语出现,将对应索引位置的值设为1 except ValueError: # 如果词语不在词汇表中 pass # 忽略不在词汇表中的词语 return doc_vector # 返回文档的独热编码向量 # 示例 AI 机器人中的应用 corpus = [ # 模拟语料库 ["我", "今天", "很", "开心"], ["我", "感觉", "有点", "难过"], ["健康", "推荐", "对", "我", "很重要"], ["情绪", "分析", "帮助", "我"] ] # 构建词汇表 vocab_set = set() # 创建一个空集合,用于存储所有不重复的词语 for doc_tokens in corpus: # 遍历语料库中的每个文档(词语列表) vocab_set.update(doc_tokens) # 将文档中的所有词语添加到集合中(自动去重) vocab_list = sorted(list(vocab_set)) # 将集合转换为列表并排序,作为最终的词汇表 print(f"词汇表: { vocab_list}") # 打印词汇表 # 对单个词语进行独热编码 word_to_encode = "开心" # 待编码的词语 one_hot_vector_word = one_hot_encode_word(word_to_encode, vocab_list) # 调用独热编码函数 print(f"词语 '{ word_to_encode}' 的独热编码: { one_hot_vector_word}") # 打印独热编码向量 # 对文档进行独热编码 doc_tokens_example = ["我", "今天", "很", "开心"] # 示例文档 one_hot_vector_doc = one_hot_encode_document(doc_tokens_example, vocab_list) # 调用独热编码文档函数 print(f"文档 '{ doc_tokens_example}' 的独热编码: { one_hot_vector_doc}") # 打印独热编码向量
独热编码虽然简单,但其缺点限制了它在复杂AI任务中的应用,尤其是在需要捕捉语义关系和处理大规模词汇表时。
-
词袋模型 (Bag of Words, BoW)
- 概念: 词袋模型将文档表示为一个固定长度的向量,向量的每个维度对应词汇表中的一个词语。向量的值通常是该词语在文档中出现的次数(词频),或者仅仅是二值(词语是否存在)。词袋模型忽略了词语的顺序和语法结构,只关注词语的出现情况,就像把所有词语都扔进一个袋子里,不考虑它们的排列。
- 优点: 简单,易于实现,计算效率高。在许多文本分类任务中表现良好。
- 缺点:
- 语义信息缺失: 忽略了词序和上下文,无法捕捉词语之间的深层语义关系。
- 维度灾难: 词汇表庞大时,向量维度非常高且稀疏。
- 信息损失: 丢失了文本的结构信息。
from collections import Counter # 导入Counter类,用于方便地计数词频 def bag_of_words(tokens: list[str], vocab: list[str]) -> list[int]: # 定义词袋模型函数 """ 将文档(词语列表)转换为词袋模型向量。 向量的值是词语在文档中出现的频率。 :param tokens: 文档的词语列表。 # 参数tokens的中文解释 :param vocab: 词汇表。 # 参数vocab的中文解释 :return: 词袋模型向量。 # 返回值的中文解释 """ word_counts = Counter(tokens) # 使用Counter统计文档中每个词语的出现次数 bow_vector = [word_counts.get(word, 0) for word in vocab] # 遍历词汇表,获取每个词语在文档中的计数,如果不存在则为0 return bow_vector # 返回词袋模型向量 # 沿用之前的语料库和词汇表 # corpus, vocab_list print("\n--- 词袋模型示例 ---") # 打印提示 doc_tokens_bow_1 = ["我", "今天", "很", "开心", "我", "非常", "开心"] # 示例文档1,包含重复词语 bow_vector_1 = bag_of_words(doc_tokens_bow_1, vocab_list) # 调用词袋模型函数 print(f"文档1: '{ doc_tokens_bow_1}'") # 打印文档1 print(f"词袋向量: { bow_vector_1}") # 打印词袋向量 doc_tokens_bow_2 = ["健康", "生活", "很重要"] # 示例文档2 (假设"生活"不在当前词汇表) bow_vector_2 = bag_of_words(doc_tokens_bow_2, vocab_list) # 调用词袋模型函数 print(f"文档2: '{ doc_tokens_bow_2}'") # 打印文档2 print(f"词袋向量: { bow_vector_2}") # 打印词袋向量 # 可以看出,"生活"因为不在词汇表中,其对应的维度为0,这体现了词袋模型的词汇表依赖性。
词袋模型是许多传统文本分类算法(如朴素贝叶斯、SVM)的经典特征表示。
-
TF-IDF (Term Frequency-Inverse Document Frequency)
- 概念: TF-IDF是一种统计方法,用于评估一个词语在一个文档集(语料库)中的重要性。它结合了两个因子:
- 词频 (Term Frequency, TF): 衡量一个词语在当前文档中出现的频率。TF值越高,表明该词语对当前文档越重要。
[ TF(t,d) = \frac{\text{单词t在文档d中出现的次数}}{\text{文档d中单词总数}} ] - 逆文档频率 (Inverse Document Frequency, IDF): 衡量一个词语在整个语料库中的稀有程度。IDF值越高,表明该词语在语料库中越罕见,因此它对区分文档的重要性越高。常见词(如停用词)的IDF值会很低,因为它在几乎所有文档中都出现。
[ IDF(t,D) = \log\left(\frac{\text{语料库D中的文档总数}}{\text{包含单词t的文档数量} + 1}\right) ]
其中,(D) 是语料库,(|D|) 是语料库中文档总数,分母加1是为了避免除数为零。 - TF-IDF: 将TF和IDF相乘,得到最终的TF-IDF值。
[ TFIDF(t,d,D) = TF(t,d) \times IDF(t,D) ]
- 词频 (Term Frequency, TF): 衡量一个词语在当前文档中出现的频率。TF值越高,表明该词语对当前文档越重要。
- 作用与优势:
- 突出重要词: 能够有效过滤掉常见但无意义的词(如停用词),同时突出那些在文档中频繁出现但在整个语料库中相对罕见的词,这些词往往更能代表文档的主题。
- 降维 (间接): 通过选择TF-IDF值高的词作为特征,可以有效地进行特征选择。
- 广泛应用: 在信息检索、文本分类、文本摘要等领域有广泛应用。
import math # 导入math模块 class TFIDFVectorizer: # 定义TF-IDF向量化器类 def __init__(self): # 构造方法 self.vocabulary = [] # 词汇表列表 self.idf = { } # 存储每个词的IDF值 self.doc_count = 0 # 文档总数 def fit(self, corpus: list[list[str]]): # 训练方法,用于构建词汇表和计算IDF """ 根据语料库构建词汇表并计算每个词的IDF值。 :param corpus: 语料库,由多个文档的词语列表组成。 # 参数corpus的中文解释 """ doc_freq = { } # 存储每个词在多少个文档中出现过的计数 all_words = set() # 存储所有唯一词语的集合 self.doc_count = len(corpus) # 获取语料库中的文档总数 for doc_tokens in corpus: # 遍历语料库中的每个文档 all_words.update(doc_tokens) # 将文档中的词语添加到所有词语集合 # 统计词语在文档中出现的次数(只算一次,即只关心是否出现过) for word in set(doc_tokens): # 遍历文档中不重复的词语 doc_freq[word] = doc_freq.get(word, 0) + 1 # 增加该词语的文档频率计数 self.vocabulary = sorted(list(all_words)) # 构建词汇表,并排序 # 计算IDF for word in self.vocabulary: # 遍历词汇表中的每个词 # IDF公式:log(文档总数 / (包含该词的文档数 + 1)) self.idf[word] = math.log(self.doc_count / (doc_freq.get(word, 0) + 1)) # 计算并存储IDF值 print("TF-IDF Vectorizer 训练完成。") # 打印训练完成提示 def transform(self, documents: list[list[str]]) -> list[list[float]]: # 转换方法,将文档转换为TF-IDF向量 """ 将文档(词语列表)转换为TF-IDF向量。 :param documents: 待转换的文档列表。 # 参数documents的中文解释 :return: 文档的TF-IDF向量列表。 # 返回值的中文解释 """ tfidf_vectors = [] # 存储TF-IDF向量的列表 for doc_tokens in documents: # 遍历每个文档 tf = Counter(doc_tokens) # 计算当前文档中每个词的词频 doc_len = len(doc_tokens) # 获取当前文档的总词数 # 计算TF-IDF向量 tfidf_vector = [] # 当前文档的TF-IDF向量 for word in self.vocabulary: # 遍历词汇表中的每个词 if word in tf and doc_len > 0: # 如果词语在当前文档中出现且文档不为空 tf_val = tf[word] / doc_len # 计算TF值 idf_val = self.idf.get(word, 0) # 获取IDF值,如果词语不在词汇表中则为0 tfidf_vector.append(tf_val * idf_val) # 计算TF-IDF值并添加到向量 else: # 如果词语不在当前文档中或文档为空 tfidf_vector.append(0.0) # 添加0.0 tfidf_vectors.append(tfidf_vector) # 将当前文档的TF-IDF向量添加到列表 return tfidf_vectors # 返回所有文档的TF-IDF向量列表 # 沿用之前的语料库 (每个文档是已分词的列表) corpus_tfidf = [ # 模拟语料库 ["我", "今天", "很", "开心"], ["我", "感觉", "有点", "难过"], ["健康", "推荐", "对", "我", "很重要"], ["情绪", "分析", "帮助", "我"], ["我", "很", "开心", "和", "健康"], # 新增一个文档,看词语权重变化 ["AI", "机器人", "情绪", "健康"] # 新增一个文档,包含新词 ] tfidf_vectorizer = TFIDFVectorizer() # 创建TF-IDF向量化器实例 tfidf_vectorizer.fit(corpus_tfidf) # 训练TF-IDF向量化器 print(f"\n词汇表 (TF-IDF): { tfidf_vectorizer.vocabulary}") # 打印词汇表 print(f"IDF 值 (部分): { dict(list(tfidf_vectorizer.idf.items())[:5])}...") # 打印部分IDF值 # 转换文档 documents_to_transform = [ # 待转换的文档列表 ["我", "今天", "很", "开心"], ["AI", "机器人", "理解", "健康"], # 包含词汇表中新词的文档 ["我", "很", "开心"] # 短文档 ] tfidf_vectors = tfidf_vectorizer.transform(documents_to_transform) # 转换文档为TF-IDF向量 print("\n--- TF-IDF 向量示例 ---") # 打印提示 for i, vec in enumerate(tfidf_vectors): # 遍历每个向量 print(f"文档 { i+1} 向量 (前5维): { vec[:5]}...") # 打印前5维向量 # 实际的向量维度会非常高,通常会使用稀疏矩阵存储
在AI机器人中,TF-IDF常用于:
- 意图识别: 将用户查询转换为TF-IDF向量,作为分类模型的输入。
- 关键词提取: TF-IDF值高的词通常是文档的关键词。
- 文档相似度计算: 比较两个文档的TF-IDF向量的相似度(如余弦相似度),用于推荐相似的健康文章或相关对话。
- 概念: TF-IDF是一种统计方法,用于评估一个词语在一个文档集(语料库)中的重要性。它结合了两个因子:
-
N-gram (N-gram Model)
- 概念: N-gram是指文本中连续的N个词语或字符序列。
- Unigram (1-gram): 单个词语。
- Bigram (2-gram): 连续的两个词语。
- Trigram (3-gram): 连续的三个词语。
- 作用:
- 捕捉局部词序信息: 词袋模型忽略词序,而N-gram可以在一定程度上捕捉词语的局部顺序信息。例如,“非常高兴”和“不高兴”在词袋模型中可能相似,但在Bigram中是完全不同的。
- 平滑稀疏性: 在语言模型中,N-gram用于估计词语序列的概率,解决数据稀疏性问题。
- 特征增强: 将N-gram作为额外的特征添加到文本向量中,可以提高文本分类、情感分析等任务的性能。
def generate_ngrams(tokens: list[str], n: int) -> list[str]: # 定义生成N-gram的函数 """ 从词语列表中生成N-gram序列。 :param tokens: 词语列表。 # 参数tokens的中文解释 :param n: N-gram的长度。 # 参数n的中文解释 :return: N-gram列表。 # 返回值的中文解释 """ ngrams = [] # 存储N-gram的列表 for i in range(len(tokens) - n + 1): # 遍历词语列表,直到无法生成完整N-gram ngrams.append(" ".join(tokens[i:i+n])) # 将连续的N个词语用空格连接,并添加到列表 return ngrams # 返回N-gram列表 tokens_for_ngram = ["我", "今天", "感觉", "非常", "好", "身体", "健康"] # 待生成N-gram的词语列表 print("\n--- N-gram 示例 ---") # 打印提示 unigrams = generate_ngrams(tokens_for_ngram, 1) # 生成Unigram (1-gram) print(f"Unigrams: { unigrams}") # 打印Unigram bigrams = generate_ngrams(tokens_for_ngram, 2) # 生成Bigram (2-gram) print(f"Bigrams: { bigrams}") # 打印Bigram trigrams = generate_ngrams(tokens_for_ngram, 3) # 生成Trigram (3-gram) print(f"Trigrams: { trigrams}") # 打印Trigram
N-gram作为特征时,通常与词袋模型或TF-IDF结合使用,构成更丰富的特征向量。例如,可以构建一个词-字符N-gram组合的向量。
- 概念: N-gram是指文本中连续的N个词语或字符序列。
-
词嵌入 (Word Embeddings) (简要介绍)
- 概念: 词嵌入是一种将词语映射到连续低维向量空间的技术。在这个空间中,语义相似的词语(如“国王”和“女王”)的向量距离会更近,而语义不相关的词语(如“国王”和“香蕉”)的向量距离会更远。词向量的每个维度没有明确的物理意义,但整个向量共同编码了词语的语义信息。
- 优势:
- 捕捉语义关系: 能够捕捉词语之间的相似性、类比关系等深层语义信息。
- 降低维度: 与独热编码相比,词嵌入的维度大大降低(通常在50-300维),避免了维度灾难。
- 解决稀疏性问题: 可以处理未见过的新词(通过预训练模型),对罕见词也能有更好的表示。
- 提高模型性能: 作为神经网络模型的输入特征,可以显著提高各种NLP任务的性能。
- 常见模型:
- Word2Vec: 最早和最著名的词嵌入模型,通过预测上下文中的词来学习词向量。
- GloVe: 基于全局词语共现统计信息构建的词嵌入模型。
- FastText: 在Word2Vec基础上,考虑了字符N-gram,能够更好地处理罕见词和未登录词。
- ELMo, BERT, GPT (Transformer-based models): 现代最先进的词嵌入模型,它们学习的是上下文相关的词向量,同一个词在不同语境下会有不同的向量表示,极大地提升了NLP任务的性能。这些模型通常是预训练好的,可以作为特征提取器或进行微调。
# 词嵌入的概念性示例 (不涉及模型训练,仅展示结果特性) # 假设我们有一个预训练的词嵌入模型,可以查询词向量 # 实际中会加载一个大的词向量文件,如: # from gensim.models import KeyedVectors # word2vec_model = KeyedVectors.load_word2vec_format('path/to/GoogleNews-vectors-negative300.bin', binary=True) def get_mock_word_embedding(word: str) -> list[float]: # 定义获取模拟词嵌入的函数 """ 模拟获取词语的低维向量表示。 真实情况下,这将从预训练的词嵌入模型中查询。 """ # 简单模拟几个词的向量,使其具有某种“语义”关系 if word == "国王": # 如果词语是“国王” return [0.1, 0.2, 0.3, 0.4] # 返回模拟向量 elif word == "女王": # 如果词语是“女王” return [0.1, 0.2, 0.3, 0.3] # 返回模拟向量(与国王相似) elif word == "男人": # 如果词语是“男人” return [0.1, 0.1, 0.0, 0.0] # 返回模拟向量 elif word == "女人": # 如果词语是“女人” return [0.1, 0.0, 0.0, 0.0] # 返回模拟向量 elif word == "香蕉": # 如果词语是“香蕉” return [0.9, 0.8, 0.7, 0.6] # 返回模拟向量(与前面词不相似) else: # 其他词语 return [0.0, 0.0, 0.0, 0.0] # 返回全零向量 print("\n--- 词嵌入概念示例 ---") # 打印提示 vec_king = get_mock_word_embedding("国王") # 获取国王的词嵌入 vec_queen = get_mock_word_embedding("女王") # 获取女王的词嵌入 vec_man = get_mock_word_embedding("男人") # 获取男人的词嵌入 vec_woman = get_mock_word_embedding("女人") # 获取女人的词嵌入 vec_banana = get_mock_word_embedding("香蕉") # 获取香蕉的词嵌入 print(f"国王的向量: { vec_king}") # 打印国王的向量 print(f"女王的向量: { vec_queen}") # 打印女王的向量 print(f"男人的向量: { vec_man}") # 打印男人的向量 print(f"女人的向量: { vec_woman}") # 打印女人的向量 print(f"香蕉的向量: { vec_banana}") # 打印香蕉的向量 # 简单计算向量相似度 (余弦相似度) def cosine_similarity(vec1: list[float], vec2: list[float]) -> float: # 定义余弦相似度函数 """ 计算两个向量的余弦相似度。 $$ \text{Cosine Similarity}(A, B) = \frac{A \cdot B}{\|A\| \|B\|} = \frac{\sum A_i B_i}{\sqrt{\sum A_i^2} \sqrt{\sum B_i^2}} $$ """ dot_product = sum(a * b for a, b in zip(vec1, vec2)) # 计算点积 magnitude_a = math.sqrt(sum(a * a for a in vec1)) # 计算向量A的模 magnitude_b = math.sqrt(sum(b * b for b in vec2)) # 计算向量B的模 if magnitude_a == 0 or magnitude_b == 0: # 避免除零错误 return 0.0 # 如果任一模为0,返回0 return dot_product / (magnitude_a * magnitude_b) # 返回余弦相似度 print("\n--- 向量相似度示例 ---") # 打印提示 print(f"国王 vs 女王 相似度: { cosine_similarity(vec_king, vec_queen):.4f}") # 打印国王和女王的相似度 print(f"男人 vs 女人 相似度: { cosine_similarity(vec_man, vec_woman):.4f}") # 打印男人和女人的相似度 print(f"国王 vs 香蕉 相似度: { cosine_similarity(vec_king, vec_banana):.4f}") # 打印国王和香蕉的相似度
词嵌入是现代NLP模型(特别是深度学习模型)的基石。在AI机器人中,它将用于将用户的文本输入转化为模型可理解的数值特征,从而进行更准确的情绪分析、意图识别和回复生成。
2.2 文本分类:情感分析与意图识别
文本分类是自然语言处理(NLP)领域中的一个基础且至关重要的任务,它的核心目标是将输入的文本自动归类到预定义的类别集合中的一个或多个类别中。对于AI机器人而言,文本分类扮演着“大脑”的角色,使其能够理解用户的“言外之意”——比如用户是开心还是沮丧(情感分析),以及用户想要做什么(意图识别)。
2.2.1 文本分类的基础原理与应用
文本分类的本质是一种监督学习任务,这意味着我们需要一个已经打好标签的数据集来训练模型。这个数据集包含大量的文本及其对应的类别标签。模型通过学习这些文本与标签之间的映射关系,从而能够在遇到新的、未见过的文本时,预测其所属的类别。
核心目标与目的:
文本分类旨在将非结构化的文本数据转化为结构化的、可操作的信息。其主要目的是为了自动化地理解、组织和管理海量的文本信息。在AI机器人场景中,它使得机器人能够:
- 情感分析(Sentiment Analysis):识别文本中表达的情绪倾向,如积极、消极、中性,或者更细粒度的情绪如愤怒、快乐、悲伤、惊讶等。这对于机器人调整对话风格、提供情绪支持或上报异常情绪至关重要。
- 意图识别(Intent Recognition):判断用户语句的深层目的,例如“订机票”、“查询天气”、“播放音乐”或“投诉”。这是驱动对话系统走向正确业务流程的关键第一步。
- 主题分类(Topic Categorization):将文本归类到预设的主题或领域,如新闻分类、邮件分类、客户反馈分类等。
- 垃圾邮件检测(Spam Detection):识别并过滤掉不请自来的、恶意的或低质量的文本信息。
- 语言检测(Language Identification):判断文本所属的自然语言种类。
- 内容审核(Content Moderation):自动识别并标记不适宜、有害或违规的内容。
文本分类的基本工作流程:
一个典型的文本分类系统通常遵循以下步骤:
-
数据收集与标注(Data Collection & Annotation):
- 目的:获取用于训练、验证和测试模型的文本数据,并为其打上正确的类别标签。这是整个流程的基础,数据质量直接决定了模型性能的上限。
- AI机器人实践:收集真实的用户对话日志,或者通过人工模拟对话生成,然后由人工专家对每条用户输入进行情感标签(如“积极”、“消极”、“中性”)和意图标签(如“查询天气”、“预订航班”)的标注。标注过程需要清晰的标注规范,以确保一致性。
-
文本预处理(Text Preprocessing):
- 目的:清洗原始文本,移除噪声,并将其标准化,以便后续的特征提取和模型输入。
- AI机器人实践:包括分词(将句子拆分成词语序列,对于中文尤为关键,例如使用
jieba
)、去除停用词、去除标点符号、数字、特殊字符、HTML标签、以及可能的同义词归一化等。这一步在之前的章节已详细阐述,是所有NLP任务的基石。
-
特征工程/文本表示(Feature Engineering/Text Representation):
- 目的:将清洗过的文本转化为模型能够理解的数值形式。由于机器学习模型只能处理数字,因此必须将文本的语义信息编码为向量。
- AI机器人实践:可以采用传统方法如词袋模型(Bag-of-Words, BoW)、TF-IDF,或者更先进的分布式表示方法如词嵌入(Word Embeddings,例如Word2Vec、GloVe、FastText),甚至基于Transformer的上下文嵌入(如BERT、RoBERTa)来表示文本。选择哪种表示方法取决于任务复杂度和可用计算资源。
-
模型选择与训练(Model Selection & Training):
- 目的:选择合适的机器学习或深度学习模型,并使用标注好的数据对其进行训练,使其学习到文本特征与类别标签之间的映射关系。
- AI机器人实践:
- 传统机器学习模型:如朴素贝叶斯(Naive Bayes)、支持向量机(SVM)、逻辑回归(Logistic Regression)等。这些模型在数据量不大或对模型可解释性有要求时表现良好。
- 深度学习模型:如循环神经网络(RNN,包括LSTM、GRU)、卷积神经网络(CNN)、以及基于注意力机制的Transformer模型(如BERT、GPT系列)等。这些模型能够自动学习更复杂的特征,并在大数据集上通常能取得更好的性能,尤其适合捕捉文本的深层语义和长距离依赖。
-
模型评估(Model Evaluation):
- 目的:使用独立的测试集评估模型的性能,以了解其在新数据上的泛化能力。
- AI机器人实践:常用的评估指标包括准确率(Accuracy)、精确率(Precision)、召回率(Recall)、F1-分数、混淆矩阵(Confusion Matrix)、ROC曲线和AUC值等。针对情感分析和意图识别,特别关注高精度和高召回率对于关键意图/情绪的识别能力。
-
模型部署与推理(Model Deployment & Inference):
- 目的:将训练好的模型集成到实际的应用系统中,使其能够接收新的用户输入并进行实时预测。
- AI机器人实践:将模型封装成API服务(如使用Flask、FastAPI),当用户输入文本时,调用该API进行预处理、特征提取,然后模型输出预测结果(如情感类别和意图类别),供对话管理器使用。
-
迭代与优化(Iteration & Optimization):
- 目的:模型性能并非一蹴而就,需要根据实际运行效果和用户反馈持续改进。
- AI机器人实践:监控模型在生产环境中的表现,识别误判的案例,收集新的数据,重新标注,然后重新训练模型。这形成一个闭环的优化过程。
上述流程构成了任何文本分类项目的骨架,对于AI机器人的情感分析和意图识别而言,每一步都至关重要,且需要结合中文语言的特殊性进行深入实践。
2.2.2 文本分类的特征工程与文本表示深度解析
在文本分类中,将原始文本转换为机器学习模型可理解的数值向量,这一过程被称为“特征工程”或“文本表示”。优秀的文本表示方法能够更好地捕捉文本中的语义信息,从而提升模型的分类性能。在“第一章:Python编程基础与数据抽象的深度探索”的“1.2 数据结构与算法的精进”和“1.3 函数式编程与装饰器:构建可复用的AI逻辑单元”以及“2.1 文本预处理”中,我们已经初步介绍了文本的数值化概念,这里将针对文本分类的特定需求,对其进行更深入、更细致的剖析和扩展。
2.2.2.1 传统词汇级特征:深度挖掘词语的统计学价值
虽然深度学习模型在NLP领域取得了显著进展,但传统的词汇级特征表示方法,如词袋模型(Bag-of-Words, BoW)和TF-IDF,因其简单、高效且易于理解的特点,在许多场景下仍然是强大的基线模型,并且常作为深度学习模型的补充特征。
-
词袋模型(Bag-of-Words, BoW)的再审视与优化
在2.1章节中,我们已经介绍了词袋模型的基本概念,它将文档表示为一个词汇表中词语出现的频率向量,忽略了词语的顺序和语法结构。
优点深度剖析:
- 概念直观,实现简单:易于理解和实现,计算成本相对较低。
- 特征维度可控:词汇表的大小决定了特征向量的维度。
- 适合稀疏数据:尤其适用于文本这种高维稀疏的数据。
局限性及应对策略的进一步思考:
- 丢失词序和语法信息:这是BoW最显著的缺点。例如,“我爱北京天安门”和“北京爱我天安门”在BoW模型下可能得到非常相似的表示,但它们的语义完全不同。
- 应对:引入N-gram特征(见下文)可以在一定程度上缓解这个问题,通过考虑词语的局部序列来捕获一些短语信息。
- 维度灾难(Curse of Dimensionality):当词汇表非常大时(尤其在中文场景下,词汇量远超英文),特征向量的维度会非常高,导致计算效率低下,且容易过拟合。
- 应对:
- 词汇筛选:去除低频词、停用词。
- 特征选择:使用信息增益(Information Gain)、卡方检验(Chi-square Test)等统计方法选择与分类任务最相关的词语。
- 特征降维:使用主成分分析(PCA)、奇异值分解(SVD)等技术将高维稀疏向量映射到低维稠密空间。
- 哈希技巧(Hashing Trick):将高维特征映射到固定低维的哈希桶中,减少内存消耗,但会牺牲一些可解释性。
- 应对:
- 语义鸿沟(Semantic Gap):BoW无法捕捉词语之间的语义关系(如“苹果”和“香蕉”都是水果),也无法处理同义词、反义词等,导致“一词多义”和“多词一义”问题。
- 应对:引入词嵌入(Word Embeddings)是解决此问题的根本方法,它将具有相似语义的词语映射到向量空间中相近的位置。
BoW的中文实践示例(补充
CountVectorizer
的使用):
在之前我们手动实现了BoW,但在实际应用中,通常会使用sklearn.feature_extraction.text.CountVectorizer
,它能更高效地完成分词、构建词汇表和计算词频。from sklearn.feature_extraction.text import CountVectorizer # 导入CountVectorizer用于将文本转换为词频矩阵 import jieba # 导入jieba用于中文分词 # 示例文本数据集,包含情绪相关的句子 documents = [ "我今天非常开心,阳光明媚心情好。", # 第一篇文档,表达积极情绪 "这个服务真是太糟糕了,我感到很生气。", # 第二篇文档,表达消极情绪 "他觉得有点儿无聊,不知道该做些什么。", # 第三篇文档,表达中性/消极情绪 "收到好消息,真是令人惊喜万分啊。", # 第四篇文档,表达积极情绪 "产品质量问题,令人非常失望和沮丧。" # 第五篇文档,表达消极情绪 ] # 定义一个中文分词函数,作为CountVectorizer的tokenizer def chinese_tokenizer(text): # 定义一个函数用于处理中文文本的分词 return list(jieba.cut(text)) # 使用jieba.cut进行分词,并返回一个词语列表 # 初始化CountVectorizer,并传入自定义的中文分词器 # token_pattern=r"(?u)\b\w\w+\b" 参数是针对英文的,对于中文分词器不需要,或者可以更宽松 # 这里我们只用tokenizer参数即可,或者不传入token_pattern vectorizer = CountVectorizer(tokenizer=chinese_tokenizer) # 初始化CountVectorizer,指定使用chinese_tokenizer进行分词 # 训练词汇表并转换文本为词频矩阵 X_bow = vectorizer.fit_transform(documents) # 对文档进行拟合(学习词汇表)并转换(生成词频矩阵) # 获取词汇表(特征名) feature_names = vectorizer.get_feature_names_out() # 获取词汇表中所有词语(特征名称) print("BoW 特征矩阵的形状 (文档数, 词汇数):", X_bow.shape) # 打印生成的词频矩阵的维度信息 print("部分词汇表:", feature_names[:10]) # 打印词汇表中的前10个词语 print("\n第一个文档的BoW向量 (稀疏表示):\n", X_bow[0]) # 打印第一个文档的BoW向量(稀疏格式) print("\n第一个文档的BoW向量 (密集表示):\n", X_bow[0