一、项目概述
1. 项目背景
在数字化时代,电子邮件、短信和即时消息已成为人们学习、工作与生活中不可或缺的沟通工具。然而,垃圾邮件、广告骚扰、诈某骗信息的泛滥也日益严重——弹窗广告、虚假中奖、刷单返利、不明链接等不仅占用大量存储空间、分散注意力,更暗藏钓鱼网站、信息窃取等安全隐患。传统的人工逐条判断和手动删除方式效率低下、耗时费力,且容易因疏忽导致个人信息泄露或财产损失。
基于这一日常困扰,我们希望借助人工智能技术,为“信息净化”提供一种简单、高效、可落地的解决方案。AI文本分类作为自然语言处理中最成熟、最贴近生活的应用之一,能够自动理解、判断和分类文本内容,让机器代替人工完成垃圾邮件与正常邮件的智能区分,使数字生活更清爽、更安全。
2. 核心功能
本项目实现以下核心功能:
文本二分类:精准区分“垃圾邮件”与“正常邮件”两类文本。
实时推理:用户输入任意短文本,模型立即输出分类结果及置信度。
批量检测:支持通过文件上传或SIOT消息队列进行批量文本分类。
可视化结果推送:通过Web页面实时显示分类结果,便于监控与集成。
3. 项目目标
构建一个轻量化、可直接运行的二分类文本分类模型,实现对邮件文本的自动识别,精准区分垃圾邮件与非垃圾邮件,达到简单实用、稳定可靠的效果。让AI模型学习“文本特征—类别标签”之间的映射规律,通过标注数据捕捉广告、诈某骗类文本的语言模式,最终实现对未知邮件内容的自主判断与智能分类。
4. 开发环境
软件:DFRobot Mind+ V2.0(含文本分类模型训练模块、实时模式编程环境)
硬件:普通电脑(配备摄像头、麦克风非必需,仅需键盘鼠标)
任务类型:文本分类(二分类)
辅助工具:SIOT v2(消息队列服务)、HTML浏览器(结果展示)
二、项目原理
本项目采用二分类文本分类架构,预先设定两个标签:垃圾邮件、正常邮件。
核心逻辑简单清晰:在Mind+可视化AI平台中,我们先导入人工标注好的邮件文本数据,软件自动完成文本清洗、分词、特征提取等底层处理;随后通过内置机器学习算法进行模型训练,让AI不断“学习”不同类型文本的语言特点;训练完成后,模型具备自主识别能力,输入新的未知文本,即可快速输出分类结果。
整个项目遵循标准AI工作流程:

整个项目遵循标准 AI 工作流程:数据准备 → 模型训练 → 模型验证 → 实时文本识别 → 输出分类结果每一步环环相扣,从 “教会 AI” 到 “AI 自主判断”,完整展现了一个小型文本分类项目的全生命周期。
三、项目实施步骤

1. 数据准备(数据采集与标注)
数据是AI的“粮食”。为了让模型学得更准,我们精心收集并整理了贴近真实场景的文本数据,共200条,分为两类:

所有数据统一采用纯文本短句格式,无特殊符号、无冗余内容,保证数据干净规范,让AI在训练时更聚焦于语义本身。每一条文本都经过人工核对标注,为模型打下扎实的学习基础。
2.操作步骤(Mind+ V2):
(1)打开 Mind+ V2.0, 点击文本分类。

(2)默认有2个类别,标签分别是Class1与Class2,需要多个分类时,可以点击下方的新增类别进行新增。
点击标签Class1、Class2后的铅笔,可以对类别标签进行修改。

这里我们为了方便区分,分别修改标签为垃圾邮件、正常邮件。

(3)为每个类别添加样本
① 手动输入样本并点击“添加样本”,重复100次。
需要对每个类别分别添加样本,通过点击添加样本来完成。

输入样本,点击添加样本,完成第一个样本的添加。

一个文字样本添加完成。

重复上面的步骤200次,依次完成2个分类各100个文字样本的添加。
② 或使用“上传文件”功能,批量导入预先准备好的.txt文本文件(每行一个样本)。
当然,Mind+ 还提供了更高效的添加样本的方式——通过上传文件的方式添加文字样本,点击上传按钮。

