ARTICLE DETAIL

资讯详情

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

Kaggle Notebook 模块导入、文件管理与代码库复用指南

Kaggle Notebook 模块导入、文件管理与代码库复用指南 在 Kaggle Notebook 里写代码写久了早晚会撞上这么一件事你在一个 Notebook 里反复调试好的数据清洗函数、特征工程类、模型封装想搬到另一个 Notebook 里接着用结果一粘贴就是几百行改一个地方要同步改三份改到最后自己都记不清哪份是最新的。这时候自然会想到——能不能像本地项目那样把自己的代码抽成独立的模块文件需要的时候直接 import 进来答案是可以但 Kaggle 的在线环境在目录结构、读写权限、会话生命周期上和本地 IDE 差别不小直接照搬本地那套做法基本会踩坑。这篇就把 kaggle 上添加、修改自己的模块和文件这件事从头捋一遍讲清楚目录该怎么放、import 为什么会失败、改动怎么才能不丢、以及怎么用数据集把代码库沉淀下来复用。不管你是刚注册 kaggle 的新手还是已经在打比赛、跑数据集的老手这套方法都能直接用。1. 先把 Kaggle 的目录结构摸清楚1.1 三个关键目录各自能干什么很多人在 Kaggle 上 import 自己的模块失败根子不在代码写错而在文件放错了地方。Kaggle Notebook 的环境里有几个固定目录每个的职责是明确分开的先记住这张表路径可读可写会话重启后典型用途/kaggle/working是是保留保存版本后自己的代码、输出结果、submission.csv/kaggle/input是否保留挂载的数据集、代码库、预训练权重/kaggle/usr/lib是否保留平台预置的一些库/kaggle/temp是是清空大文件的临时中转/kaggle/working是唯一属于你的可写目录也是 Notebook 启动时的默认工作目录。你在单元格里!touch test.py文件就落在这里。而/kaggle/input下面挂的是你在右侧面板添加的数据集它是只读的——这一点极其关键很多人想直接改数据集里的文件结果报Read-only file system就是因为没意识到这一点。之所以这样设计是因为 Kaggle 需要保证数据集的完整性和可复现性。如果每个用户都能改挂载进来的数据那同一个数据集在不同人手里内容就不一样了比赛成绩的可比性也就没了。所以平台把「只读的数据」和「可写的产物」硬性隔开这其实是个好习惯本地项目也值得学。1.2 为什么你的 import 会失败Python 的 import 机制说穿了就一句话解释器拿着模块名去sys.path这个列表里的每一个目录挨个找同名文件。找到了就加载找不到就ModuleNotFoundError。你在 Notebook 里敲import sys print(sys.path)会看到类似这样的输出[/kaggle/working, /opt/conda/lib/python310.zip, /opt/conda/lib/python3.10, /opt/conda/lib/python3.10/lib-dynload, /opt/conda/lib/python3.10/site-packages, /kaggle/usr/lib]注意第一项就是/kaggle/working。这意味着你把mymodule.py丢在/kaggle/working下直接import mymodule是能成功的。但如果你把模块放在/kaggle/input/myrepo/utils.py它不在sys.path里自然就找不到。这时候你有两个选择要么把它加进sys.path要么用 importlib 按文件路径直接加载。这两种方式后面会详细讲。还有一个隐蔽的坑Kaggle Notebook 里如果你用%cd或者os.chdir()切走了当前目录sys.path里的代表当前目录就可能解析到别处导致原本能 import 的模块突然失效。这种情况我遇到过不止一次排查半天才发现是自己前面切了目录忘了切回来。注意每次会话重启sys.path的修改都会丢失必须在 Notebook 开头重新执行一遍。所以把路径注入代码放在最前面的单元格里是个好习惯。1.3 会话重启后文件去哪了Kaggle Notebook 的会话有两层概念很多人到很后面才搞明白。第一层是「运行中的会话」就是你点开 Notebook、点 Run 之后的那段运行时间通常有时长上限闲置太久会被回收。第二层是「保存的版本」也就是你点 Save Version 之后生成的那个快照。/kaggle/working里的文件在会话运行期间一直在。但如果会话被回收、你重新打开 Notebook 从头跑之前 working 目录里手写的文件默认是没了的——除非你在保存版本时把它们一起提交了或者把它们写进了输出产物里。这个机制和本地磁盘完全不同本地你写个文件就在那了Kaggle 上必须显式保存。所以我的做法是在 Kaggle 上写自己的模块不要在会话里手敲一遍就完事要么用%%writefile明确落盘并随版本保存要么把代码库做成数据集长期复用。前者适合临时调试后者适合沉淀。这个区分非常关键后面第 3 节会展开。2. 在 Notebook 里就地创建和修改自己的模块2.1 %%writefile把单元格变成文件最顺手的一招就是 IPython 提供的%%writefile魔术命令。它能把当前单元格的内容直接写成一个文件。比如%%writefile /kaggle/working/feature_utils.py import numpy as np import pandas as pd def fill_missing(df: pd.DataFrame, cols: list) - pd.DataFrame: 对指定列做中位数填充返回新对象不改原表。 df df.copy() for c in cols: if c in df.columns: df[c] df[c].fillna(df[c].median()) return df def target_encode(df, col, target, m10): 带平滑的目标编码m 是平滑系数。 prior df[target].mean() agg df.groupby(col)[target].agg([mean, count]) smooth (agg[mean] * agg[count] prior * m) / (agg[count] m) return df[col].map(smooth)执行完这个单元格/kaggle/working/feature_utils.py就真实存在了。可以立刻验证!ls -lh /kaggle/working/%%writefile有两个变体值得记住%%writefile -a是追加模式往已有文件末尾续写不带-a是覆盖模式每次执行都会把整个文件重写。所以我改模块的流程通常是这样直接在原来的单元格里改代码重新执行一次文件就被覆盖成新的了。整个体验和本地编辑保存没什么差别但省去了上传下载的麻烦。一个实战细节%%writefile会原样写入不做任何转义所以你的中文注释、特殊符号都能正常保留。但如果文件里包含%开头的行可能会被当成魔术命令这时用%%writefile的转义写法或者干脆改成with open(...)写。我一般习惯在写完后用!head -n 5瞄一眼确认没被截断。2.2 让 Python 找到你的模块sys.path 的正确用法文件写好了接下来是让它能被 import。前面说了/kaggle/working本身就在sys.path里所以最省事的做法就是把模块写到这里然后import feature_utils df feature_utils.fill_missing(df, [age, income])但如果你把模块放在了/kaggle/input下的数据集里就需要手动注入路径import sys MODULE_PATH /kaggle/input/my-code-lib if MODULE_PATH not in sys.path: sys.path.insert(0, MODULE_PATH) import feature_utils这里用insert(0, ...)而不是append是有讲究的插到最前面优先级最高如果恰好有个同名的第三方包你的模块会优先被加载。这在你想覆盖某个库的时候特别有用后面第 4 节会讲。用append的话你的模块排在最后容易被前面的同名包截胡。那个if MODULE_PATH not in sys.path的判断也不是多余的。如果你的 Notebook 里多个单元格都执行了这段注入代码不加判断会往列表里塞重复项虽然不影响运行但排查问题时看着乱。养成幂等写法的习惯代码会干净很多。2.3 只加载单个文件importlib 那条路有些场景下你不想动sys.path只想按绝对路径加载某个具体的 .py 文件。比如你有一堆不同实验版本的脚本想同时对比它们的行为这时候sys.path方案就不合适了因为模块名会冲突。用 importlib 可以精确控制import importlib.util def load_module_from_path(module_name: str, file_path: str): spec importlib.util.spec_from_file_location(module_name, file_path) module importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module v1 load_module_from_path(exp_v1, /kaggle/input/exp-v1/model.py) v2 load_module_from_path(exp_v2, /kaggle/input/exp-v2/model.py) print(v1.VERSION, v2.VERSION)这段代码的好处是v1和v2是两个完全独立的模块对象即使它们内部都叫model.py也不会互相覆盖。做模型对比实验的时候这个技巧能省下大量复制粘贴。代价是这种方式加载的模块不会自动注册到sys.modules里除非你手动sys.modules[module_name] module所以模块内部的相对导入、pickle 反序列化等操作可能会有意外行为。我的经验是临时性、一次性的脚本加载用 importlib需要长期复用、被多个文件互相引用的代码库老老实实走 sys.path 标准包结构。2.4 改完代码要 reload不然白改这可能是新手最容易抓狂的点。你在 Notebook 里改完feature_utils.py重新 import发现行为没变——因为 Python 的模块在第一次 import 后就缓存在sys.modules里了后续的import语句直接返回缓存对象根本不去读磁盘。解决办法有两个。手动的方式import importlib import feature_utils importlib.reload(feature_utils)自动的方式我强烈推荐直接在 Notebook 第一个单元格里放%load_ext autoreload %autoreload 2%autoreload 2的含义是每次执行单元格之前自动重新加载所有非核心模块。这样你改完 .py 文件直接跑下一个单元格就能生效完全不用管 reload 的事。这个设置我在 Kaggle 上几乎每个 Notebook 都会加属于提效刚需。注意autoreload 对已经被实例化并长期持有的对象是不生效的比如你开头就model MyModel()创建了一个实例后面改了MyModel类的定义那个老实例的行为不会变必须重新实例化。别问我怎么知道的。3. 用数据集承载代码库一次写好处处复用3.1 为什么代码要放数据集而不是放 working/kaggle/working有个绕不开的限制它是跟着单个 Notebook 走的。A Notebook 里写的模块B Notebook 拿不到。你可以通过保存版本、从输出里下载、再上传的方式搬运但来回几次就烦了。更优雅的方案是把你的代码库做成一个 Kaggle 数据集。数据集是账号级别的资源可以被任意 Notebook 挂载天然解决了跨 Notebook 复用的问题。而且数据集支持多版本你可以保留历史出问题了回滚。判断标准很简单一次性、只在这一个 Notebook 里用的代码写/kaggle/working用%%writefile落盘。会被多个 Notebook 引用、或者需要版本管理的代码做成数据集。我自己有个my-ml-toolkit的私有数据集里面放了数据校验、特征处理、交叉验证、提交格式化这几类通用代码。每次开新 Notebook右侧添加这个数据集两行代码注入路径整套工具就到位了。这比每次都从零写一遍效率高太多。3.2 制作与挂载从本地到线上制作流程不复杂但有几个细节容易翻车。首先在本地把目录整理成标准的包结构my-ml-toolkit/ ├── __init__.py ├── feature_utils.py ├── validators.py └── submit_utils.py然后打包上传。如果你要做的是「数据集」上传时选择包含代码文件的压缩包或文件夹即可如果你更希望它表现得像一个「代码库」也可以在相应入口上传。上传完成后在 Notebook 右侧的 Add Data 里搜索并添加挂载路径会是/kaggle/input/你的数据集slug/。这里有个命名陷阱。数据集的 slug 和你设置的显示名称可能不一致建议挂载后先跑一句!ls /kaggle/input/确认实际的目录名再往sys.path里加。我见过不少人因为 slug 里带了下划线、连字符的区别而反复报路径错误。另外上传的目录里如果包含__pycache__、.ipynb_checkpoints、.DS_Store这类垃圾文件虽然不影响功能但会让目录看起来乱也白白增加体积。上传前清理一下是个好习惯。3.3 软链接与目录包init.py数据集挂载进来是只读的这意味着你不能往里写__pycache__。Python 在 import 时会尝试生成字节码缓存写入失败通常只是拖慢一点加载速度会有个警告但不影响运行。如果你实在不想看到警告可以用sys.dont_write_bytecode True关掉字节码写入。如果你觉得每次都要手动注入路径太麻烦可以把数据集内容软链接到 working 下!ln -s /kaggle/input/my-ml-toolkit /kaggle/working/toolkit这样/kaggle/working/toolkit就出现在sys.path覆盖范围内了。不过要注意软链接指向的内容依然是只读的改文件还是会失败。软链接的价值主要在于简化路径引用不是解决写权限。关于__init__.py如果你希望模块能以from toolkit.feature_utils import fill_missing这种方式导入那包目录下必须有__init__.py哪怕是空文件。Python 3.3 之后有隐式命名空间包理论上没有__init__.py也能导入但在 Kaggle 这种混合环境下行为不够稳定我建议还是老老实实加上。空文件也行成本极低。3.4 版本迭代与多人协作的约定数据集每次更新都会生成新版本。挂载时默认用最新版但你也可以指定历史版本号。这个机制在团队协作里非常有用把代码库的某个稳定版本固定下来比赛冲刺阶段所有人用同一版避免「你那边跑得通我这边跑不通」的扯皮。具体做法是在 Add Data 时选择固定版本或者在 Notebook 设置里锁定。这样一来即使有人更新了数据集你的 Notebook 也不会被动跟着变。还有一个经验在代码库里维护一个 VERSION 常量。VERSION 1.3.2Notebook 开头打印一下日志里就能看到用的哪版代码。出问题回溯时这条信息能省下很多时间。这不是什么高科技但真正做起来的人不多属于典型的「知道容易做到难」。4. 修改第三方库与非 Python 文件的实战4.1 覆盖 site-packages 里的包有时候你需要改的不仅是自己的代码还有第三方库。比如某个库的默认行为不符合你的需求你只想改其中一行逻辑。在本地你可以直接编辑site-packages里的文件Kaggle 上那些目录通常是只读的会失败。正确的姿势是「影子覆盖」把你修改后的模块版本放到sys.path最前面让它抢在原始版本之前被加载。操作步骤是这样的import sys sys.path.insert(0, /kaggle/working/patched) import some_library print(some_library.__file__)打印出来的路径如果是/kaggle/working/patched/some_library/...说明覆盖成功了。如果还是 site-packages 里的路径那就是insert(0, ...)没生效或者这个库已经被更早的 import 加载进sys.modules了——这种情况下必须重启会话因为模块缓存一旦建立就没法替换。要拿到原库的源码可以先复制一份!cp -r /opt/conda/lib/python3.10/site-packages/some_library /kaggle/working/patched/然后在 working 下随便改。改的时候注意__pycache__目录建议删掉避免旧字节码干扰。注意这种覆盖方式只在当前会话有效重启就没了。如果你需要长期使用把打补丁的模块放进你的代码库数据集里每次挂载后自动复制到 working 再注入路径。我一般会写一个bootstrap.py专门干这件事几个 Notebook 共用逻辑统一。4.2 配置文件、权重、CSV 这类非代码文件模块不只是 .py 文件。配置文件、模型权重、词表、映射表同样需要管理。它们的处理原则和代码不同因为体积往往大得多。对于小的配置类文件JSON、YAML、TXT我习惯用%%writefile或者 Python 的json.dump写在/kaggle/working下和代码放一起import json config { seed: 42, n_folds: 5, model: {hidden: 256, dropout: 0.2} } with open(/kaggle/working/config.json, w, encodingutf-8) as f: json.dump(config, f, ensure_asciiFalse, indent2)对于大文件权重、embedding、大的 parquet就不要重复上传了直接引用原始数据集的挂载路径。但要注意一点挂载进来的文件是只读的如果你想在它上面做修改必须先复制到 working 或 temp。import shutil shutil.copy( /kaggle/input/base-weights/model.bin, /kaggle/working/model.bin )复制大文件时优先考虑/kaggle/temp因为它的读写性能通常更好而且不占用 working 的产物体积。缺点是会话结束后会被清理所以适合中途处理不适合作为最终产物。4.3 写文件时的权限坑input 只读前面反复提到只读这里展开讲一下表现和排查。当你对/kaggle/input下的文件做写操作时会看到明确的报错OSError: [Errno 30] Read-only file system: /kaggle/input/xxx/model.bin如果你在代码里是用with open(path, w)打开报错会更早在打开阶段就失败了。排查思路很直接看报错里的路径前缀。只要路径以/kaggle/input开头就必然是这个原因。有一种情况比较隐蔽某些库会在内部尝试写日志或者写缓存到你传入的目录。比如你传了一个/kaggle/input/xxx作为工作目录给某个训练框架它会在里面建 checkpoint 目录这时候报错信息可能来自框架深处看着莫名其妙。遇到不认识的文件系统错误先检查路径。解决办法统一为把需要写的内容重定向到/kaggle/working或/kaggle/temp只在读取原始数据时才用/kaggle/input。另外Python 临时目录的默认设置也值得改一下import tempfile tempfile.tempdir /kaggle/temp有些库会往系统默认临时目录写不少东西改到/kaggle/temp能避免占用 working 空间也能减少被只读限制波及的概率。这一行代码看起来不起眼但在跑大任务时能省不少事。5. 保存、运行与提交让改动真正落地5.1 保存版本时到底保存了什么Kaggle Notebook 的保存机制需要理解清楚否则你的改动可能白做。点击 Save Version 时通常会走一遍完整执行然后把这次执行的结果快照下来包括Notebook 本身的内容所有单元格的代码和输出/kaggle/working目录下的文件注意第二点只有/kaggle/working下的文件会被保存为版本产物。/kaggle/temp下的东西不会/kaggle/input本来就是只读的也不涉及。这意味着如果你把模块写在了/kaggle/temp下保存版本后它就消失了下次打开 Notebook 从这份快照启动import会直接失败。我自己第一次遇到这个问题时百思不得其解以为是平台 bug后来才明白是目录选错了。所以结论很明确要跟着版本走的文件一律放/kaggle/working。还有一个细节保存版本时可以选择「快速保存」还是「完整运行」。快速保存只记录当前状态不重新执行代码完整运行会从第一个单元格开始跑一遍。如果你在会话里手动改过一些中间变量完整运行后这些临时状态会丢失因为它是从干净状态重新执行的。做最终提交前我建议至少跑一次完整运行确保从头到尾可以复现避免「我这里能跑通」但换个环境就崩的情况。5.2 后台自动运行的配置方式Kaggle 的 Notebook 支持保存为版本后继续在后台运行也就是常说的长时间任务模式。这对训练模型、跑大规模特征工程很有用因为你不需要一直开着浏览器页面。配置方式是在保存版本时勾选相应的选项让这次执行在后台完成。几点实际经验第一后台运行的时长有限制超时会被中断。跑长任务前先估算一下时间把能并行的部分尽量并行或者把任务拆成几段每段保存一次中间结果。第二后台运行期间你看不到实时输出所以要靠日志来观察进度。我会在代码里加定期打印import time for i, batch in enumerate(loader): if i % 100 0: print(f[{time.strftime(%H:%M:%S)}] step {i}, flushTrue)flushTrue很重要不加的话输出会缓冲日志里可能长时间一片空白。第三会话断开后重新连接之前后台运行的变量状态是拿不到的只能从保存的产物里恢复。所以关键中间结果要落盘别只存在内存里。对于需要反复触发的场景也可以考虑用命令行工具来推送 Notebook但这属于进阶用法日常用界面操作完全够了。5.3 提交文件的产出位置如果你是在打比赛最终提交文件必须放在/kaggle/working下通常命名为submission.csv。Kaggle 的提交入口会自动在这个目录里找符合要求的文件。一个容易忽略的点是提交前最好做一次格式校验因为格式不对会在提交环节才报错浪费一次机会。我习惯写个小函数import pandas as pd def check_submission(path, expected_cols, n_rowsNone): df pd.read_csv(path) assert list(df.columns) expected_cols, f列名不符: {list(df.columns)} if n_rows is not None: assert len(df) n_rows, f行数不符: {len(df)} assert df.isnull().sum().sum() 0, 存在空值 print(格式检查通过, df.shape) return df在执行完所有流程、生成 submission 之后立刻调用一次确认没问题再保存版本。这种前置校验的成本极低但能挡掉大量低级错误。另外如果你生成了多个版本的结果最终要提交哪个最好在文件名上区分清楚比如submission_v3.csv然后在最后一步统一复制成submission.csv避免手滑提交了旧版本。6. 常见报错与排查速查6.1 报错对照表把我在 Kaggle 上遇到过的和模块、文件相关的典型问题整理成一张表排查时可以直接对号入座报错信息根本原因解决方式ModuleNotFoundError: No module named xxx模块不在sys.path中注入路径或改用 importlib 加载改代码后行为不变模块缓存在sys.modules%autoreload 2或importlib.reloadOSError: [Errno 30] Read-only file system往/kaggle/input写文件改为写/kaggle/working或tempPermissionError: [Errno 13]路径不存在或指向目录检查路径是否拼错确认文件已上传保存版本后模块没了文件写在了/kaggle/temp迁移到/kaggle/working覆盖第三方库不生效库已被提前 import重启会话后第一时间注入路径No such file or directory但文件明明在slug 与显示名不一致!ls /kaggle/input/确认真实目录名导入本地包报相对导入错误缺少__init__.py补上包结构文件这张表建议存下来。Kaggle 上的报错信息往往比较笼统靠表快速分类能省很多时间。6.2 几个我踩过的坑第一个坑是关于路径拼接的。我习惯用 f-string 拼路径有一次把 slug 里的连字符写成了下划线代码跑了几十分钟在最后一步才报文件找不到白等。后来我改成在 Notebook 开头统一做一次路径校验import os BASE /kaggle/input/my-ml-toolkit assert os.path.isdir(BASE), f路径不存在: {BASE}\n实际内容: {os.listdir(/kaggle/input)}断言里把/kaggle/input的实际内容打出来一眼就能看出名字哪里不对。这个几行的检查帮我省下的时间可能有好几个小时。第二个坑是关于模块内的全局状态。我在一个工具模块里用了一个模块级的缓存字典想着能跨调用复用。结果发现每次跑都像是重新开始的一度以为 autoreload 把我的缓存清了。真实原因是我在不同的单元格里重复执行了 import 相关代码触发了模块重新加载缓存自然就重置了。模块级的可变全局状态在 Notebook 里非常不可靠因为执行顺序是你手动的任意一次重跑都可能重建模块。需要跨单元格共享的状态老老实实用文件或者明确的对象传递。第三个坑是关于中文输出的。模块里如果有打印中文的语句在某些执行模式下可能出现编码问题。统一在文件头部声明并使用 UTF-8 写入能规避大部分情况# -*- coding: utf-8 -*-虽然 Python 3 默认源码是 UTF-8但显式声明不占什么成本遇到奇怪问题时能少一层怀疑。第四个坑更有意思我尝试在一个模块里用多进程加速特征计算结果在 Notebook 里一直卡死。原因是多进程的启动方式在不同环境下行为不同需要注意保护入口代码避免子进程重复执行主流程。Kaggle 的 Notebook 本质是一个交互式环境多进程最好用 fork 方式并且把主逻辑收进if __name__ __main__或者独立的函数里。如果实在跑不通退回到单进程加批处理稳定优先。第五个坑是文件命名冲突。我把自己的模块命名为utils.py结果和某个第三方库的utils撞名了import 进来的是别人的东西排查了很久。给自己模块命名时加个前缀比如fs_utils.py、fs_features.py冲突概率立刻降到接近零。这是成本最低、收益最高的一个习惯。第六个是体积问题。我有一阵喜欢把所有中间产物都往/kaggle/working里放结果保存版本时产物包变得很大保存速度慢有时候还会失败。后来改成中间产物用完就删只留最终需要的。加一句清理代码就行。import os import shutil for p in [/kaggle/working/tmp_cache, /kaggle/working/debug_dump]: if os.path.exists(p): shutil.rmtree(p) print(os.listdir(/kaggle/working))跑完之后打印一下工作目录心里有数也是提交前的一个自检动作。说到底在 Kaggle 上管理自己的模块和文件核心就三件事把可写的代码放对目录让 import 能找到它把需要留存的东西放进版本产物。这三点想通了剩下的都是细节调整。我个人现在的习惯是每个 Notebook 开头固定三段一段配置 autoreload一段注入代码库路径并做存在性校验一段设置临时目录。这三段跑通后面的工作基本就是一马平川了。
返回列表