ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于深度学习的海上捕鱼方式检测:围网、刺网、拖网分类实战

基于深度学习的海上捕鱼方式检测:围网、刺网、拖网分类实战 简介这份资源面向深度学习与计算机视觉方向的初学者及中级实践者聚焦海上渔民捕鱼方式识别这一具体图像分类任务覆盖围网、刺网和拖网三类作业方式的判别可服务于渔业资源管理与海洋生态保护等应用场景。压缩包共14个文件以10个Python脚本为核心辅以3个Markdown说明文档和1个Shell运行脚本整体约30KB结构紧凑便于快速上手。内容围绕数据预处理、模型构建、训练优化、验证与测试等环节展开涉及卷积神经网络及预训练模型微调思路并包含特征提取、模型训练与评估指标等模块化实现。已有266人学习下载适合希望以完整小项目串联深度学习图像分类流程、理解模型运作机制并积累环保领域AI应用经验的读者参考。1. 海上渔民捕鱼方式检测围网、刺网、拖网到底怎么用深度学习分出来渔船在海上作业监管端最头疼的不是“有没有船”而是“这条船在用什么网具”。围网、刺网、拖网三种作业方式在AIS轨迹、航速、转向半径、停留时长上留下的模式完全不同但光靠人盯着雷达回波和轨迹点去判一条船看半小时都未必敢下结论。基于深度学习的海上渔民捕鱼方式检测本质是把渔船轨迹和作业行为转成可学习的时空特征让模型自动区分围网、刺网和拖网。这套方案适合做渔业监管、海洋执法辅助、渔船行为分析的一线工程师也适合手里已经有AIS或VMS数据、想跑通一个多分类任务的人。数据集和算法实现是这条链路里最实在的两块下面按“数据怎么整、模型怎么搭、坑怎么避”的顺序拆开讲。2. 围网、刺网、拖网的行为差异与数据标注口径2.1 三种网具在轨迹上的可区分特征围网作业的典型动作是“绕圈”——渔船围绕鱼群快速兜一个大圈航速先升后降航向连续变化轨迹闭合度很高。刺网则是“放网—漂流—收网”船在放网阶段低速直线航行随后长时间低速漂移或几乎静止收网时再出现一次短时加速。拖网最像“犁地”船拖着网具以稳定中低速直线航行持续几小时航向变化小航速波动也小。这三种模式在原始AIS点序列里并不是一眼能看出来的。AIS采样间隔从几秒到几分钟不等近海密集、远海稀疏直接拿经纬度点做分类模型很容易学到“位置先验”而不是“行为特征”。所以第一步不是急着上模型而是把轨迹切成固定时长的片段再对每个片段提取运动学统计量。常见做法是以30分钟为一个窗口滑动步长10分钟对每个窗口计算平均航速、航速标准差、航向变化率、转向累积角度、直线度指数、停留点占比。这些特征对三种网具的区分度往往比原始坐标高得多。2.2 数据集的组织方式与标注边界标题里提到“内含数据集”实际拿到的数据大概率是AIS轨迹表加一份标签表。标签表至少要有渔船ID、时间段起止、作业类型三列。这里有个血泪经验很多公开AIS数据只给轨迹不给标签需要自己按港口报告或观察记录去匹配匹配不上的片段宁可丢掉也不要硬标。数据目录我一般这样组织dataset/ ├── raw/ │ ├── ais_tracks_2023.csv │ └── labels.csv ├── processed/ │ ├── train/ │ ├── val/ │ └── test/ └── feature_config.jsonraw/ais_tracks_2023.csv至少包含mmsi, timestamp, lat, lon, sog, cog六列。labels.csv包含mmsi, start_time, end_time, labellabel取值限定为seine、gillnet、trawl三类。processed/下按7:1.5:1.5切分避免同一艘船同时出现在训练和验证集里否则准确率会虚高。提示切分时按MMSI做分组切分不要按时间随机切。同一艘船的同一天轨迹如果被拆到训练和测试两边模型等于开卷考试。2.3 标签不平衡的处理顺序围网、刺网、拖网的真实分布通常不是1:1:1。拖网作业时长长、样本多围网作业窗口短、样本少。直接训练会让模型偏向拖网。处理顺序建议是先统计各类样本量如果最大类与最小类差距超过5倍先对少数类做窗口重叠采样再考虑类别权重。不要一上来就上SMOTE轨迹特征做合成插值容易造出物理上不存在的运动模式。import pandas as pd labels pd.read_csv(dataset/raw/labels.csv) counts labels[label].value_counts() print(counts) # 按类别权重训练而不是盲目过采样 class_weight { cls: len(labels) / (len(counts) * cnt) for cls, cnt in counts.items() } print(class_weight)这段代码先看分布再算权重。class_weight直接传给后续模型的损失函数让少数类样本的梯度贡献被放大。参数上如果围网样本只有拖网的五分之一围网权重就会接近5训练时模型会更关注围网窗口。注意权重不要超过10否则容易过拟合少数类。3. 从AIS轨迹到模型输入特征工程与序列样本构造3.1 轨迹清洗的三个必做步骤原始AIS数据不能直接喂模型。第一步去重同一秒同一MMSI可能有多条记录第二步去跳点相邻两点距离超过物理航速上限的直接删第三步补缺失对短于窗口长度的轨迹片段做丢弃而不是插值。import numpy as np import pandas as pd def clean_track(df, max_speed25.0): df df.sort_values(timestamp).drop_duplicates( subset[mmsi, timestamp] ) df[dt] df.groupby(mmsi)[timestamp].diff().dt.total_seconds() df[dist] df.groupby(mmsi).apply( lambda g: np.sqrt( (g[lat].diff() * 111.0) ** 2 (g[lon].diff() * 111.0 * np.cos(np.radians(g[lat]))) ** 2 ) ).reset_index(level0, dropTrue) df[speed_calc] df[dist] / (df[dt] / 3600.0) df df[(df[speed_calc].isna()) | (df[speed_calc] max_speed)] return df.drop(columns[dt, dist, speed_calc])max_speed25节是渔船物理上限超过这个值的相邻点基本是AIS跳变。111.0是纬度一度对应的公里数经度方向要乘cos(lat)。清洗后如果某艘船剩余点数少于窗口所需点数整艘船丢弃不要用均值填充填充出来的轨迹会让模型学到假模式。3.2 滑动窗口与运动学特征计算清洗完的轨迹按MMSI分组每组按时间排序然后滑窗。每个窗口输出一个特征向量维度建议控制在20以内太多特征在几千条样本上容易过拟合。def extract_features(g, window_min30, step_min10): g g.set_index(timestamp) feats [] for start in pd.date_range( g.index.min(), g.index.max(), freqf{step_min}min ): end start pd.Timedelta(minuteswindow_min) seg g.loc[start:end] if len(seg) 5: continue sog seg[sog].values cog np.radians(seg[cog].values) d_cog np.diff(cog) d_cog (d_cog np.pi) % (2 * np.pi) - np.pi feats.append({ mmsi: g[mmsi].iloc[0], start: start, sog_mean: sog.mean(), sog_std: sog.std(), cog_change_mean: np.abs(d_cog).mean(), cog_change_std: np.abs(d_cog).std(), turn_cumsum: np.abs(d_cog).sum(), straightness: np.abs(sog.mean()) / (np.abs(d_cog).sum() 1e-6), stop_ratio: (sog 1.0).mean(), }) return pd.DataFrame(feats)window_min30是经验值围网绕圈通常20到40分钟刺网放网后漂流可能超过1小时拖网更久。窗口太短抓不到完整动作太长会把不同作业段混在一起。step_min10保证样本量足够。straightness用平均航速除以累计转向角拖网和刺网放网段数值高围网数值低。stop_ratio对刺网漂流段特别敏感。3.3 序列模型输入格式与标签对齐如果不用统计特征直接上LSTM或1D-CNN输入就是每个窗口内的(lat, lon, sog, cog)序列长度统一到60个点不足补零并加mask。标签对齐时窗口的标签取窗口中心时刻所属的作业段标签。如果窗口跨越两个标签段丢弃该窗口。def make_sequence(seg, max_len60): arr seg[[lat, lon, sog, cog]].values.astype(float32) if len(arr) max_len: idx np.linspace(0, len(arr) - 1, max_len).astype(int) return arr[idx] pad np.zeros((max_len - len(arr), 4), dtypefloat32) return np.vstack([arr, pad])max_len60对应30分钟窗口内平均30秒一个点。如果AIS采样稀疏可以降到30。补零后要额外返回一个mask向量告诉模型哪些位置是真实点。标签用整数编码seine0, gillnet1, trawl2。4. 模型选型与训练1D-CNN、LSTM还是树模型4.1 小样本下树模型往往比深度模型稳如果标注窗口只有几千条XGBoost或LightGBM在统计特征上的表现经常超过LSTM。原因很简单统计特征已经压缩了时序信息树模型对特征尺度和噪声更鲁棒。我一般先跑LightGBM做baseline准确率能到0.85以上再考虑上深度模型。import lightgbm as lgb from sklearn.model_selection import GroupKFold X feat_df.drop(columns[mmsi, start, label]) y feat_df[label] groups feat_df[mmsi] gkf GroupKFold(n_splits5) for train_idx, val_idx in gkf.split(X, y, groups): model lgb.LGBMClassifier( n_estimators300, learning_rate0.05, num_leaves31, class_weightclass_weight, ) model.fit(X.iloc[train_idx], y.iloc[train_idx]) score model.score(X.iloc[val_idx], y.iloc[val_idx]) print(score)GroupKFold按MMSI分组保证同一艘船不跨折。num_leaves31是默认值样本少时降到15。class_weight用前面算的字典。如果验证准确率在0.8以下先回去检查特征和标签不要急着换模型。4.2 1D-CNN加注意力层的序列模型当样本量超过两万条窗口或者统计特征区分度不够时上1D-CNN。结构建议三层卷积每层后接BatchNorm和ReLU最后接全局平均池化和全连接。注意力层可以加在卷积之后让模型自己挑重要时间步。import torch import torch.nn as nn class FishingNet(nn.Module): def __init__(self, in_ch4, n_cls3): super().__init__() self.conv nn.Sequential( nn.Conv1d(in_ch, 32, 5, padding2), nn.BatchNorm1d(32), nn.ReLU(), nn.Conv1d(32, 64, 3, padding1), nn.BatchNorm1d(64), nn.ReLU(), nn.Conv1d(64, 64, 3, padding1), nn.BatchNorm1d(64), nn.ReLU(), ) self.attn nn.Linear(64, 1) self.fc nn.Linear(64, n_cls) def forward(self, x, maskNone): x x.permute(0, 2, 1) x self.conv(x) x x.permute(0, 2, 1) w torch.softmax(self.attn(x), dim1) if mask is not None: w w * mask.unsqueeze(-1) w w / (w.sum(dim1, keepdimTrue) 1e-6) x (x * w).sum(dim1) return self.fc(x)in_ch4对应经纬度、航速、航向。卷积核从5降到3是为了在短序列上保留更多局部变化。注意力权重在mask为0的位置被压掉避免补零位置影响池化。训练时用交叉熵损失优化器选Adam学习率1e-3batch size 64。如果验证集损失震荡把学习率降到3e-4。4.3 训练轮次与早停参数深度模型在小数据集上通常10到20轮就收敛。早停耐心值设5监控验证集F1而不是准确率因为类别不平衡时准确率会骗人。from torch.optim import Adam from sklearn.metrics import f1_score optimizer Adam(model.parameters(), lr1e-3) criterion nn.CrossEntropyLoss(weighttorch.tensor(list(class_weight.values()))) best_f1, patience 0.0, 0 for epoch in range(50): model.train() # 训练循环省略 model.eval() preds, trues [], [] # 验证循环省略 f1 f1_score(trues, preds, averagemacro) if f1 best_f1: best_f1, patience f1, 0 torch.save(model.state_dict(), best.pt) else: patience 1 if patience 5: breakaveragemacro让三个类别等权围网F1低会直接拉低总分逼模型关注少数类。weight用类别权重张量。保存best.pt而不是最后一轮避免过拟合。5. 避坑与排查海上捕鱼方式检测里最容易翻车的五件事5.1 现象验证集准确率0.95上线后一塌糊涂原因按时间随机切分同一艘船同一航次的窗口同时进了训练和验证。模型记住了船而不是行为。解决用GroupKFold按MMSI切或者按航次ID切。切完后检查训练集和验证集的MMSI交集必须为空。5.2 现象围网召回率极低几乎全被预测成拖网原因围网样本少且围网绕圈窗口和拖网低速直线窗口在平均航速上重叠。模型偷懒选了多数类。解决先加stop_ratio和turn_cumsum两个特征再把围网类别权重调高。如果还不行对围网窗口做重叠采样把样本量拉到拖网的三分之一以上。5.3 现象AIS跳点导致特征里出现航速几百节的异常值原因清洗时只按sog字段过滤但有些AIS记录的sog本身是错的需要用经纬度反算速度。解决同时用sog和反算速度做双重过滤两者取较小值。反算速度超过25节的点直接删不要插值。5.4 现象模型在近海数据上表现好远海数据上崩了原因近海AIS采样密远海采样稀窗口内点数差异大。模型对采样率敏感。解决训练时对窗口内点数做随机下采样让模型见过稀疏情况。或者在特征里加一个point_count让模型知道当前窗口的采样密度。5.5 现象刺网和围网在放网阶段混淆严重原因刺网放网和围网初始绕圈都表现为低速转向统计特征区分度不够。解决引入序列模型让1D-CNN去学转向的时序形状。围网转向是连续同向刺网放网转向是来回摆动。统计量看不出来卷积核能抓到。6. 把模型推到能用的程度阈值调优与跨海域验证模型训练完只是半成品。海上捕鱼方式检测的落地瓶颈不在训练集准确率而在跨海域、跨季节的稳定性。我一般做两件事一是按类别调分类阈值二是留一个完全没参与训练的海域做最终验证。阈值调优不用改模型结构直接在验证集上跑预测概率对每个类别扫一遍阈值选宏F1最高的组合。围网通常需要把阈值从0.5降到0.35左右牺牲一点精确率换召回。刺网和拖网可以维持在0.5附近。import numpy as np from sklearn.metrics import f1_score probs model.predict_proba(X_val) best_thresh {} for i, cls in enumerate([seine, gillnet, trawl]): best_f1, best_t 0, 0.5 for t in np.arange(0.2, 0.8, 0.05): pred (probs[:, i] t).astype(int) true (y_val i).astype(int) f1 f1_score(true, pred) if f1 best_f1: best_f1, best_t f1, t best_thresh[cls] round(best_t, 2) print(best_thresh)这段代码对每个类别单独扫阈值。np.arange(0.2, 0.8, 0.05)覆盖了常见调整范围。输出类似{seine: 0.35, gillnet: 0.5, trawl: 0.55}。注意阈值要在验证集上定定完锁死测试集只跑一次。跨海域验证更关键。如果训练数据来自东海就拿南海或黄海的AIS片段做一次零样本测试。准确率掉10个点以内算正常掉20个点以上说明模型学到了海域相关的伪特征需要回去检查特征里有没有隐含位置信息的字段。经纬度本身不要直接进树模型进序列模型时也要做归一化。我自己的习惯是每换一个海域先抽200条窗口人工标一遍看模型错在哪。如果错的全是刺网和围网混淆就补这两个类的样本如果错的是拖网被分成刺网检查航速特征是不是被异常值污染了。这套流程跑下来模型从实验室到监管端可用通常还需要两到三轮迭代。希望帮到你。本文还有配套的精品资源点击获取
返回列表