选择文件上传,选择 文字样本 的文本文件,这里上传了一个有100个文字样本的.txt文件。

完成了100个文字样本的添加。

同样的方法,添加正常邮件样本。

(4)确认两个类别各拥有100条样本后,数据准备完成。
数据采集亮点:样本来源于真实生活场景,兼顾多样性与代表性,并刻意加入了易混淆的短句(如“发某票已开”与“代开发某票”),以增强模型的鲁棒性。
2.模型训练
点击 训练模型 完成模型的训练。
3.模型校验与效果评估
型训练完成后,进入校验阶段。我们输入多组未参与训练的测试文本,观察输出结果及置信度。
输入测试数据文本,即可实时看到输出结果。如输入:发某票,输出:垃圾邮件72%,正常邮件28% 。

继续输入已开,即这时输入的文本为发某票已开,输出:垃圾邮件0%,正常邮件100% 。
典型测试用例:

经过30组随机测试,模型准确率达到96.7%。整体表现稳定可靠,满足实际使用需求。
校验完成后,点击“导出模型”将训练好的模型文件保存到本地,供后续推理使用。

我们逐条测试、反复对比,观察 AI 判断是否准确、置信度是否合理。经过多组样本测试,模型表现稳定,判断结果与预期高度一致,证明训练效果可靠。这一步就像老师对学生进行单元测试,确保 AI 真正掌握了技能,而不是死记硬背。
4.实时结果推送(SIOT + Web展示)
为实现垃圾分类结果的实时监控和远程查看,我们利用Mind+内置的SIOT(物联网消息队列)功能,将推理结果推送到Web页面。
配置步骤:
(1)点击 红点 ,修改或者记录弹窗中的主题名称。
(2)打开SIOT v2的Web管理页面,添加相同的主题。

继续添加主题

(3)确认SIOT连接正常(绿点亮起)。

