基于行空板与MultinomialNB的古诗词分类系统实践

基于行空板与MultinomialNB的古诗词分类系统实践 1. 项目缘起当硬件开发板遇上古典文学作为一名长期在嵌入式开发和机器学习交叉领域折腾的开发者我经常思考一个问题如何让那些功能强大的开源硬件不只是停留在控制LED灯、读取传感器数据的“玩具”阶段而是能承载一些更有文化厚度和趣味性的应用直到我手头拿到一块行空板这个想法变得具体起来。行空板这块集成了高性能处理器、丰富外设和Python环境的国产开源硬件其定位本就是为教育和创意项目而生。它让我想起了早些年用树莓派做的一些自然语言处理小实验但行空板更友好的交互界面自带屏幕和一体化的设计让我觉得它可以成为一个绝佳的“文化计算”载体。所谓文化计算就是用计算的方法去处理、分析和呈现文化内容。那么有什么比我们博大精深的古诗词更适合作为切入点的呢于是“基于行空板的MultinomialNB古诗词分类”这个项目构想诞生了。它的核心目标很简单在行空板这块小小的板子上构建一个能自动对输入的古诗词句子进行题材分类比如山水田园、边塞征战、咏史怀古、送别思乡等的微型智能系统。我选择MultinomialNB多项式朴素贝叶斯算法并非因为它是最前沿、最复杂的模型恰恰相反正是它的简单、高效和对小规模文本数据表现出的良好鲁棒性使其成为在资源有限的嵌入式设备上部署机器学习模型的理想选择。这个项目听起来像是软件算法的事但实际上它是一个典型的“软硬结合”实践。它涉及到如何在嵌入式环境行空板中准备Python数据科学栈、如何处理非结构化的中文文本、如何训练一个轻量级分类模型以及最终如何设计一个交互界面让用户能直观地使用它。整个过程就像给一块冰冷的电路板注入了一丝古典文学的“灵魂”。接下来我将详细拆解从零开始实现这个项目的每一个步骤、背后的思考以及我踩过的那些坑。2. 行空板开发环境搭建与数据准备在行空板上进行机器学习项目第一步不是写代码而是打造一个趁手的“厨房”——开发环境。行空板默认基于Linux系统并预装了Python3和基本的库但对于机器学习任务我们还需要一些专门的“厨具”。2.1 行空板基础配置与依赖安装拿到行空板连接Wi-Fi后我首先通过SSH或者直接使用其自带的WebIDEJupyter Lab进行开发。我强烈推荐使用WebIDE因为它提供了浏览器内的代码编辑、运行和文件管理功能非常方便。在行空板的终端或WebIDE的终端中我们需要安装核心的数据处理和机器学习库。由于行空板的处理器架构通常是ARM和存储空间限制直接使用pip install安装大型库如scikit-learn的完整版可能会遇到编译依赖或空间不足的问题。最稳妥的方法是安装scikit-learn的精简版以及必要的科学计算库。# 更新pip并安装必要的基础库 pip3 install --upgrade pip # 安装NumPy和SciPy这是许多机器学习库的基础 pip3 install numpy scipy # 安装pandas用于数据处理 pip3 install pandas # 安装scikit-learn这是MultinomialNB的家 pip3 install scikit-learn # 安装结巴分词用于中文文本处理 pip3 install jieba这里有一个关键的坑行空板的默认软件源可能较慢甚至某些ARM架构的预编译包wheel可能不存在。我的经验是如果安装失败可以尝试使用国内镜像源例如清华源pip3 install scikit-learn jieba -i https://pypi.tuna.tsinghua.edu.cn/simple安装完成后建议写一个简单的测试脚本验证关键库是否能正常导入import numpy as np import pandas as pd from sklearn.naive_bayes import MultinomialNB import jieba print(“所有核心库导入成功”)2.2 古诗词数据集的收集与清洗算法模型的好坏七八成取决于数据。对于古诗词分类我们需要一个带有题材标签的数据集。公开的中文古诗词数据集并不多且质量参差不齐。我经过一番搜寻和整理找到了一个包含数万首唐诗宋词并标注了“山水”、“边塞”、“咏物”、“送别”等题材的数据集。你也可以从一些开源中文NLP项目中找到类似资源。数据通常是以JSON或CSV格式存在。我将其下载到行空板本地目录例如/home/pi/poetry_data.csv。数据清洗是枯燥但至关重要的一步去重与去噪删除完全重复的诗句以及标签混乱或诗句内容为乱码的条目。标签统一将相似的标签进行合并。例如“山水田园”和“田园”可以合并为“山水田园”“送别”和“离别”合并为“送别”。文本清洗移除诗句中的现代标点如引号、破折号、数字、以及非中文字符。但保留古典标点如“”、“。”、“”、“”等因为它们有时也承载情感信息。样本平衡检查各个类别的样本数量。如果某个类别如“边塞”的诗句数量远少于其他类别如“抒情”模型可能会偏向于多数类。一种简单的处理方式是进行欠采样随机丢弃多数类样本或过采样复制或生成少数类样本。对于我们的入门项目可以暂时忽略但心里要有数。清洗后的数据我将其保存为一个新的CSV文件包含两列text诗句内容和label题材标签。2.3 中文文本预处理分词与停用词英文有天然的空格分隔单词而中文需要先进行“分词”。我选择jieba库因为它轻量且效果不错。但是古典诗词的语言与现代汉语差异很大jieba的默认词典可能无法准确切分一些古汉语词汇或专有名词如“阑干”、“逶迤”。我的解决方案是构建一个古诗词专用词典。我从清洗后的数据中统计高频的双字和三字组合将那些明显是一个整体且jieba会切错的词加入到用户自定义词典中。import jieba # 加载自定义词典文件每行格式词语 词频 词性 jieba.load_userdict(“poetry_dict.txt”) # 对一句诗进行分词 sentence “孤帆远影碧空尽唯见长江天际流。” seg_list jieba.lcut(sentence) # 精确模式返回列表 print(seg_list) # 输出[‘孤帆’ ‘远影’ ‘碧空’ ‘尽’ ‘’ ‘唯见’ ‘长江’ ‘天际’ ‘流’ ‘。’]接下来是去除停用词。停用词是指在文本中频繁出现但本身没有太多分类意义的词如“的”、“了”、“在”等。对于古诗词停用词列表也需要定制。古典诗词中“之”、“乎”、“者”、“也”等虚词以及“何处”、“谁人”等常见代词可以考虑加入停用词表。我从网上找了一个基础的中文停用词表并手动添加了一些古诗词中常见的虚词和泛称代词。预处理函数大致如下def preprocess_text(text, stopwords): “”” 文本预处理函数分词并去除停用词。 Args: text: 原始诗句字符串。 stopwords: 停用词集合。 Returns: 处理后的词语列表。 “”” # 分词 words jieba.lcut(text) # 去除停用词和非中文字符保留诗词中的标点有时有助于情感判断这里我们先过滤掉 filtered_words [word for word in words if word not in stopwords and ‘\u4e00’ word ‘\u9fff’] return filtered_words注意是否去除标点是一个值得商榷的点。像“”可能出现在慷慨激昂的边塞诗中“……”可能用于愁绪绵长的思乡诗中。在这个项目中我选择先去除以简化特征。在后续优化时可以将标点作为单独的特征进行考虑。3. 特征工程从文字到数字机器学习模型无法直接理解文字我们必须将清洗、分词后的文本转化为它能处理的数字形式这个过程就是特征工程。对于文本分类最经典和常用的方法是词袋模型结合TF-IDF。3.1 词袋模型与TF-IDF向量化词袋模型很简单它忽略词语的顺序和语法只关心“哪些词出现了”以及“出现了多少次”。整个数据集的所有词语构成一个“词袋”每首诗被表示为一个长向量向量的每个维度对应词袋中的一个词值就是该词在这首诗里出现的次数或经过加权的值。但是简单的词频统计有问题像“明月”、“春风”这类在所有题材诗中都可能出现的常见词其频次很高但对分类的区分度不大。而像“烽火”、“胡尘”这类词可能只在“边塞”诗中频繁出现它们才是关键特征。TF-IDF就是为了解决这个问题而设计的。TF代表“词频”IDF代表“逆文档频率”。一个词的TF-IDF值随着它在该文档中出现的次数成正比增加但随着它在整个语料库中出现的频率成反比下降。这样常见词的权重会被压低而稀有且在某类文档中常见的词权重会提高。在scikit-learn中我们可以使用TfidfVectorizer一站式完成分词需配合自定义分词器、停用词过滤、TF-IDF计算。from sklearn.feature_extraction.text import TfidfVectorizer import jieba # 自定义分词器适配jieba def chinese_tokenizer(text): return jieba.lcut(text) # 初始化TF-IDF向量化器 # max_features: 只保留最重要的N个词汇特征控制向量维度对嵌入式设备很重要 # tokenizer: 使用我们自定义的分词器 # stop_words: 传入停用词列表 tfidf_vectorizer TfidfVectorizer(tokenizerchinese_tokenizer, stop_wordsstopwords_list, max_features2000) # 根据行空板内存调整2000是一个起点 # 假设corpus是包含所有预处理后诗句文本的列表 # 注意这里传入的是未分词的原始诗句字符串TfidfVectorizer会调用我们的tokenizer X_tfidf tfidf_vectorizer.fit_transform(corpus)执行fit_transform后X_tfidf就是一个稀疏矩阵每一行代表一首诗每一列代表一个特征词值就是TF-IDF权重。这个矩阵就是我们模型的输入特征。3.2 特征维度选择与权衡max_features这个参数至关重要。设置得太小如500可能会丢失关键特征导致模型精度下降。设置得太大如10000生成的矩阵会非常庞大不仅训练速度慢在推理时即对新诗分类时也会占用更多内存和计算资源这对于行空板这样的嵌入式设备是很大的负担。我通过一个简单的实验来确定这个参数将数据集按比例划分为训练集和测试集然后尝试不同的max_features值例如500, 1000, 2000, 3000在训练集上训练MultinomialNB模型在测试集上查看准确率。同时在行空板上监控内存占用和预测单句所需时间。我得到的经验是对于这个古诗词数据集当max_features在1500到2500之间时准确率提升已不明显但内存占用和计算时间线性增长。考虑到行空板的性能我最终选择了2000。这是一个在精度和效率之间的典型折中。4. MultinomialNB模型原理与训练特征准备好了接下来就是“厨师”登场——MultinomialNB模型。为什么是它我们得先理解它是怎么工作的。4.1 朴素贝叶斯原理浅析朴素贝叶斯分类器基于贝叶斯定理并做了一个“朴素”的假设特征之间相互独立。这个假设在现实中很难成立比如“长江”和“东流”这两个词在诗词中很可能同时出现但神奇的是即便如此它在文本分类上往往表现不俗。对于一首诗我们想求它属于某个题材c如“山水田园”的概率P(c|诗)。根据贝叶斯定理P(c|诗) P(诗|c) * P(c) / P(诗)由于对于同一首诗P(诗)是常数所以我们只需要比较P(诗|c) * P(c)的大小。P(c)是先验概率即数据集中题材c出现的频率。关键是如何计算P(诗|c)即在题材c下出现这首诗的概率。在词袋模型下一首诗就是一组词的集合。基于“朴素”假设我们认为这些词的出现是相互独立的。因此P(诗|c) P(词1|c) * P(词2|c) * … * P(词n|c)而P(词i|c)就是在题材c的所有诗中词语词i出现的概率。这正好可以用我们TF-IDF向量化后每个特征词在各类别下的频率分布来估计。MultinomialNB就是专门处理这种特征为离散计数或像TF-IDF这样的连续值但模型将其视为多项式分布情况的朴素贝叶斯变体。4.2 模型训练与评估训练过程在scikit-learn中异常简单from sklearn.model_selection import train_test_split from sklearn.naive_bayes import MultinomialNB from sklearn.metrics import classification_report, accuracy_score # 1. 划分数据集 (80%训练 20%测试) X_train, X_test, y_train, y_test train_test_split(X_tfidf, labels, test_size0.2, random_state42) # 2. 初始化并训练模型 nb_classifier MultinomialNB() nb_classifier.fit(X_train, y_train) # 3. 在测试集上预测并评估 y_pred nb_classifier.predict(X_test) print(“准确率”, accuracy_score(y_test, y_pred)) print(“\n详细分类报告\n”, classification_report(y_test, y_pred))训练完成后nb_classifier对象就保存了所有P(词i|c)和P(c)的信息。评估报告不仅能看总体准确率还能看每个类别的精确率、召回率和F1-score帮助我们判断模型在哪些题材上表现好哪些题材上容易混淆。例如在我的实验中模型在“山水田园”和“边塞征战”上区分度很高F1-score 0.85因为它们的特征词差异明显“青山绿水” vs “烽火狼烟”。但在“送别”和“思乡”上容易混淆F1-score ~ 0.7因为两者都常包含“离愁”、“明月”、“孤舟”等意象。这恰恰反映了古诗词本身情感的复杂性。4.3 模型保存与轻量化训练好的模型需要保存下来供行空板上的应用加载使用。scikit-learn推荐使用joblib因为它对于包含大量numpy数组的模型如我们的分类器比标准的pickle更高效。import joblib # 保存模型 model_filename ‘poetry_nb_classifier.joblib’ joblib.dump(nb_classifier, model_filename) # 保存TF-IDF向量化器对新诗进行预测时必须用同一个向量化器转换 vectorizer_filename ‘tfidf_vectorizer.joblib’ joblib.dump(tfidf_vectorizer, vectorizer_filename)将这两个.joblib文件拷贝到行空板上我们的“大脑”就准备好了。整个模型文件通常只有几MB大小非常适合嵌入式设备。5. 行空板应用集成与交互设计现在算法模型已经就绪是时候让它“住进”行空板并和用户见面了。行空板最大的优势之一就是其内置的屏幕和简单的UI编程能力我们可以用pinpong库行空板官方推荐或tkinter来创建一个图形界面。5.1 使用PinPong库构建简易GUIpinpong库封装了行空板的硬件控制和UI组件使用起来比tkinter更简单直观。我们的界面需要以下几个元素一个文本输入框TextInput让用户输入或粘贴一首诗。一个按钮Button点击后触发分类。一个标签Label或文本框显示分类结果。from pinpong.board import Board, TextInput, Button, Label from pinpong.libs.dfrobot_ssd1306 import SSD1306_I2C # 假设使用OLED屏幕 import joblib import jieba # 初始化行空板 Board().begin() # 加载模型和向量化器 print(“正在加载模型…”) classifier joblib.load(‘poetry_nb_classifier.joblib’) vectorizer joblib.load(‘tfidf_vectorizer.joblib’) print(“模型加载完毕”) # 定义分类函数 def classify_poetry(): # 获取输入框中的文本 poem_text text_input.value() if not poem_text.strip(): result_label.text(“请输入诗句”) return # 使用相同的预处理和向量化流程 # 注意这里需要调用vectorizer.transform 而不是fit_transform poem_tfidf vectorizer.transform([poem_text]) # 预测 prediction classifier.predict(poem_tfidf)[0] # 显示结果 result_label.text(f“预测题材{prediction}”) # 创建UI组件 text_input TextInput(10, 10, 200, 100, “请输入古诗词…”) # 位置和大小 classify_btn Button(10, 120, 80, 40, “分类”, classify_poetry) result_label Label(10, 170, 200, 30, “结果将显示在这里”) # 进入主循环等待用户交互 while True: pass这段代码创建了一个非常基础的界面。在实际项目中你可能需要更美观的布局并添加一个“清空”按钮或者显示预测的概率分布classifier.predict_proba让用户看到模型对各题材的“置信度”。5.2 性能优化与实时性考虑在行空板上运行性能是需要时刻关注的。我遇到了两个主要问题首次加载慢加载joblib模型文件尤其是TfidfVectorizer它包含了2000维的词汇表需要几秒钟。这会在应用启动时造成卡顿。解决方案在程序启动时在后台线程中预先加载模型并显示一个“加载中”的提示。或者考虑将词汇表等不变的数据以更轻量的格式如Python字典存储和加载。预测延迟用户点击按钮后从分词、TF-IDF转换到预测整个过程大约需要100-200毫秒。对于交互来说这个延迟可以接受但感知明显。优化措施缓存分词结果如果用户反复对相似句子进行分类可以缓存分词结果。简化预处理在保证效果的前提下审视预处理步骤是否可简化。例如在实时预测时可以跳过一些复杂的清洗步骤。使用predict_proba的阈值如果模型对所有类别的预测概率都低于某个阈值如0.6可以返回“无法确定”或“其他”这比强行给出一个低置信度的答案体验更好。5.3 扩展思考从单句到整诗从分类到生成这个项目目前处理的是单句或短诗。一个自然的扩展是对整首诗进行分类。这带来了新的挑战如何整合一首诗中多个句子的信息简单的方法是将所有句子拼接成一个长文本再处理但会丢失句子间的结构信息。更高级的方法可以考虑使用简单的神经网络如TextCNN或考虑诗句顺序如RNN但这些模型在行空板上部署的复杂度会大大增加。另一个更有趣的方向是结合分类结果进行交互。例如当分类为“山水田园”时行空板可以控制RGB灯显示绿色并播放一段鸟鸣流水的声音通过音频输出接口。当分类为“边塞征战”时显示红色并播放鼓声。这样硬件就和诗词的意境产生了联动项目的趣味性和展示性会更强。更进一步我们甚至可以做一个“飞花令”游戏行空板随机给出一个关键词如“月”用户说出包含该词的诗句行空板实时判断诗句是否合规并分类同时记录比分。这需要引入额外的规则判断逻辑但技术上完全可行。6. 项目总结与避坑指南回顾整个项目从环境搭建、数据处理、模型训练到硬件集成是一个完整的AIoT微型项目闭环。它证明了即使在行空板这样资源受限的设备上也能运行有意义的机器学习应用。以下是几点核心心得和踩坑总结数据质量决定天花板古诗词数据集的标注质量直接影响模型效果。网上找到的数据集标签可能不准确或不一致花费时间进行人工校对和清洗是值得的。一个小的、干净的、标注准确的数据集远胜于一个大的、嘈杂的数据集。嵌入式环境下的依赖管理是头号难题不同于在PC上行空板的ARM架构和有限的存储空间使得安装某些Python库变得棘手。务必在项目开始时就规划好所需库及其版本并优先寻找ARM兼容的预编译轮子wheel。如果必须从源码编译要做好耗时长且可能失败的心理准备。特征维度是性能与精度的平衡点max_features是TF-IDF中最重要的调参项之一。在嵌入式设备上不要盲目追求高维度。通过绘制“特征数-准确率”和“特征数-预测耗时”曲线可以找到一个合理的甜蜜点。在我的行空板类似树莓派4B性能上2000维是一个不错的起点。朴素贝叶斯的“朴素”假设在诗词中失效案例这个模型无法理解“东风不与周郎便铜雀春深锁二乔”中的典故和上下文关系。它只看到“东风”、“周郎”、“铜雀”、“春深”等词。如果这些词在“咏史”和“抒情”题材中分布相似模型就可能分错。这是算法本身的局限也是所有基于词袋模型方法的通病。对于这类问题要么接受要么考虑更复杂的模型但需权衡设备性能。交互设计要考虑硬件特性行空板的屏幕不大输入文字不太方便。在实际演示时我更倾向于预先在程序中内置一些经典诗句作为例子让用户通过按钮选择而不是手动输入。或者可以开发一个配套的手机App或网页端通过Wi-Fi将诗句发送给行空板处理并返回结果这样用户体验会好很多。这个项目就像一次小小的跨界探险把古老的诗词、现代的机器学习算法和具体的硬件平台连接在一起。它可能没有工业级应用那么严谨和强大但其中涉及的思路、方法和遇到的问题对于想要入门嵌入式AI或创意编程的朋友来说是一次非常有益的实践。当你看到行空板屏幕亮起并准确地为你手中的诗句打上“山水田园”的标签时那种亲手创造智能的成就感便是对所有这些折腾最好的回报。