news 2026/10/5 9:59:15

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于深度学习的海上捕鱼方式检测:围网、刺网、拖网分类实战

简介:这份资源面向深度学习与计算机视觉方向的初学者及中级实践者,聚焦海上渔民捕鱼方式识别这一具体图像分类任务,覆盖围网、刺网和拖网三类作业方式的判别,可服务于渔业资源管理与海洋生态保护等应用场景。压缩包共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.json

raw/ais_tracks_2023.csv至少包含mmsi, timestamp, lat, lon, sog, cog六列。labels.csv包含mmsi, start_time, end_time, label,label取值限定为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_speed=25.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(level=0, drop=True) 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_speed=25节是渔船物理上限,超过这个值的相邻点基本是AIS跳变。111.0是纬度一度对应的公里数,经度方向要乘cos(lat)。清洗后如果某艘船剩余点数少于窗口所需点数,整艘船丢弃,不要用均值填充,填充出来的轨迹会让模型学到假模式。

3.2 滑动窗口与运动学特征计算

清洗完的轨迹按MMSI分组,每组按时间排序,然后滑窗。每个窗口输出一个特征向量,维度建议控制在20以内,太多特征在几千条样本上容易过拟合。

def extract_features(g, window_min=30, step_min=10): g = g.set_index("timestamp") feats = [] for start in pd.date_range( g.index.min(), g.index.max(), freq=f"{step_min}min" ): end = start + pd.Timedelta(minutes=window_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_min=30是经验值,围网绕圈通常20到40分钟,刺网放网后漂流可能超过1小时,拖网更久。窗口太短抓不到完整动作,太长会把不同作业段混在一起。step_min=10保证样本量足够。straightness用平均航速除以累计转向角,拖网和刺网放网段数值高,围网数值低。stop_ratio对刺网漂流段特别敏感。

3.3 序列模型输入格式与标签对齐

如果不用统计特征,直接上LSTM或1D-CNN,输入就是每个窗口内的(lat, lon, sog, cog)序列,长度统一到60个点,不足补零并加mask。标签对齐时,窗口的标签取窗口中心时刻所属的作业段标签。如果窗口跨越两个标签段,丢弃该窗口。

def make_sequence(seg, max_len=60): 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), dtype="float32") return np.vstack([arr, pad])

max_len=60对应30分钟窗口内平均30秒一个点。如果AIS采样稀疏,可以降到30。补零后要额外返回一个mask向量,告诉模型哪些位置是真实点。标签用整数编码:seine=0, gillnet=1, trawl=2。

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_splits=5) for train_idx, val_idx in gkf.split(X, y, groups): model = lgb.LGBMClassifier( n_estimators=300, learning_rate=0.05, num_leaves=31, class_weight=class_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_leaves=31是默认值,样本少时降到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_ch=4, n_cls=3): super().__init__() self.conv = nn.Sequential( nn.Conv1d(in_ch, 32, 5, padding=2), nn.BatchNorm1d(32), nn.ReLU(), nn.Conv1d(32, 64, 3, padding=1), nn.BatchNorm1d(64), nn.ReLU(), nn.Conv1d(64, 64, 3, padding=1), nn.BatchNorm1d(64), nn.ReLU(), ) self.attn = nn.Linear(64, 1) self.fc = nn.Linear(64, n_cls) def forward(self, x, mask=None): x = x.permute(0, 2, 1) x = self.conv(x) x = x.permute(0, 2, 1) w = torch.softmax(self.attn(x), dim=1) if mask is not None: w = w * mask.unsqueeze(-1) w = w / (w.sum(dim=1, keepdim=True) + 1e-6) x = (x * w).sum(dim=1) return self.fc(x)

in_ch=4对应经纬度、航速、航向。卷积核从5降到3,是为了在短序列上保留更多局部变化。注意力权重在mask为0的位置被压掉,避免补零位置影响池化。训练时用交叉熵损失,优化器选Adam,学习率1e-3,batch 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(), lr=1e-3) criterion = nn.CrossEntropyLoss(weight=torch.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, average="macro") if f1 > best_f1: best_f1, patience = f1, 0 torch.save(model.state_dict(), "best.pt") else: patience += 1 if patience >= 5: break

average="macro"让三个类别等权,围网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条窗口人工标一遍,看模型错在哪。如果错的全是刺网和围网混淆,就补这两个类的样本;如果错的是拖网被分成刺网,检查航速特征是不是被异常值污染了。这套流程跑下来,模型从实验室到监管端可用,通常还需要两到三轮迭代。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/5 9:59:15

工业级MRAM存储方案:STM32F303VE与MR25H40CDF驱动开发与掉电保护实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/5 9:58:28

Android锁屏日期空白:Slice加载链路与生命周期排障全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/5 9:58:08

Materials Studio聚合物建模与盒子弛豫实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/5 9:56:52

Moodle题库建设全流程:分类、批量导入与随机组卷实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/5 9:53:59

CKEditor5视频引入、实时预览与自定义Toolbar插件开发实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/5 9:53:48

工业级MRAM存储方案:STM32F745VG驱动MR25H40CDF实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华