(4)编写一个简单的HTML页面,通过MQTT over WebSocket订阅同一主题,实时显示分类结果。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SIoT 实时看板 - 中文垃圾邮件分类</title>
<style>
* {
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', system-ui, sans-serif;
background: #eef2f5;
margin: 0;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
.card {
background: white;
border-radius: 20px;
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
padding: 20px 24px;
margin-bottom: 24px;
}
h2 {
margin-top: 0;
font-size: 1.5rem;
color: #1e4663;
}
h3 {
font-size: 1.1rem;
margin: 0 0 12px 0;
display: flex;
align-items: center;
gap: 8px;
}
.config-row {
display: flex;
flex-wrap: wrap;
gap: 16px;
margin-bottom: 20px;
}
.config-field {
flex: 1;
min-width: 140px;
}
label {
display: block;
font-size: 0.75rem;
font-weight: 600;
color: #2c5a74;
margin-bottom: 6px;
}
input {
width: 100%;
padding: 8px 12px;
border: 1px solid #cbdde6;
border-radius: 14px;
font-family: monospace;
font-size: 0.9rem;
background: #fefefe;
}
button {
background: #eef2f7;
border: none;
padding: 8px 20px;
border-radius: 40px;
font-weight: 600;
cursor: pointer;
transition: 0.2s;
margin-right: 12px;
margin-bottom: 8px;
}
button.primary {
background: #1f6392;
color: white;
}
button.primary:hover {
background: #0e4a70;
}
button.danger {
background: #ffe6e3;
color: #c0392b;
}
.status {
display: inline-flex;
align-items: center;
gap: 8px;
background: #f0f4f9;
padding: 6px 14px;
border-radius: 60px;
font-size: 0.85rem;
}
.led {
width: 10px;
height: 10px;
border-radius: 50%;
background: #aaa;
}
.led.online { background: #2ecc71; box-shadow: 0 0 4px #2ecc71; }
.led.offline { background: #e74c3c; }
/* 双栏消息区布局 (上下排列,每个独立) */
.messages-grid {
display: flex;
flex-direction: column;
gap: 24px;
}
.message-panel {
background: #fafcff;
border-radius: 20px;
overflow: hidden;
border: 1px solid #e2edf2;
box-shadow: 0 2px 6px rgba(0,0,0,0.03);
}
.msg-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 20px;
background: #f8fafd;
border-bottom: 1px solid #e2edf2;
}
.msg-list {
height: 380px;
overflow-y: auto;
padding: 12px;
background: white;
display: flex;
flex-direction: column;
}
/* 倒序排列:新消息会动态插入到最前面,flex-direction: column 配合 prepend 即可 */
.msg-item {
background: #f9fbfe;
border-left: 4px solid #2c8cbb;
margin-bottom: 12px;
padding: 12px 16px;
border-radius: 16px;
font-size: 0.85rem;
flex-shrink: 0;
}
.system-msg-item {
border-left-color: #f39c12;
background: #fffaf3;
}
.msg-meta {
display: flex;
gap: 12px;
margin-bottom: 8px;
font-size: 0.7rem;
color: #4d7a9b;
}
.topic-badge {
font-family: monospace;
background: #e9f0f5;
padding: 2px 12px;
border-radius: 30px;
font-weight: 600;
}
.payload {
font-family: 'Fira Code', monospace;
word-break: break-word;
white-space: pre-wrap;
background: #ffffff;
padding: 8px 12px;
border-radius: 12px;
border: 1px solid #eef2f0;
}
.empty-tip {
text-align: center;
color: #9bb7cd;
padding: 48px;
font-style: italic;
}
.log-tip {
margin-top: 12px;
font-size: 0.75rem;
background: #f4f8fc;
padding: 8px 12px;
border-radius: 16px;
color: #2c5a74;
}
footer {
text-align: center;
font-size: 0.7rem;
color: #7d9eb5;
margin-top: 20px;
}
</style>
</head>
<body>
<div class="container">
<div class="card">
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap;">
<h2>📡 SIoT 实时看板 - 中文垃圾邮件分类</h2>
<div class="status">
<span class="led offline" id="statusLed"></span>
<span id="statusText">未连接</span>
</div>
</div>
<div class="config-row">
<div class="config-field"><label>WebSocket 地址</label><input type="text" id="wsHost" value="192.168.123.17"></div>
<div class="config-field"><label>端口</label><input type="number" id="wsPort" value="1888"></div>
<div class="config-field"><label>路径</label><input type="text" id="wsPath" value="/ws"></div>
</div>
<div class="config-row">
<div class="config-field"><label>用户名</label><input type="text" id="mqttUser" value="siot"></div>
<div class="config-field"><label>密码</label><input type="password" id="mqttPass" value="dfrobot"></div>
<div class="config-field"><label>Client ID (自动)</label><input type="text" id="clientIdPreview" readonly style="background:#eef2f5;"></div>
</div>
<div class="config-row">
<div class="config-field"><label>📌 订阅主题 1</label><input type="text" id="topic1" value="siot/ai"></div>
<div class="config-field"><label>📌 订阅主题 2</label><input type="text" id="topic2" value="siot/ai/score"></div>
</div>
<div>
<button id="connectBtn" class="primary">🔌 连接 & 订阅</button>
<button id="disconnectBtn" class="danger">⛔ 断开连接</button>
<button id="reconnectBtn">🔄 重连</button>
<button id="clearBtn">🗑️ 清空所有消息</button>
</div>
<div class="log-tip">
💡 提示:业务消息(订阅主题)与系统日志已分区域显示,<strong>新消息均显示在顶部</strong>。<br>
请使用 HTTP 服务器打开本页面(如 python -m http.server 8000),避免 file:// 协议限制。
</div>
</div>
<div class="messages-grid">
<!-- 业务消息区:订阅的主题消息 -->
<div class="message-panel">
<div class="msg-header">
<span>📨 实时业务消息 (siot/ai & siot/ai/score)</span>
<span style="font-size:0.7rem;">✨ 最新消息在上方</span>
</div>
<div class="msg-list" id="businessMsgList">
<div class="empty-tip">💬 暂无业务消息,等待推送...</div>
</div>
</div>
<!-- 系统消息区:连接、错误、订阅状态等 -->
<div class="message-panel">
<div class="msg-header">
<span>⚙️ 系统事件 / 日志</span>
<span style="font-size:0.7rem;">📌 最新日志在上方</span>
</div>
<div class="msg-list" id="systemMsgList">
<div class="empty-tip">🔧 系统日志将显示在这里</div>
</div>
</div>
</div>
<footer>原生 WebSocket + MQTT 3.1.1 | 消息分离显示 · 新消息置顶</footer>
</div>
<script>
(function(){
// DOM 元素
const wsHostInput = document.getElementById('wsHost');
const wsPortInput = document.getElementById('wsPort');
const wsPathInput = document.getElementById('wsPath');
const mqttUserInput = document.getElementById('mqttUser');
const mqttPassInput = document.getElementById('mqttPass');
const topic1Input = document.getElementById('topic1');
const topic2Input = document.getElementById('topic2');
const clientIdPreview = document.getElementById('clientIdPreview');
const connectBtn = document.getElementById('connectBtn');
const disconnectBtn = document.getElementById('disconnectBtn');
const reconnectBtn = document.getElementById('reconnectBtn');
const clearBtn = document.getElementById('clearBtn');
const businessMsgList = document.getElementById('businessMsgList');
const systemMsgList = document.getElementById('systemMsgList');
const statusLed = document.getElementById('statusLed');
const statusTextSpan = document.getElementById('statusText');
// 状态变量
let ws = null;
let isConnected = false;
let currentClientId = null;
let expectedTopics = [];
// ---------- 辅助函数:消息展示(各自独立,新消息 prepend 到顶部)----------
// 业务消息(主题数据)
function addBusinessMessage(topic, payload) {
// 移除占位符
const emptyDiv = businessMsgList.querySelector('.empty-tip');
if (emptyDiv && businessMsgList.children.length === 1 && emptyDiv) {
businessMsgList.innerHTML = '';
}
const msgDiv = document.createElement('div');
msgDiv.className = 'msg-item';
const time = new Date().toLocaleTimeString('zh-CN', { hour12: false }) + '.' + String(Date.now() % 1000).padStart(3,'0');
msgDiv.innerHTML = `
<div class="msg-meta">
<span class="topic-badge">📁 ${escapeHtml(topic)}</span>
<span>🕒 ${time}</span>
</div>
<div class="payload">📨 ${escapeHtml(payload)}</div>
`;
// 最新消息插入到最前面 (prepend)
businessMsgList.prepend(msgDiv);
// 限制最大条目数 (保留最多400条)
while (businessMsgList.children.length > 400) {
businessMsgList.removeChild(businessMsgList.lastChild);
}
// 滚动到顶部,方便看到最新消息
businessMsgList.scrollTop = 0;
}
// 系统消息(连接、错误、订阅状态等)
function addSystemMessage(msg, isError = false) {
const emptyDiv = systemMsgList.querySelector('.empty-tip');
if (emptyDiv && systemMsgList.children.length === 1 && emptyDiv) {
systemMsgList.innerHTML = '';
}
const msgDiv = document.createElement('div');
msgDiv.className = 'msg-item system-msg-item';
if (isError) {
msgDiv.style.borderLeftColor = '#e74c3c';
msgDiv.style.background = '#fff5f5';
}
const time = new Date().toLocaleTimeString('zh-CN', { hour12: false }) + '.' + String(Date.now() % 1000).padStart(3,'0');
msgDiv.innerHTML = `
<div class="msg-meta">
<span class="topic-badge" style="background:#fdebd0;">💬 系统</span>
<span>🕒 ${time}</span>
</div>
<div class="payload">${escapeHtml(msg)}</div>
`;
systemMsgList.prepend(msgDiv);
while (systemMsgList.children.length > 400) {
systemMsgList.removeChild(systemMsgList.lastChild);
}
systemMsgList.scrollTop = 0;
}
function escapeHtml(str) {
if (!str) return '';
return str.replace(/[&<>]/g, function(m) {
if (m === '&') return '&';
if (m === '<') return '<';
if (m === '>') return '>';
return m;
});
}
// 清空所有消息(业务+系统)
function clearAllMessages() {
businessMsgList.innerHTML = '<div class="empty-tip">💬 暂无业务消息,等待推送...</div>';
systemMsgList.innerHTML = '<div class="empty-tip">🔧 系统日志将显示在这里</div>';
addSystemMessage("🧹 已清空所有消息记录", false);
}
// 生成随机 Client ID
function genClientId() {
return "web_siot_" + Math.random().toString(36).substring(2, 12) + "_" + Date.now();
}
// 更新连接状态UI
function setUiConnected(connected, errMsg = null) {
isConnected = connected;
if (connected) {
statusLed.className = "led online";
statusTextSpan.innerText = "已连接";
connectBtn.disabled = true;
disconnectBtn.disabled = false;
reconnectBtn.disabled = false;
if (currentClientId) clientIdPreview.value = currentClientId;
} else {
statusLed.className = "led offline";
statusTextSpan.innerText = errMsg ? `离线 (${errMsg})` : "未连接";
connectBtn.disabled = false;
disconnectBtn.disabled = true;
reconnectBtn.disabled = false;
clientIdPreview.value = currentClientId ? currentClientId + " (已断开)" : "未连接";
}
}
// ---------- MQTT over WebSocket 核心功能 ----------
function sendSubscribe(topics) {
if (!ws || ws.readyState !== WebSocket.OPEN) {
addSystemMessage("❌ WebSocket 未打开,无法订阅", true);
return false;
}
const packetId = Math.floor(Math.random() * 65535);
let payloadBuffer = [];
payloadBuffer.push((packetId >> 8) & 0xFF, packetId & 0xFF);
for (let topic of topics) {
if (!topic) continue;
const topicBytes = new TextEncoder().encode(topic);
const len = topicBytes.length;
payloadBuffer.push((len >> 8) & 0xFF, len & 0xFF);
payloadBuffer.push(...topicBytes);
payloadBuffer.push(0x00); // QoS 0
}
const remainingLength = payloadBuffer.length;
let remainBytes = [];
let lenVal = remainingLength;
do {
let digit = lenVal % 128;
lenVal = Math.floor(lenVal / 128);
if (lenVal > 0) digit |= 0x80;
remainBytes.push(digit);
} while (lenVal > 0);
const fixedHeader = [0x82, ...remainBytes];
const fullPacket = new Uint8Array([...fixedHeader, ...payloadBuffer]);
ws.send(fullPacket);
addSystemMessage(`📡 发送订阅请求: ${topics.filter(t=>t).join(', ')} (PacketID=${packetId})`);
return true;
}
function sendMqttConnect(user, pass, clientId) {
if (!ws || ws.readyState !== WebSocket.OPEN) return false;
const protocolName = "MQTT";
const protocolLevel = 4;
let connectFlags = 0x02;
if (user && user.length > 0) connectFlags |= 0x80;
if (pass && pass.length > 0) connectFlags |= 0x40;
const keepAlive = 60;
let varHeader = [];
varHeader.push(0, protocolName.length);
for (let i=0; i<protocolName.length; i++) varHeader.push(protocolName.charCodeAt(i));
varHeader.push(protocolLevel);
varHeader.push(connectFlags);
varHeader.push(keepAlive >> 8, keepAlive & 0xFF);
let payloadBytes = [];
const clientIdBytes = new TextEncoder().encode(clientId);
payloadBytes.push((clientIdBytes.length >> 8) & 0xFF, clientIdBytes.length & 0xFF);
payloadBytes.push(...clientIdBytes);
if (user && user.length > 0) {
const userBytes = new TextEncoder().encode(user);
payloadBytes.push((userBytes.length >> 8) & 0xFF, userBytes.length & 0xFF);
payloadBytes.push(...userBytes);
}
if (pass && pass.length > 0) {
const passBytes = new TextEncoder().encode(pass);
payloadBytes.push((passBytes.length >> 8) & 0xFF, passBytes.length & 0xFF);
payloadBytes.push(...passBytes);
}
const remainingLength = varHeader.length + payloadBytes.length;
let remainBytes = [];
let lenVal = remainingLength;
do {
let digit = lenVal % 128;
lenVal = Math.floor(lenVal / 128);
if (lenVal > 0) digit |= 0x80;
remainBytes.push(digit);
} while (lenVal > 0);
const fixedHeader = [0x10, ...remainBytes];
const connectPacket = new Uint8Array([...fixedHeader, ...varHeader, ...payloadBytes]);
ws.send(connectPacket);
addSystemMessage(`🔐 发送 CONNECT 报文 (ClientID: ${clientId})`);
return true;
}
function parseMqttPacket(dataArray) {
const data = new Uint8Array(dataArray);
if (data.length < 2) return;
const header = data[0];
const packetType = (header >> 4) & 0x0F;
let multiplier = 1;
let remainingLength = 0;
let offset = 1;
while (true) {
if (offset >= data.length) return;
const digit = data[offset];
remainingLength += (digit & 0x7F) * multiplier;
multiplier *= 128;
offset++;
if ((digit & 0x80) === 0) break;
}
if (offset + remainingLength > data.length) return;
const payloadStart = offset;
if (packetType === 2) { // CONNACK
if (remainingLength >= 2) {
const returnCode = data[payloadStart+1];
if (returnCode === 0) {
addSystemMessage("✅ MQTT 连接成功 (CONNACK)");
setUiConnected(true);
const t1 = topic1Input.value.trim();
const t2 = topic2Input.value.trim();
const topicsToSub = [t1, t2].filter(t => t);
if (topicsToSub.length) {
sendSubscribe(topicsToSub);
expectedTopics = topicsToSub;
} else {
addSystemMessage("⚠️ 没有填写有效主题,请手动填写后点击重连", true);
}
} else {
addSystemMessage(`❌ 连接拒绝,返回码: ${returnCode}`, true);
setUiConnected(false, "认证失败");
ws.close();
}
}
}
else if (packetType === 3) { // PUBLISH
let pos = payloadStart;
if (pos+2 > data.length) return;
const topicLen = (data[pos] << 8) | data[pos+1];
pos += 2;
if (pos+topicLen > data.length) return;
const topicBytes = data.slice(pos, pos+topicLen);
const topic = new TextDecoder().decode(topicBytes);
pos += topicLen;
const payloadBytes = data.slice(pos, payloadStart+remainingLength);
let payloadStr = new TextDecoder().decode(payloadBytes);
if (payloadStr === "") payloadStr = "(空)";
// 业务消息只显示订阅的主题,但为了完整,所有 PUBLISH 都展示在业务区(用户也可只关注那两个主题)
addBusinessMessage(topic, payloadStr);
}
else if (packetType === 9) { // SUBACK
addSystemMessage("📎 订阅确认 (SUBACK) 已收到");
}
}
// 连接主逻辑
function connect() {
if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) {
addSystemMessage("已有连接,请先断开", true);
return;
}
let host = wsHostInput.value.trim();
let port = parseInt(wsPortInput.value, 10);
let path = wsPathInput.value.trim();
if (!host) { addSystemMessage("请填写服务器地址", true); return; }
if (isNaN(port)) port = 1888;
if (!path.startsWith('/')) path = '/' + path;
const url = `ws://${host}:${port}${path}`;
const username = mqttUserInput.value.trim();
const password = mqttPassInput.value;
const clientId = genClientId();
currentClientId = clientId;
clientIdPreview.value = clientId + " (连接中...)";
addSystemMessage(`🔌 正在连接 ${url}`);
ws = new WebSocket(url);
ws.binaryType = "arraybuffer";
ws.onopen = () => {
addSystemMessage("WebSocket 已打开,发送 MQTT CONNECT...");
sendMqttConnect(username, password, clientId);
};
ws.onerror = (err) => {
addSystemMessage(`WebSocket 错误: ${err.type}`, true);
setUiConnected(false, "WebSocket错误");
};
ws.onclose = (ev) => {
addSystemMessage(`WebSocket 已关闭 (code: ${ev.code})`, ev.wasClean ? false : true);
setUiConnected(false, "连接关闭");
ws = null;
};
ws.onmessage = (event) => {
if (event.data instanceof ArrayBuffer) {
parseMqttPacket(new Uint8Array(event.data));
} else {
addSystemMessage("收到非二进制数据(忽略)", true);
}
};
}
function disconnect() {
if (ws) {
ws.close();
ws = null;
}
setUiConnected(false, "主动断开");
currentClientId = null;
clientIdPreview.value = "";
}
function reconnect() {
disconnect();
setTimeout(() => { connect(); }, 200);
}
// 绑定事件
connectBtn.addEventListener('click', connect);
disconnectBtn.addEventListener('click', disconnect);
reconnectBtn.addEventListener('click', reconnect);
clearBtn.addEventListener('click', clearAllMessages);
// 初始化状态
setUiConnected(false);
if (window.location.protocol === 'file:') {
addSystemMessage("⚠️ 检测到 file:// 协议,请改用 HTTP 服务器打开此页面,否则 WebSocket 可能受限!", true);
}
addSystemMessage("👋 欢迎使用 SIoT 看板,点击「连接 & 订阅」开始接收消息");
})();
</script>
</body>
</html>

5. 实时模式下的模型推理(Mind+图形化编程)
在Mind+的“实时模式”下,我们可以通过拖拽积木块快速调用训练好的模型进行推理,无需编写Python代码。
程序设计框架流程

在mind+实时模式下,添加 模型训练推理 库。

选择文本分类,依此添加 初始化文本分类,添加使用()加载文本分类模型积木,选择前面训练后导出的模型,添加对()进行一次文本分类推理积木,在文本框中输入需要推理的文本,这里输入了代开发某票,就可以看到推理的结果。

具体积木代码(图形化描述):
事件:当绿旗被点击
扩展库:调用初始化文本分类积木
模型加载:使用“垃圾邮件识别模型”加载文本分类模型
输入处理:询问“请输入邮件内容:”并等待
推理执行:对(回答)进行一次文本分类推理
结果输出:
说“分类结果:”+(推理结果的类别)
说“垃圾邮件置信度:”+(文本分类ID0的置信度)
说“正常邮件置信度:”+(文本分类ID1的置信度)
循环:重复执行步骤4~7
通过以上图形化编程,我们实现了一个交互式垃圾邮件分类器,用户可在Mind+舞台区直接输入文本并立即得到反馈。
四、项目总结
本项目基于DFRobot Mind+ V2.0平台,以纯软件方式完整实现了文本分类任务,成功构建并部署了垃圾邮件识别模型。不仅解决了日常生活中信息杂乱的小困扰,更亲身体验了AI从“训练”到“应用”的全过程,充满实践意义与成就感。
项目亮点
低门槛、高实用性:无需复杂代码编写,无需专业硬件设备,依托Mind+可视化的AI模型训练与推理功能,通过“数据准备—模型训练—校验—部署”四步流程,快速完成完整文本分类项目,极大降低了人工智能入门的技术门槛。
真实场景驱动:所有训练数据均来源于生活实际,模型直接服务于日常信息过滤,体现了“人工智能源于生活、服务生活”的价值。
可扩展的实时推送:结合SIOT和Web页面,实现了分类结果的远程监控,为后续集成到邮件客户端或消息过滤器提供了技术原型。
图形化与代码双模式:既适合初学者拖拽积木学习AI原理,也可导出Python代码供进阶开发者二次开发。
训练效果总结

反思与展望
改进方向:目前仅支持短文本单句分类,未来可增加长文本分段处理、支持更多类别(如社交、促销、重要工作邮件等),并引入在线增量学习能力。
应用扩展:可将模型部署到行空板等边缘设备(创意智造组),结合硬件实现当检测到垃圾邮件时自动触发报警灯或物理删除按钮。
通过本次项目,我们不仅理解了文本分类的核心概念与实现原理,更深刻体会到人工智能技术的平易近人与实用价值——用技术解决小问题,用智能提升生活品质。这正是AI学习与实践最动人的意义。

返回首页
回到顶部


评论