653. BYU - Locating Bacterial Flagellar Motors 2025 | byu-locating-bacterial-flagellar-motors-2025
虽然这次比赛我实际上是单人参赛(除了 @daphne4sg),但我想感谢所有在另一个 LLM 比赛中帮助过我并分享生活美好事物的队友 @goodcoder、@bigochampion 和 @cybersimar08。我从你们身上学到了很多。我也想感谢 @andrewjdarley 举办了如此精彩的比赛。这个项目对我的工作面试有很大帮助。由于你们的目标非常有趣,我非常愿意进行进一步的开发。
该方案源于我职业生涯中最好的项目,那是我一年前在硕士学位期间完成的。我通过工业合作关系与 Micron Technology 的一位总监合作。该项目极具挑战性——由于数据隐私限制,我们只能使用 88 张 HBM 扫描图像来构建高性能异常检测器。
此外,标签是 AOI 判定结果和人类专家注释的混合体。我们的目标是对人类专家的偏好进行建模。我的 BYU 解决方案的一些部分改编自这个 HBM 项目,例如关键点生成和从图结构中推断标签。
然而,我方法中最具创新性的方面是利用视觉 - 语言模型(VLM)进行少样本预测,而不是像 YOLO 这样的传统物体检测器。本质上,我使用诸如“这个 HBM 是异常的”或"HBM 是正常的”这样的提示来表示目标的存在或不存在,允许模型进行初步分类猜测。
我的解决方案使用基于图的方法校准文本 - 图像嵌入空间,使 VLM 能够有效地执行我们的特定任务。结果,我能够仅用大约 20 张图像训练模型,并且它成功检测到了 HBM 数据集中人类专家识别的几乎所有异常。
初步想法可以参考我早些时候公开的 notebook。我想生成大量的点,然后通过建立它们与标签的关系来细化它们。我的策略是在细菌周围采样,因为马达存在于它的头部。如果我聚合所有的关键点并总结它们,就可以捕捉到正确的语义并避开噪声标签。然而,最大的问题是如何表示每个点的特征,所以我仅使用从简单 CNN 提取的补丁特征,然后构建 GNN 来预测马达的位置。结果证明这是一个非常糟糕的模型。
有趣的是,我两个月前差点在 这个帖子 中泄露了我的解决方案。我就此发展与 @hengck23 进行了简短的讨论。在这次讨论之后,我开始实施它,这个想法变成了下面展示的流程。基本上,我使用 YOLO11 的 C3K2 特征图,因为每个空间位置的特征可以代表细化的物体。设置较小的置信度阈值(conf=0.05)可以过滤掉足够多的由 YOLO11 确定的物体位置。这种设计极大地提高了分数,基本方法得到了很大改进。
特征对马达预测有很大影响。我尝试了几乎每种类型的特征,如 HoG、Daisy、EfficientNet、ResNet 和不同版本的 YOLO,除了 YOLO11 外,它们的效果都不好,但分数仍然不够好。几天后,我在 PYG 文档 上发现了 AddRandomWalkPE。我突然意识到这是 3D 任务中缺失的成分。添加它后分数立即提升。所以节点特征是 C3K2 + RandomWalkPE,其中 walk_length=8。我认为它提高分数的主要原因是因为它可以表示每个点的位置,隐式地建模马达的位置。
YOLO 通过设置低置信度阈值成功生成了很多点,但在 3D 空间中仍然稀疏,并且很有可能遗漏物体的存在。所以我解决这个问题的策略是使用预测点作为中心,然后在半径内均匀采样其他点。我设置 radius 和 num_samples 分别控制范围和密度。我发现使用这种策略训练 GNN 也能提高分数。
由于将单个点注释为马达不能完全代表实际的语义(这就是为什么主持人在指标中给出了如此大的半径容差),我通过以下条件将点标记为正样本:
thr_sim。thr 内。def feat_labeling(points, feat, extract_feat, tomo_id, train_labels, thr = 10, thr_sim=0.5):
label = train_labels[train_labels.tomo_id == tomo_id]
print('location:', label[['Motor axis 0', 'Motor axis 1', 'Motor axis 2']].values[0])
n_motors = label['Number of motors'].item()
if n_motors == 0:
return np.zeros(points.shape[0], )
d, h, w = label[['Array shape (axis 0)', 'Array shape (axis 1)', 'Array shape (axis 2)']].values[0]
loc = label[['Motor axis 0', 'Motor axis 1', 'Motor axis 2']].values[0]
tz, ty, tx = loc.astype('int32')
fd, fh, fw = feat.shape[0], feat.shape[2], feat.shape[3]
z = tz.astype('int32')
y = ((ty/h) * fh).astype('int32')
x = ((tx/w) * fw).astype('int32')
#sim
extract_feat = extract_feat / np.linalg.norm(extract_feat, axis=-1, keepdims=True)
target_feat = feat[z, :, y, x][None, :]
target_feat = target_feat / np.linalg.norm(target_feat, axis=-1, keepdims=True)
sim = extract_feat @ target_feat.T
bool1 = sim[:, 0]>thr_sim
#dist
dist = np.linalg.norm(points - loc[None, :], axis=-1) #(N, 3)
bool2 = dist<thr
#label
label = np.array(bool1 & bool2, dtype='float32')
return label
我尝试了各种图结构,最终发现结合 k-NN 图和 Delaunay 图 能产生最佳结果。k-NN 图提供高密度,而 Delaunay 图在远距离集群之间建立连接。通过利用这两种属性,结合的方法产生了更通用的模型。下面的实验结果清楚地解释了为什么有人只提交了 16 次就去睡觉了。
| 公共分数 | 私有分数 | CV | |
|---|---|---|---|
| Knn Graph | 0.835 | 0.835 | 0.968 |
| Radius Graph | 0.826 | 0.830 | 0.966 |
| Delaunay Graph | 0.801 | 0.806 | 0.962 |
| Knn + Radius | 0.841 | 0.835 | 0.966 |
| Knn + Delaunay | 0.856 | 0.843 | 0.966 |
首先在竞赛数据集上训练 YOLO。我不使用外部数据集,因为我认为注释者不是同一个人,所以可能会对马达的存在给出不同的判断。然后为每个折叠训练两个 GraphSages,使用 Binary Cross Entropy 和 positive_weight=8。在我选择的提交中,集成函数是最大值,但在某些场景下 HDBSCAN 更好。GNN 的训练数据是从 YOLO 过滤的点中收集的,其中包含正负数据。这里没有使用额外的数据集。
不幸的是,我在实验中陷入了困境,无法在截止时间的最后几分钟做出正确的选择。YOLO11L 预测了很多假阳性,这在这场比赛中是好的。但我没有选择它。这意味着我作为数据科学家的心态和经验还需要提高。
| 公共分数 | 私有分数 | CV | 提交时间 | |
|---|---|---|---|---|
| YOLO11L | 0.856 | 0.843 | 0.968 | 7HR |
| YOLO11X | 0.865 | 0.831 | 0.983 | 11HR |
我的流程相当复杂,验证这些方法花费了我大量的工程精力。这是过去三个月的所有实验记录:Google Sheet
如果您有任何问题,请在下方评论或使用 email & linkin 联系我!
欢迎您在您的博客上分享我的解决方案——这是我的荣幸!我希望其他人会觉得它有用,并在未来的比赛中参考它。