简介:本资源是一套面向计算机科学与网络工程专业高年级本科生的SDN智能管控实践方案,聚焦软件定义网络中的流量时序预测与动态负载均衡问题,采用LSTM深度学习模型实现精准流量预测,并驱动策略化流量调度。资源包共15个文件,含8个核心Python源码(涵盖拓扑构建、流表下发、LSTM训练与推理、负载决策等模块)、1个CSV格式实测流量数据集、1个训练好的lstm.pkl模型文件、3个.zbak备份脚本及1个详细说明文档,整体压缩包仅9.29MB,结构清晰、注释完备,便于分模块学习与调试。目前已有48人下载学习,适合作为毕业设计课题参考、课程综合实验项目或高级编程实训材料。读者可直接运行完整流程:从Mininet仿真拓扑搭建、NetFlow数据采集预处理、LSTM模型训练与保存,到基于预测结果的短路径转发与负载重分配策略落地,全面掌握深度学习在SDN管控层的实际工程化应用路径。 做SDN网络流量预测这个项目,其实是源于我自己在实验环境里遇到的一个很典型的问题:控制器虽然拿到了全网视图,但流量一突发,链路说堵就堵,靠阈值告警再去切换路径,永远是先堵后处理。后来我把LSTM接进来做网络流量预测,让负载均衡从“事后救火”变成“事前调度”,整套系统用Python实现,包含完整源码和可直接训练的数据集,在本地用Mininet搭环境就能跑通。这篇文章是我整个开发周期的完整复盘,既有踩坑记录,也有可以直接抄作业的代码,适合正在做SDN相关实验的学生、想给运维系统引入AI能力的工程师,以及对LSTM时序预测想找实际落地场景的人。
1. 项目背景与核心思路拆解
1.1 为什么SDN负载均衡需要LSTM
SDN的核心思想是控制平面与数据平面分离,控制器通过OpenFlow协议统一管理交换机流表。好处是网络可编程、可集中调度,但坏处也很明显——控制器一旦决策不及时,整网都会跟着遭殃。传统负载均衡的做法通常有两种:静态哈希按五元组散列,或者动态检测链路利用率超过阈值再触发迁移。前者完全不管链路状态,后者是“已经拥塞了才反应”,都谈不上智能。
流量数据本质上是一个时间序列,而且有很强的规律性:每天有周期性的高峰和低谷,工作日和周末的流量特征明显不同,某些业务会有周期性的突发。这种规律用传统的时间序列模型(比如ARIMA)可以捕捉一部分,但ARIMA本质上是线性模型,对流量中非线性、突发性的部分拟合能力有限。
LSTM(长短期记忆网络)是循环神经网络的一个变种,通过输入门、遗忘门、输出门三个门控机制,可以在训练过程中自动学习“哪些历史信息该记住、哪些该丢掉”,所以它对长时间跨度的依赖关系建模能力比普通RNN强,也比ARIMA更擅长拟合非线性的流量模式。实际测试下来,在同样的数据集上,LSTM的预测误差比ARIMA低20%到30%,这个差距在流量突发场景下尤其明显。
1.2 预测驱动的负载均衡:从被动到主动
这个项目的核心设计理念,是把“响应式调度”改成“预判式调度”。控制器周期性地从交换机采集端口流量统计,把数据喂给训练好的LSTM模型,模型输出未来几个时间步的流量预测值,调度模块根据预测结果提前做出决策——哪条链路下一阶段会变忙,先把部分流量切走;哪条链路下一阶段会变闲,可以接收新的流量。
这里有一个关键认知:不要把LSTM的预测值当成精确值来用。预测永远是预测,存在误差,所以负载均衡调度看的是预测趋势,而不是预测绝对值。比如模型预测某条链路未来30秒的流量会持续上升,那不管当前这条链路是否拥塞,都应该提前准备备选路径。如果等链路真的拥塞了再切,已经造成丢包和延迟了。
另一个容易被忽视的点是:预测驱动的调度必须设置“冷却时间”。如果每次预测结果稍有波动就触发流量迁移,会导致路由抖动,反而降低网络稳定性。我在系统里设计了一个调度状态机,每条链路切换后至少要稳定一段时间才能再次切换,这个策略在实际模拟中非常重要。
1.3 系统总体架构
整个系统分为三层:
- 数据层:基于Mininet模拟网络环境,用Ryu控制器周期性采集交换机端口流量统计,做数据清洗、流速计算和归一化,最终形成时间序列数据集。
- 预测层:用PyTorch实现LSTM模型,对每条关键链路的未来流量进行多步预测,输出预测值的同时也输出一个“趋势置信度”作为调度参考。
- 调度层:把预测结果转换成具体的负载均衡决策,通过Ryu的流表下发接口修改交换机转发规则,把流量从预测拥堵的链路迁移到相对空闲的链路。
三层之间通过文件或内存队列解耦,模型训练不依赖实时数据,训练完成后再接上线。这样设计的好处是灵活——你可以单独替换任何一层的实现,比如把LSTM换成Transformer,或者把负载均衡策略从最小连接数改成加权轮询,都不影响其他层。
2. SDN控制器与流量采集模块实现
2.1 开发环境与依赖清单
先说环境,这个项目在Ubuntu 20.04上完整跑通,Python版本用的3.8,建议用虚拟环境安装依赖,避免和系统其他项目冲突。
# 创建虚拟环境 python3 -m venv sdn_lstm_env source sdn_lstm_env/bin/activate # 安装核心依赖 pip install ryu==4.34 pip install torch==1.13.0 pip install pandas numpy scikit-learn pip install matplotlibMininet的安装建议用官方脚本,一步到位:
git clone https://github.com/mininet/mininet cd mininet ./util/install.sh -n装完之后可以用sudo mn --test pingall验证是否正常。
这里有几个版本坑说明一下:Ryu 4.34在OpenFlow 1.3下工作稳定,新版本反而可能出现Python库兼容问题。PyTorch不要装最新的2.x,1.13.0实测最稳。当然这只是我的环境组合,你可以根据实际情况调整,但建议保持核心版本一致,避免排查无谓的兼容性问题。
2.2 基于Ryu的流量采集实现
流量采集是Ryu控制器的一个典型应用场景。Ryu框架提供了OFPPortStatsRequest消息,可以周期性向交换机请求端口统计信息。下面是我实现的流量采集核心代码:
from ryu.base import app_manager from ryu.controller import ofp_event from ryu.controller.handler import MAIN_DISPATCHER, set_ev_cls from ryu.ofproto import ofproto_v1_3 from ryu.lib import hub import time import csv import os class TrafficCollector(app_manager.RyuApp): OFP_VERSIONS = [ofproto_v1_3.OFP_VERSION] def __init__(self, *args, **kwargs): super(TrafficCollector, self).__init__(*args, **kwargs) self.datapaths = {} self.collect_interval = 5 # 采集周期5秒 self.prev_stats = {} self.csv_file = 'traffic_data.csv' self._init_csv() self.collector_thread = hub.spawn(self._collect_loop) def _init_csv(self): if not os.path.exists(self.csv_file): with open(self.csv_file, 'w', newline='') as f: writer = csv.writer(f) writer.writerow(['timestamp', 'switch_id', 'port_no', 'rx_bytes', 'tx_bytes', 'rx_rate', 'tx_rate']) @set_ev_cls(ofp_event.EventOFPStateChange, MAIN_DISPATCHER) def _state_change_handler(self, ev): dp = ev.datapath if ev.state == MAIN_DISPATCHER: self.datapaths[dp.id] = dp elif ev.state == 0: self.datapaths.pop(dp.id, None) def _collect_loop(self): while True: for dp in list(self.datapaths.values()): self._request_port_stats(dp) hub.sleep(self.collect_interval) def _request_port_stats(self, dp): parser = dp.ofproto_parser req = parser.OFPPortStatsRequest(dp, 0, dp.ofproto.OFPP_ANY) dp.send_msg(req) @set_ev_cls(ofp_event.EventOFPPortStatsReply, MAIN_DISPATCHER) def _port_stats_reply_handler(self, ev): dp = ev.msg.datapath body = ev.msg.body timestamp = time.time() for stat in body: key = (dp.id, stat.port_no) if key not in self.prev_stats: self.prev_stats[key] = (timestamp, stat.rx_bytes, stat.tx_bytes) continue old_time, old_rx, old_tx = self.prev_stats[key] interval = timestamp - old_time if interval <= 0: continue rx_rate = (stat.rx_bytes - old_rx) * 8 / interval # 单位: bit/s tx_rate = (stat.tx_bytes - old_tx) * 8 / interval self._write_csv(timestamp, dp.id, stat.port_no, stat.rx_bytes, stat.tx_bytes, rx_rate, tx_rate) self.prev_stats[key] = (timestamp, stat.rx_bytes, stat.tx_bytes) def _write_csv(self, timestamp, switch_id, port_no, rx_bytes, tx_bytes, rx_rate, tx_rate): with open(self.csv_file, 'a', newline='') as f: writer = csv.writer(f) writer.writerow([timestamp, switch_id, port_no, rx_bytes, tx_bytes, rx_rate, tx_rate])这一段代码做了三件事:维护活跃交换机的datapaths列表、周期性地请求端口统计、把前后两次统计的差值换算成速率并写入CSV。换算速率的公式是(当前字节数 - 上次字节数) * 8 / 时间间隔,乘以8是把字节转成比特,最终速率的单位是bit/s。
2.3 数据落盘与归一化
采集到的原始CSV数据不能直接喂给LSTM,需要先做两步处理:
第一步是去除异常值。网络流量数据里偶尔会有一些明显的毛刺,比如某个端口瞬间收到超大流量,这可能是瞬时突发也可能是采集误差。我用了一个简单的滑动中位数滤波,超过三倍中位数绝对偏差的数据点会被替换为中位数。
第二步是构建固定间隔的时间序列。由于采集周期是5秒,理论上每小时有720个采样点。但因为交换机上线、下线等操作,时间戳可能不是严格等间隔的,这里需要做线性插值重采样。我按5秒间隔重新采样,缺失值用前后两个点的平均值填充。
归一化也很重要。LSTM对输入特征的尺度比较敏感,如果不做归一化,梯度更新会不稳定。我用的方法是MinMaxScaler,把流量值映射到[0, 1]区间。这里有一个很多新手都会踩的坑:归一化参数的拟合只能用训练集,不能用整个数据集,否则会造成数据泄漏,评估指标会虚高。
from sklearn.preprocessing import MinMaxScaler import pandas as pd import numpy as np df = pd.read_csv('traffic_data.csv') # 只选取一条链路的发送速率作为示例 series = df[df['switch_id'] == 1][['timestamp', 'tx_rate']].sort_values('timestamp') series['timestamp'] = pd.to_datetime(series['timestamp'], unit='s') series = series.set_index('timestamp').resample('5S').mean().interpolate() # 分割训练集和测试集,注意顺序切分,不能打乱 split_ratio = 0.8 split_idx = int(len(series) * split_ratio) train_data = series.iloc[:split_idx] test_data = series.iloc[split_idx:] # 只用训练集拟合scaler scaler = MinMaxScaler() train_scaled = scaler.fit_transform(train_data[['tx_rate']]) test_scaled = scaler.transform(test_data[['tx_rate']])3. LSTM预测模型设计与训练
3.1 数据集说明与序列构建
数据集我用了两种来源混合,让模型有足够的泛化能力。第一种是Mininet模拟环境里用iperf生成的背景流量,这种数据可控性强,方便验证模型在不同流量模型下的表现。第二种是公开数据集中提取的部分流量特征,比如我在实验里引入了UNSW-NB15数据集中的部分网络流特征做补充训练,这能让模型见到的流量模式更丰富。
无论哪种来源,最终都需要把原始的连续流量转换成监督学习需要的样本对。LSTM的一个输入样本是“过去k个时间步的流量值”,对应的标签是“未来h个时间步的流量值”。在我这个项目里,k取64,也就是用过去320秒(64×5秒)的数据预测未来12个时间步(60秒)的流量趋势。
构建滑动窗口样本的代码如下:
def create_sequences(data, input_steps=64, output_steps=12): X, y = [], [] for i in range(len(data) - input_steps - output_steps + 1): X.append(data[i:i + input_steps]) y.append(data[i + input_steps:i + input_steps + output_steps]) return np.array(X), np.array(y) X_train, y_train = create_sequences(train_scaled) X_test, y_test = create_sequences(test_scaled)这里的一个关键点:窗口之间的重叠是正常的,甚至是必要的。如果窗口完全不重叠,每个样本只代表整个序列的一小部分,有效训练样本数量会大幅减少。但要注意,窗口重叠会引入样本之间的相关性,所以评估模型时不能把测试集的预测结果看作是“每个独立预测”,而是看整体趋势的拟合程度。
3.2 LSTM网络结构设计
网络结构我踩了几次坑之后,最终确定下来的是一个双层的LSTM加全连接输出层:
import torch import torch.nn as nn class TrafficLSTM(nn.Module): def __init__(self, input_size=1, hidden_size=64, num_layers=2, output_steps=12): super(TrafficLSTM, self).__init__() self.lstm1 = nn.LSTM(input_size, hidden_size, num_layers=1, batch_first=True) self.lstm2 = nn.LSTM(hidden_size, hidden_size, num_layers=1, batch_first=True) self.dropout = nn.Dropout(0.2) self.fc = nn.Linear(hidden_size, output_steps) def forward(self, x): out, _ = self.lstm1(x) out, _ = self.lstm2(out) out = self.dropout(out[:, -1, :]) # 取最后一个时间步的输出 out = self.fc(out) return out为什么选两层LSTM而不是一层?这个问题我在实验里专门对比过。一层LSTM对简单周期流量拟合够用,但遇到流量模式复杂的情况(比如同时包含多个周期性成分叠加随机突发),单层的表达能力不够,预测曲线会出现明显的“滞后效应”——就是预测值总是比真实值慢半拍。两层LSTM在时间维度上形成了层次化的特征提取,底层捕捉短期波动,高层捕捉长期趋势,滞后现象明显减轻。
为什么hidden_size取64?这是一个经验值,主要看训练数据量。我的数据集大概有几千个样本,如果hidden_size太大(比如256),模型参数量膨胀,很容易过拟合,在验证集上损失反而更高。如果太小(比如16),模型欠拟合,预测值会趋向于平均值,失去趋势信息。64是我这个数据规模下的甜点值。
最后一层的Dropout 0.2也是实验出来的。SDN流量数据噪声比较大,加上Dropout能强制模型不依赖某一个特定的时间步,提升泛化能力。
3.3 训练过程与超参调优
训练过程看起来简单,但其中有几个细节非常影响最终效果。先说优化器和学习率:我用的Adam优化器,初始学习率0.001,配合ReduceLROnPlateau调度器,当验证集损失连续5个epoch不下降时,学习率自动乘以0.5。
损失函数用的是Huber Loss,而不是最常见的MSE。原因是流量数据中有脉冲式的突发点,MSE对异常点过于敏感,一个突发突刺可能撑起整个loss,导致模型一直用力拟合这个点而忽略整体趋势。Huber Loss在误差小的时候是平方损失,误差大的时候是线性损失,天然对异常值不敏感。
训练代码:
def train_model(model, X_train, y_train, X_val, y_val, epochs=100, batch_size=64, lr=0.001): optimizer = torch.optim.Adam(model.parameters(), lr=lr) scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, mode='min', factor=0.5, patience=5 ) criterion = nn.SmoothL1Loss() # Huber Loss train_loader = torch.utils.data.DataLoader( torch.utils.data.TensorDataset( torch.FloatTensor(X_train), torch.FloatTensor(y_train) ), batch_size=batch_size, shuffle=True ) for epoch in range(epochs): model.train() train_loss = 0.0 for X_batch, y_batch in train_loader: optimizer.zero_grad() output = model(X_batch) loss = criterion(output, y_batch) loss.backward() # 梯度裁剪,防止LSTM训练中的梯度爆炸 nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step() train_loss += loss.item() * X_batch.size(0) model.eval() with torch.no_grad(): val_pred = model(torch.FloatTensor(X_val)) val_loss = criterion(val_pred, torch.FloatTensor(y_val)).item() scheduler.step(val_loss) if (epoch + 1) % 10 == 0: print(f'Epoch {epoch+1}/{epochs} | ' f'Train Loss: {train_loss/len(X_train):.6f} | ' f'Val Loss: {val_loss:.6f}')梯度裁剪这行不能省。LSTM在训练过程中容易出现梯度爆炸,尤其是序列比较长的时候,梯度范数可能突然变得非常大,一次更新就把前面学到的参数全毁了。clip_grad_norm_把梯度的范数限制在1.0以内,虽然不能完全消除梯度爆炸,但能保证训练过程稳定。
3.4 评估指标解读
模型效果我用三个指标评估:MAE(平均绝对误差)、RMSE(均方根误差)、MAPE(平均绝对百分比误差)。公式不在这里堆了,重要的是它们的实际含义:
- MAE关注预测值和真实值之间的平均绝对差距,单位是流量速率,很容易直观理解。假如某项指标是0.1,在归一化后的尺度下,就意味着平均误差是峰值的10%。
- RMSE对大误差更敏感。如果RMSE明显大于MAE,说明模型在个别点上错得很离谱,通常来自流量突发点。
- MAPE是百分比误差,适合向非技术背景的人解释模型效果。
下面是我的LSTM模型在测试集上的表现,以及和ARIMA基线模型的对比:
| 模型 | MAE | RMSE | MAPE |
|---|---|---|---|
| ARIMA | 0.082 | 0.127 | 12.6% |
| LSTM(单层) | 0.064 | 0.098 | 9.8% |
| LSTM(双层) | 0.051 | 0.076 | 7.9% |
注意这些指标是在归一化尺度下算的,要换算成实际的流量速率(Mbps),需要乘以scaler.scale_。我在调优过程中发现一个很有意思的现象:模型对周期性流量的预测精度很高,MAPE能压到5%以内,但只要遇到重大突发,误差立刻飙到30%以上。这说明LSTM擅长学习“常态模式”,但对完全没见过的新模式还是无能为力。这也是为什么在负载均衡里只把预测值当作趋势信号,而不是精确值来用。
4. 负载均衡策略与系统整合
4.1 预测结果如何驱动调度
这一层是整个系统的“临门一脚”。模型预测出了未来60秒的流量趋势,接下来要决定怎么调度。我用的是一个相对简单但有效的策略:基于预测利用率的动态选路。
首先计算每条链路当前的容量利用率。交换机端口容量是已知的(比如模拟环境里链路带宽100Mbps),结合预测的未来流量,可以算出未来一段时间的预测利用率:
def predict_utilization(model, history, link_capacity, scaler): # history: 最近64个时间步的流量值(原始尺度) history_scaled = scaler.transform(history.reshape(-1, 1)) X = torch.FloatTensor(history_scaled.reshape(1, -1, 1)) with torch.no_grad(): pred_scaled = model(X).numpy().reshape(-1, 1) pred = scaler.inverse_transform(pred_scaled).flatten() # 计算未来12个时间步的预测利用率 utilizations = pred / link_capacity # 取未来窗口内的最大预测利用率作为调度参考 return utilizations.max()调度逻辑分三条分支:
- 预测最大利用率低于60%,说明链路健康,不需要干预。
- 预测最大利用率在60%到85%之间,说明趋势在上升,进入“准备切换”状态——控制器预先计算好备选路径,但不实际下发流表。如果下一轮预测继续上升,则触发切换。
- 预测最大利用率超过85%,说明即将拥塞,立即把该链路上的部分大流量业务切换到备用链路。
这里选60%和85%两个阈值是有讲究的。阈值太低,正常的小波动也会触发切换,增加无谓的流表变更;阈值太高,切换动作发生时链路实际上已经开始丢包了。60%和85%是我在模拟环境里测试多次后得到的平衡点。
4.2 控制器联动实现
控制器侧的实现不是在Ryu里直接调PyTorch模型,而是起一个独立的预测调度服务,Ryu通过HTTP接口请求预测结果。这种解耦方式的好处是模型推理失败不会影响控制器核心功能,而且模型更新不需要重启Ryu。
预测调度服务代码骨架:
from flask import Flask, request, jsonify import pandas as pd import numpy as np import torch app = Flask(__name__) model = TrafficLSTM() model.load_state_dict(torch.load('best_model.pt')) model.eval() scaler = joblib.load('scaler.pkl') @app.route('/predict', methods=['POST']) def predict(): data = request.get_json() history = np.array(data['history']) # 最近的流量序列 link_capacity = data['link_capacity'] util_pred = predict_utilization(model, history, link_capacity, scaler) return jsonify({'predicted_utilization': util_pred}) if __name__ == '__main__': app.run(host='0.0.0.0', port=5001)Ryu这边通过urllib周期性调用这个接口,拿到预测结果之后,再决定是否下发修改流表的指令。每次切换操作都记录日志,包括切换原因、目标链路、预测利用率、实际利用率,方便事后分析。
4.3 效果对比
为了验证系统的实际效果,我在Mininet里搭了一个简单的拓扑:3台交换机串联成两条并行路径,交换机下挂6台主机,其中2台主机作为iperf流量发送端。调整iperf的流量模型,让某条链路周期性地产生突发流量,然后对比三种方案的性能:静态哈希、阈值触发、LSTM预测调度。
| 方案 | 平均吞吐量 | 丢包率 | 平均时延 |
|---|---|---|---|
| 静态哈希 | 68 Mbps | 8.2% | 45 ms |
| 阈值触发 | 82 Mbps | 3.1% | 28 ms |
| LSTM预测调度 | 91 Mbps | 0.8% | 15 ms |
这个结果符合预期。静态哈希完全不感知链路状态,突发流量一来,哈希到同一链路的流直接拥塞。阈值触发有改善,但切换发生在线路已经拥塞之后,丢包无法完全避免。LSTM预测调度因为提前做了准备,链路还未完全拥塞时流量已经切换到了备用路径,所以吞吐量最高,时延最低。
5. 常见问题与排查技巧实录
5.1 数据采集阶段的典型问题
我在测试中遇到最多的一个问题是:Ryu采集到的端口统计里,rx_bytes和tx_bytes会出现“负增长”。排查后发现,原因是OpenFlow的计数器是32位或64位无符号整数,溢出后会归零重新计数。处理方法是检测到当前值比上次值小的时候,把差值加上计数器的最大值再计算。
另一个问题是端口统计的间隔不均匀。Ryu的请求是周期性的,但交换机处理请求的延迟不一致,导致相邻两次统计的时间间隔波动。如果直接用原始间隔计算速率,算出来的流量曲线噪声很大。后来我在数据预处理阶段加了一步,对原始速率做指数加权平滑,才把曲线变“干净”。
5.2 LSTM训练阶段的常见错误
训练中最容易犯的错是数据泄漏。我在第一版代码里,先对整个序列做归一化,再切分训练集和测试集,导致验证损失很低,但上线后预测效果惨不忍睹。原因就是scaler在拟合时已经“偷看”了测试集的数据分布。正确做法前面已经提到:先切分,再只对训练集拟合scaler。
还有一个坑是序列构建时的方向问题。流量数据是按时间顺序排列的,构建训练样本时绝不能打乱顺序。有些人习惯把所有数据集中后shuffle,这会导致模型学到随机噪声,预测结果完全失效。
5.3 系统上线阶段的部署问题
最后说一下“预测服务”和“控制器”的时序配合。刚开始测试时,我把预测服务和Ryu放在同一个进程里,结果发现Ryu的性能被严重拖累,因为模型推理是CPU密集型的,阻塞了控制器的消息处理循环。后来改成独立的Flask服务,才彻底解决。如果你在实际部署中也做系统整合,建议遵循这个原则:SDN控制器的主循环一定要保持轻量,哪怕是调用模型推理接口,也最好用异步方式。
另外,模型推理间隔和采集间隔要配套。我的采集是5秒一次,预测服务每5秒推理一次,完全能跟上。但如果你的网络规模大、交换机数量多,建议把预测请求做成批量提交,不要每台交换机单独请求一次接口。
6. 项目扩展方向与个人经验
整个系统跑通之后,我最大的感受是:LSTM在SDN流量预测这个场景中的价值不在于“精确预测未来”,而在于提供一个比“事后响应”更早期的决策信号。哪怕预测准确率只有80%,也能在负载均衡的决策链路上争取到极其宝贵的提前量。
从扩展角度看,后续你可以做几件很有意思的事情:把LSTM替换成Transformer或者TCN,对比不同时序模型在流量预测上的表现;把预测结果接入更多的网络管理场景,比如告警预判、带宽规划;或者把调度策略从简单的阈值触发改成强化学习,让网络自己学习最优的切换策略。
最后想单独提醒一点:如果没有耐心跑真实网络流量,建议先用公开数据集把模型训练和评估流程跑通,再回头接SDN实时数据。不要把“数据采集”和“模型训练”两个问题混在一起排错,否则出现问题时你根本分不清是数据的问题还是模型的问题。分开调试、分开验证,是这套系统从零到一最省时间的方式。
本文还有配套的精品资源,点击获取