回到首页 返回首页
回到顶部 回到顶部
返回上一页 返回上一页
best-icon

【灵感数字组】Mind+ V2 AI 文本分类模型训练 —— 智能垃圾邮件识别系统 简单

头像 豆爸 2026.04.15 182 0

一、项目概述

1. 项目背景

在数字化时代,电子邮件、短信和即时消息已成为人们学习、工作与生活中不可或缺的沟通工具。然而,垃圾邮件、广告骚扰、诈某骗信息的泛滥也日益严重——弹窗广告、虚假中奖、刷单返利、不明链接等不仅占用大量存储空间、分散注意力,更暗藏钓鱼网站、信息窃取等安全隐患。传统的人工逐条判断和手动删除方式效率低下、耗时费力,且容易因疏忽导致个人信息泄露或财产损失。

基于这一日常困扰,我们希望借助人工智能技术,为“信息净化”提供一种简单、高效、可落地的解决方案。AI文本分类作为自然语言处理中最成熟、最贴近生活的应用之一,能够自动理解、判断和分类文本内容,让机器代替人工完成垃圾邮件与正常邮件的智能区分,使数字生活更清爽、更安全。

2. 核心功能

本项目实现以下核心功能:

文本二分类:精准区分“垃圾邮件”与“正常邮件”两类文本。

实时推理:用户输入任意短文本,模型立即输出分类结果及置信度。

批量检测:支持通过文件上传或SIOT消息队列进行批量文本分类。

可视化结果推送:通过Web页面实时显示分类结果,便于监控与集成。

3. 项目目标

构建一个轻量化、可直接运行的二分类文本分类模型,实现对邮件文本的自动识别,精准区分垃圾邮件与非垃圾邮件,达到简单实用、稳定可靠的效果。让AI模型学习“文本特征—类别标签”之间的映射规律,通过标注数据捕捉广告、诈某骗类文本的语言模式,最终实现对未知邮件内容的自主判断与智能分类。

4. 开发环境

软件:DFRobot Mind+ V2.0(含文本分类模型训练模块、实时模式编程环境)

硬件:普通电脑(配备摄像头、麦克风非必需,仅需键盘鼠标)

任务类型:文本分类(二分类)

辅助工具:SIOT v2(消息队列服务)、HTML浏览器(结果展示)

二、项目原理

本项目采用二分类文本分类架构,预先设定两个标签:垃圾邮件、正常邮件。

核心逻辑简单清晰:在Mind+可视化AI平台中,我们先导入人工标注好的邮件文本数据,软件自动完成文本清洗、分词、特征提取等底层处理;随后通过内置机器学习算法进行模型训练,让AI不断“学习”不同类型文本的语言特点;训练完成后,模型具备自主识别能力,输入新的未知文本,即可快速输出分类结果。

整个项目遵循标准AI工作流程:

exported_image (5).png

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

exported_image (6).png


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

Snipaste_2026-04-24_12-32-51.jpg

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

001.jpg
(2)默认有2个类别,标签分别是Class1与Class2,需要多个分类时,可以点击下方的新增类别进行新增。
002.jpg

点击标签Class1、Class2后的铅笔,可以对类别标签进行修改。

Snipaste_2026-04-18_19-32-30.jpg

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

004.jpg

(3)为每个类别添加样本

① 手动输入样本并点击“添加样本”,重复100次。

需要对每个类别分别添加样本,通过点击添加样本来完成。

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

006.jpg

一个文字样本添加完成。

007.jpg

重复上面的步骤200次,依次完成2个分类各100个文字样本的添加。

② 或使用“上传文件”功能,批量导入预先准备好的.txt文本文件(每行一个样本)。

当然,Mind+ 还提供了更高效的添加样本的方式——通过上传文件的方式添加文字样本,点击上传按钮。

008.jpg

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

009.jpg

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

010.jpg

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

011.jpg

(4)确认两个类别各拥有100条样本后,数据准备完成。

数据采集亮点:样本来源于真实生活场景,兼顾多样性与代表性,并刻意加入了易混淆的短句(如“发某票已开”与“代开发某票”),以增强模型的鲁棒性。

2.模型训练

点击 训练模型 完成模型的训练。
012.jpg

3.模型校验与效果评估

