Kaggle Notebook 自建模块:导入、热更新与 Dataset 复用

发布时间:2026/10/2 18:27:51
Kaggle Notebook 自建模块:导入、热更新与 Dataset 复用 第一次在 Kaggle Notebook 里 import 自己写的工具函数绝大多数人都会在同一个地方摔一跤ModuleNotFoundError: No module named utils。文件明明就在眼前单元格里!ls也看得见Python 就是不认。更气人的是第二种情况——你把.py改了三遍Notebook 跑出来的结果和第一遍一模一样你开始怀疑是不是 Kaggle 有缓存或者是自己眼睛出了问题。这两个坑我在早期做特征工程脚本复用的时候全都踩过而且踩得很难看一次是在比赛截止前一小时发现reload没生效一次是把几个 G 的中间结果写进了/kaggle/tempSession 一关全没了。这篇内容就是把这些年我在 Kaggle 上折腾「自己的模块和文件」的经验完整摊开讲。核心要解决四件事在 Notebook 里怎么凭空创建.py文件并让import找得到、改完代码怎么让它立刻生效、怎么把写好的一堆模块沉淀成可跨 Notebook 复用的资产、以及Save Version和竞赛提交时文件到底被保存到了什么地方。适合刚上手 Kaggle 的新人也适合已经能跑通 baseline、但每次都在复制粘贴同一段预处理代码的老用户。全文按“先认清目录权限 → 再造模块 → 再解决热更新 → 再谈复用与持久化 → 最后复盘报错”这条线走每一步都给出可直接抄的命令和代码。1. Kaggle 笔记本的目录权限地图先搞清哪儿能写、哪儿写完就没1.1 /kaggle/input 与 /kaggle/working 的读写边界Kaggle Notebook 的容器里能看到的顶层目录就那么几个但它们的脾气完全不同。/kaggle/input是挂载区你Add Data加进来的数据集、别人分享的 Utility Script、竞赛给的训练数据全部出现在这里。它的属性是只读你在里面mkdir、touch、rm都会直接报Read-only file system。这个设计不是为了难为你而是因为/kaggle/input下的内容是按版本号索引的共享资产平台要保证同一个 Dataset 版本在你和别人机器上字节级一致一旦允许写入缓存和校验就全乱套了。/kaggle/working才是你的地盘也是 Notebook 启动后的默认当前目录os.getcwd()返回的就是它。它有写权限而且关键点在于保存版本时这个目录里的文件会被打包成 Notebook 的 Output 一并存下来。这就是为什么你做竞赛时生成的submission.csv必须落在这里——它是你唯一能“带走”的路径。还有一个经常被忽略的/kaggle/temp同样是可写的但它和/kaggle/working共享同一份磁盘配额并且不会被保存。它的定位是草稿纸适合放解压出来的中间数据、临时模型权重、调参过程中产生的垃圾文件。很多人把它当成第二个 working 用结果 Session 一结束发现训练了三小时的中间结果蒸发了。路径可写保存版本时是否保留典型用途/kaggle/input否不适用只读挂载数据集、竞赛数据、Utility Script/kaggle/working是是自己写的模块、submission.csv、最终产物/kaggle/temp是否解压中间件、临时权重、调参垃圾/root、/usr等系统目录视情况否装包、改配置当前 Session 有效1.2 为什么文件明明写着下次打开却不见了这个问题的答案几乎永远落在三个原因上按出现频率排序第一你把文件写到了/kaggle/temp或者/tmp或者用了相对路径但当时cwd被os.chdir改过。Kaggle 的 Session 是一次性的容器销毁后这些目录直接重置。第二文件确实写进了/kaggle/working但你只是关掉了浏览器标签没有点 Save Version。Notebook 的 working 目录在未保存版本的情况下不会留档下次打开就是干净的。第三你保存了版本但保存的是Quick Save而不是完整运行某些依赖前面单元格状态才生成的文件在重放时没跑出来。我自己给自己定了一条死规矩凡是希望下次还能看到的东西一律写/kaggle/working下的绝对路径并且在收工前跑一次完整保存。相对路径在 Notebook 里看着优雅但只要你中间哪个单元格调了os.chdir去做!cd风格的操作后面所有相对路径就全歪了这种 bug 排查起来极其恶心。1.3 进 Notebook 先跑的三行体检命令养成习惯开新 Notebook 的第一件事不是 import pandas而是体检import os, sys print(cwd:, os.getcwd()) print(python:, sys.version.split()[0]) print(已挂载的数据集:, os.listdir(/kaggle/input))再补一条磁盘检查尤其是你准备下载大模型权重或者解压大压缩包之前!df -h /kaggle/working !ls -la /kaggle/workingdf -h会告诉你当前配额还剩多少。Kaggle 的 working 空间有上限写满之后的报错非常不直观——通常表现为OSError: [Errno 28] No space left on device或者保存版本时静默失败、Output 里少文件。提前看一眼比事后在几十 G 的目录里找哪个文件该删要省事得多。提示如果你的 Notebook 是从某个已保存版本“重新打开”的/kaggle/working里会自动恢复上次保存的输出文件。文件多的时候容器启动会明显变慢这也是为什么不要把临时文件留在 working 里的一个现实理由。2. 在单元格里直接“长”出模块文件%%writefile 的用法与目录组织2.1 %%writefile 的最小可用写法在 Kaggle 上创建自己的.py文件最顺手的工具是 IPython 的%%writefile魔法命令。它把一个单元格的内容原样写进指定文件%%writefile /kaggle/working/mytools/metrics.py import numpy as np def qwk(y_true, y_pred): 简化版二次加权 Kappa仅用于演示模块导入 y_true np.asarray(y_true) y_pred np.asarray(y_pred) return float((y_true y_pred).mean())这里有几个必须知道的细节。%%writefile必须是单元格的第一行前面不能有任何内容连注释都不行否则会被当成语法错误。默认行为是覆盖如果你想要追加需要写成%%writefile -a 路径。另外路径不带/时会相对于cwd也就是/kaggle/working但我强烈建议一律写绝对路径理由上一节已经说过了。写完之后你可以立刻验证!cat /kaggle/working/mytools/metrics.py如果这一步看到的内容和你单元格里写的不一致别怀疑 Python去检查是不是有不带绝对路径的重复写入把文件覆盖了。2.2 目录结构怎么设计才不会乱模块超过三个之后扁平放一堆util1.py、util2.py就开始难维护了。我的习惯是建一个包目录!mkdir -p /kaggle/working/mytools然后在里面放__init__.py。Python 3 里命名空间包理论上不需要__init__.py也能导入但在 Kaggle 这种“文件是魔法命令一个个写出来”的场景下显式放一个空文件能避免很多玄学问题尤其是你后面要把整个目录打包成 Dataset 复用的时候。%%writefile /kaggle/working/mytools/__init__.py __all__ [metrics, features]推荐的目录约定长这样它同时兼顾了“Notebook 里随时改”和“之后打包复用”两个需求/kaggle/working/mytools/ __init__.py metrics.py # 评估指标 features.py # 特征工程 io_utils.py # 读写与路径处理 configs/ base.json # 配置文件也放进来读的时候用绝对路径配置类的文件建议直接用%%writefile写成 JSON 或者 YAML比在 Notebook 里硬编码字典好维护得多也能跟模块一起被打包带走。2.3 让 import 找到你的目录sys.path 的三种挂法文件存在不等于能被导入。Python 的导入搜索路径是sys.path列表/kaggle/working本身通常在列表里但/kaggle/working/mytools的父目录才需要在路径上——因为你要import mytools而不是import metrics。运行时挂载最直接的一种import sys sys.path.insert(0, /kaggle/working) import mytools.metrics as metrics print(metrics.qwk([1, 0], [1, 1]))insert(0, ...)把它放在搜索顺序最前面好处是你改的模块一定优先于同名的第三方包。坏处也正是这个千万别把自己的文件命名为json.py、random.py、types.py一旦这些名字出现在搜索路径首位标准库和第三方库的导入会被你的文件劫持报出各种莫名其妙的AttributeError。我见过一次把工具文件叫select.py结果整个 Notebook 里 pandas 的某些内部导入行为全乱了排查了四十分钟。第二种挂法是通过.pth文件适合你想让路径在整个 Session 里自动生效import site, os pth os.path.join(site.getsitepackages()[0], kaggle_local.pth) with open(pth, w) as f: f.write(/kaggle/working\n)第三种是环境变量PYTHONPATH但 Kaggle 的 Notebook 内核已经启动了改环境变量对当前内核无效只对!python xxx.py这种子进程有用。综合下来我的建议是Notebook 内交互开发用sys.path.insert子进程脚本用PYTHONPATH.pth除非你有强迫症否则不必。挂载方式生效范围重启内核后是否保留推荐场景sys.path.insert当前内核否交互开发最常用.pth文件当前容器所有 Python 进程否容器销毁即失效多处复用路径PYTHONPATH子进程否!python train.py打包成 Dataset 挂载跨 Notebook是长期复用的模块仓库2.4 相对导入为什么会炸写包的时候你会本能地想用from . import features然后在 Notebook 里直接import metrics试一下立刻炸ImportError: attempted relative import with no known parent package原因很简单相对导入的前提是模块知道自己属于哪个包也就是__package__有值。当你在 Notebook 顶层直接导入一个文件时它被当作顶层模块__package__是None自然没有“父包”可以..回去。正确做法是两条。第一在包里统一用绝对导入从包名根部写起# mytools/features.py from mytools.io_utils import load_config这样无论从 Notebook 还是从 Dataset 挂载导入路径都是唯一的。第二确保包的父目录在sys.path上也就是前面说的insert(0, /kaggle/working)。只要你坚持“绝对导入 父目录上路径”这个组合跨环境复用基本不会出问题。注意包名不要和已安装的第三方库重名。你叫mytools、kaggle_utils都很安全叫utils、common就很容易撞上别人写的同名包或者在 Dataset 挂载后被另一个同名目录覆盖这种冲突在 Kaggle 上排查起来非常费劲。3. 改完 .py 却还跑旧代码import 缓存与 reload 的完整排查3.1 现象文件明明改了函数返回值没变先把场景还原清楚。你在/kaggle/working/mytools/features.py里写了一个build_features第一次导入跑通输出 42 维特征。然后你用%%writefile把它改成输出 58 维重新跑一遍单元格打印出来的形状还是 42。你去!cat文件确认文件里确实是新的代码58 明明白白写在那里。甚至你删掉了__pycache__重启单元格还是 42。如果你遇到的是这个现象那可以确定问题不在文件而在当前内核的sys.modules里还躺着旧模块对象。3.2 根因sys.modules 里躺着旧对象pycache只是背锅的Python 的导入机制是这样的第一次import mytools.features时解释器找到文件、编译成字节码、执行模块体、把结果模块对象存进sys.modules这个字典里。之后任何一次import只要sys.modules里有这个名字就直接返回缓存对象根本不会再去看磁盘上的文件。__pycache__里的.pyc是另一回事。它的作用是缓存编译结果判断是否需要重新编译的依据是源文件的修改时间和大小。你正常改文件mtime 变了.pyc会被自动重编译所以删__pycache__通常解决不了这个问题。很多人把锅扣在.pyc上删了半天没效果就是这个原因。更隐蔽的一层是这样写的代码from mytools.features import build_features这种from ... import ...形式会把函数对象绑定到当前命名空间的一个新名字上。即使你后来importlib.reload了模块build_features这个名字指向的还是老函数对象不会自动更新。这是“reload 了也没用”的最常见原因比缓存本身更坑。3.3 importlib.reload 的正确姿势标准解法是importlib.reload但用法有讲究import importlib import mytools.features importlib.reload(mytools.features) # 重新执行模块体sys.modules 里的对象被就地更新关键点在于重新执行模块体时使用的是原来的模块对象也就是说别处import mytools.features拿到的是同一个被更新过的对象。但如果你之前用了from ... import build_features那个局部名字还是老的需要重新执行一次 from 导入from mytools.features import build_features # 重新绑定多级包的情况下reload 顺序也要注意。mytools/__init__.py里如果from mytools import features那你得先 reload 子模块mytools.features再 reload 父包mytools否则父包里的引用还是指向旧子模块。我踩过一次这个坑改了metrics.pyreload 了mytools结果mytools.metrics依然是旧对象因为父包的__init__在上一轮就已经把旧引用固化了。写一个可靠的 reload 小工具我自己一直在用import importlib, sys def reload_pkg(prefix): 按依赖从深到浅 reload 所有以 prefix 开头的模块 names [n for n in sys.modules if n prefix or n.startswith(prefix .)] for n in sorted(names, keylambda x: x.count(.), reverseTrue): importlib.reload(sys.modules[n]) return names reload_pkg(mytools)3.4 autoreload把手工 reload 这件事自动化手工 reload 写多了会烦IPython 有个autoreload扩展可以自动处理%load_ext autoreload %autoreload 2autoreload有三种模式。0是关闭。1是每次执行单元格前重新导入所有以%aimport标记过的模块。2是每次执行前重新导入所有模块除了%aimport排除的这是最省心的模式我日常就用2。它有几个边界要知道。第一它只对磁盘上的.py文件生效对你在 Notebook 单元格里直接定义的类、函数完全无效——这些代码没有对应的文件autoreload 无从下手。第二某些模块在 import 时有副作用比如启动线程、打开文件句柄、注册全局钩子reload 会把这些副作用叠加执行一次可能造成资源泄漏或者重复注册。第三也是最容易被忽略的一点autoreload是交互开发期的工具在 Save Run All 这种一次性从零重放的场景里它是多余的而且如果扩展加载失败或者行为异常会给你带来额外的不确定性。所以我的做法是分两条线交互调试阶段开%autoreload 2等代码稳定、准备正式保存版本前把它注释掉保证一次干净的重放。方案改代码后是否自动生效对 from-import 是否有效副作用风险重启内核重跑是是无但丢状态importlib.reload手动触发否需重新 from 导入低%autoreload 2是是模块副作用可能重复执行删__pycache__否否无效操作4. 把模块沉淀成 Dataset跨 Notebook 复用的正经做法4.1 两条上传路径网页端与 API单个 Notebook 里折腾模块规模一大就会遇到新问题三个比赛共用同一套特征工程你不想每次都复制粘贴那两百行。这时候正确做法是把模块目录变成一个私有 Dataset然后在任何 Notebook 里Add Data挂载。网页端最省事Datasets 页面点New Dataset把整个mytools文件夹拖上去填一个不重名的标题创建后就能在任何 Notebook 的输入面板里搜到。缺点是每次更新都要手工拖一遍版本多了容易搞混。API 适合经常迭代的人。先在账号设置里拿到 API Token在 Kaggle Notebook 里通过 Secrets 注入Add-ons→Secrets然后在 Notebook 里mkdir -p ~/.kaggle echo $KAGGLE_KEY_JSON ~/.kaggle/kaggle.json chmod 600 ~/.kaggle/kaggle.json kaggle datasets create -p /kaggle/working/mytools --dir-mode zip后续更新用kaggle datasets version -p /kaggle/working/mytools -m add calibration utils。这里有个小细节-m的说明一定要写清楚因为 Dataset 的版本历史会一直留着半年后你自己回头看只有一句“update”是完全想不起来改了什么的。4.2 只读挂载对模块设计的三条约束Dataset 挂载进/kaggle/input/your-dataset-name/之后是只读的这直接约束了你的模块怎么写。第一条模块里不要写任何缓存文件到自身目录。很多人喜欢在模块里做一层cache.pkl加速特征计算本地跑没问题挂到 Kaggle 上就报Read-only file system。正确做法是把缓存路径做成参数或者读环境变量默认指向/kaggle/working/cacheimport os CACHE_DIR os.environ.get(MYTOOLS_CACHE, /kaggle/working/mytools_cache) os.makedirs(CACHE_DIR, exist_okTrue)第二条不要在模块层面做pip install -e这种可编辑安装。可编辑安装依赖源目录可写只读挂载下直接失败。如果需要第三方依赖用带--no-index的本地 wheel 安装把 wheel 文件也放进 Datasetpip install --no-index --find-links/kaggle/input/mywheels mypackage第三条日志和中间产物一律外置。模块内部不要logging.basicConfig(filename...)指向包目录交给调用方配置。4.3 一个能长期维护的模块仓库目录约定我把这套约定用了两年多跨了十几个 Notebook基本没出过导入问题mytools/ __init__.py # 只做 __all__ 声明不写业务逻辑 metrics.py features/ __init__.py tabular.py text.py io_utils.py configs/ default.yaml tests/ test_metrics.py # 放一个最小冒烟测试上传前跑一遍__init__.py只做声明这件事看着多余但它的价值在于避免导入时的副作用。一旦你在__init__里写了from mytools.features import build_features这种实打实的导入任何import mytools都会连带把整个依赖树拉起来一旦某个子模块依赖了当前环境没装的包整个包就废了。保持__init__干净让使用者按需导入具体子模块是长期可维护的关键。4.4 更新 Dataset 版本后Notebook 怎么拿到新代码这里有一个很多人踩过的坑你在 Notebook 里挂载的 Dataset 版本是被固定的。你给 Dataset 推了新版本已经打开的 Notebook 不会自动切过去它仍然指向挂载时那个旧版本号。解决办法有两种在 Notebook 的输入面板里把该数据集移除再重新添加会默认选最新版或者直接重开一个 Notebook。如果你希望在 Notebook 里显式声明依赖的版本可以在 Dataset 的metadata里打上版本语义然后在 Notebook 开头断言一下import json info json.load(open(/kaggle/input/mytools/dataset-metadata.json)) assert info[version] 32, f模块版本过旧: {info[version]}这行断言看着简单但它救过我一次一次比赛我在两个 Notebook 之间来回切其中一个还在跑三十多版之前的特征逻辑两边结果差了 0.3 个点查了两小时才发现是版本不一致。5. Save Version 与竞赛提交文件到底被存到哪儿了5.1 Quick Save 与 Save Run All 的差别Kaggle 的保存有两个入口行为差别很大。Quick Save保存的是当前 Notebook 的代码和当前输出状态它不会重新执行任何单元格。这意味着如果你的输出依赖前面某些单元格的运行结果而那些单元格这次没跑Quick Save 之后你打开会发现 Output 是空的或者残缺的。Save Run All会在一个全新的容器里从头到尾按顺序重新执行所有单元格。这是提交竞赛结果时必须走的路径因为只有它才能证明你的 Notebook 是自洽可复现的。代价是慢而且会暴露所有隐藏的状态依赖——比如某个变量其实是你手动在中间单元格临时赋值调试出来的重放时它就不存在了。我自己的判断标准很简单任何要在 Output 里留文件的场景一律用 Save Run All。Quick Save 我只用来保存一个还在调试中的草稿状态。5.2 输出文件在哪儿取怎么在下一个 Notebook 里用保存完成后/kaggle/working里的文件会出现在 Notebook 页面右侧的Output标签页里可以直接下载。如果这个 Notebook 的产物你打算在别的 Notebook 里用可以在 Output 页面一键New Dataset之后就变成/kaggle/input/...挂载进来。一个很实用的技巧从 Notebook 直接创建的 Dataset 会自动跟踪这个 Notebook 的输出你在 Notebook 里更新文件、重新保存版本Dataset 那边可以拉取最新省去手工上传。跑模型训练产出的 checkpoint 用这种方式管理比每次打包下载要顺得多。5.3 提交时的路径与磁盘陷阱竞赛提交时Kaggle 找的是你 Output 里的submission.csv。它的路径必须是/kaggle/working/submission.csv位置不对或者文件名不对提交按钮会直接告诉你找不到文件。我见过有人把它写到了submission/submission.csv纠结了半天。磁盘陷阱更隐蔽。/kaggle/working有容量上限写满之后保存版本会失败但错误提示往往不指向真正的凶手。常见的三个爆盘来源一是训练时每个 epoch 都存一个 checkpoint二是把解压出来的大文件留在了 working 而不是 temp三是 Jupyter 的隐藏检查点或者__pycache__堆积这个量级通常不大但目录里几千个小文件会显著拖慢保存速度。几个可以直接用的规避手段# 每个 epoch 只保留最好的一个权重 if score best: best score torch.save(model.state_dict(), /kaggle/working/best.pt) # 而不是 /kaggle/working/epoch_{n}.pt# 解压一律解到 temp !unzip -q /kaggle/input/xxx/data.zip -d /kaggle/temp/data # 提交前清一遍 !rm -rf /kaggle/working/**/__pycache__ !df -h /kaggle/working另外提一句如果你用的是 TPU 或者 GPU 加速器中途有闲置超时长时间不操作 Session 会被回收。这也意味着那些“跑一半先存着明天接着来”的思路在 Kaggle 上行不通必须让整个流程在一次运行内闭合。6. 三组真实报错的排查链路复盘6.1 ModuleNotFoundError从打印 sys.path 开始逐步定位碰到ModuleNotFoundError不要瞎改代码按这个顺序走。第一步确认包的父目录在不在路径上。:import sys print(\n.join(sys.path))看有没有/kaggle/working。没有的话sys.path.insert(0, /kaggle/working)再试。第二步确认目录里有没有__init__.py。如果你 import 的是mytools.metrics但mytools目录下缺__init__.py在部分路径拼接方式下会被当成普通目录而不是包。补一个空文件代价极低。第三步确认文件名和 import 语句大小写一致。Kaggle 跑在 Linux 上Metrics.py和metrics.py是两个不同的文件Windows 上写惯了代码的人在这里特别容易翻车。第四步%%writefile的路径和你sys.path里加的是不是同一个目录。我出现过一次这样的组合文件写在/kaggle/working/mytools/路径加的是/kaggle/working/myutils/一个字母之差排查了十五分钟。第五步如果前面都对了还是报错!find /kaggle/working -name *.py把实际文件列出来对一遍用眼睛确认文件真的存在。%%writefile失败有时不会抛异常尤其是在单元格第一行有隐式空格的时候。6.2 改了代码结果没变一次完整的幽灵问题排查这个问题的排查路径和上一个完全不同重点是“到底哪一层没更新”。先确认磁盘上的文件是新的用!cat或!md5sum看内容或者哈希。这一步排除了%%writefile没写进去的可能。第二步在 Notebook 里打印模块对象的文件位置确认它加载的是你以为的那个文件import mytools.metrics as m print(m.__file__)如果输出指向了/kaggle/input/...而不是/kaggle/working/...说明你手工写的版本被挂载的 Dataset 版本盖住了因为路径搜索顺序里 input 在前。这种情况要么调整sys.path顺序要么干脆别用挂载版本。第三步确认你调用的函数是模块属性还是 from-import 出来的局部名字。前者 reload 后自动更新后者不会。把from mytools.metrics import qwk改成import mytools.metrics as metrics然后metrics.qwk(...)调用能省掉一大类困惑。第四步如果还是不对直接重启内核重跑看结果是否变化。如果重启后正常了那百分之百是sys.modules缓存前面的reload_pkg工具就是为它准备的。6.3 磁盘写满 / 输出保存失败working 空间用超的排查保存版本时 Output 里少文件、或者报一个很含糊的错误先怀疑空间!du -sh /kaggle/working/* | sort -h | tail -20这条命令按大小排序列出 working 下的目录最大的几个一眼就能看到。训练类 Notebook 的凶手通常是 checkpoint 目录和日志文件数据处理类的通常是没清理的临时 parquet。清理顺序建议从/kaggle/temp开始它本来就不该留着重要东西。注意torch.save出来的权重经常一个就几百兆配合多个实验分支很容易堆到上限。最后再分享一个我自己的习惯。我在每个 Notebook 接近尾声的地方都会加一个固定的“收尾单元格”做三件事删掉__pycache__和临时目录、打印df -h和ls /kaggle/working确认输出清单、然后才点 Save Run All。这个单元格在交互调试时是注释掉的只在正式保存时放开。坚持了半年之后我基本再没遇到过“保存完发现东西少了”这种糟心事。模块这事也是一样把创建、reload、打包、清理这四步固化成流程剩下的精力才能留给真正重要的建模和特征。