型训练完成后,进入校验阶段。我们输入多组未参与训练的测试文本,观察输出结果及置信度。

输入测试数据文本,即可实时看到输出结果。如输入:,输出:垃圾邮件72%,正常邮件28% 。

013.jpg
继续输入已开,即这时输入的文本为票已开,输出:垃圾邮件0%,正常邮件100% 。

典型测试用例:

Snipaste_2026-04-24_12-33-20.jpg

经过30组随机测试,模型准确率达到96.7%。整体表现稳定可靠,满足实际使用需求。

校验完成后,点击“导出模型”将训练好的模型文件保存到本地,供后续推理使用。

017.jpg

我们逐条测试、反复对比,观察 AI 判断是否准确、置信度是否合理。经过多组样本测试,模型表现稳定,判断结果与预期高度一致,证明训练效果可靠。这一步就像老师对学生进行单元测试,确保 AI 真正掌握了技能,而不是死记硬背。
4.实时结果推送(SIOT + Web展示)

为实现垃圾分类结果的实时监控和远程查看,我们利用Mind+内置的SIOT(物联网消息队列)功能,将推理结果推送到Web页面。

配置步骤:

(1)点击 红点 ,修改或者记录弹窗中的主题名称。
019.jpg

(2)打开SIOT v2的Web管理页面,添加相同的主题。

020.jpg
继续添加主题

022.jpg

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

023.jpg

(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>

 

Snipaste_2026-04-18_20-41-36.jpg

5. 实时模式下的模型推理(Mind+图形化编程)

在Mind+的“实时模式”下,我们可以通过拖拽积木块快速调用训练好的模型进行推理,无需编写Python代码。

程序设计框架流程

image.png

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

a85eaca04eaa484d02fcdb2055b7ea09.png

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

screenshot-分类分类实时模式推理-中文垃圾邮件-1776747235601.png

具体积木代码(图形化描述):

事件:当绿旗被点击

扩展库:调用初始化文本分类积木

模型加载:使用“垃圾邮件识别模型”加载文本分类模型

输入处理:询问“请输入邮件内容:”并等待

推理执行:对(回答)进行一次文本分类推理

结果输出

说“分类结果:”+(推理结果的类别)

说“垃圾邮件置信度:”+(文本分类ID0的置信度)

说“正常邮件置信度:”+(文本分类ID1的置信度)

循环:重复执行步骤4~7

通过以上图形化编程,我们实现了一个交互式垃圾邮件分类器,用户可在Mind+舞台区直接输入文本并立即得到反馈。

 

四、项目总结

本项目基于DFRobot Mind+ V2.0平台,以纯软件方式完整实现了文本分类任务,成功构建并部署了垃圾邮件识别模型。不仅解决了日常生活中信息杂乱的小困扰,更亲身体验了AI从“训练”到“应用”的全过程,充满实践意义与成就感。

项目亮点

低门槛、高实用性:无需复杂代码编写,无需专业硬件设备,依托Mind+可视化的AI模型训练与推理功能,通过“数据准备—模型训练—校验—部署”四步流程,快速完成完整文本分类项目,极大降低了人工智能入门的技术门槛。

真实场景驱动:所有训练数据均来源于生活实际,模型直接服务于日常信息过滤,体现了“人工智能源于生活、服务生活”的价值。

可扩展的实时推送:结合SIOT和Web页面,实现了分类结果的远程监控,为后续集成到邮件客户端或消息过滤器提供了技术原型。

图形化与代码双模式:既适合初学者拖拽积木学习AI原理,也可导出Python代码供进阶开发者二次开发。

训练效果总结

Snipaste_2026-04-24_12-33-48.jpg

反思与展望

改进方向:目前仅支持短文本单句分类,未来可增加长文本分段处理、支持更多类别(如社交、促销、重要工作邮件等),并引入在线增量学习能力。

应用扩展:可将模型部署到行空板等边缘设备(创意智造组),结合硬件实现当检测到垃圾邮件时自动触发报警灯或物理删除按钮。

通过本次项目,我们不仅理解了文本分类的核心概念与实现原理,更深刻体会到人工智能技术的平易近人与实用价值——用技术解决小问题,用智能提升生活品质。这正是AI学习与实践最动人的意义。

评论

user-avatar