ARTICLE DETAIL

资讯详情

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

Made-With-ML data 模块解析:基于 Ray Data 的分布式文本预处理流水线,从 CSV 到微调就绪张量

Made-With-ML data 模块解析:基于 Ray Data 的分布式文本预处理流水线,从 CSV 到微调就绪张量 Made-With-ML data 模块解析基于 Ray Data 的分布式文本预处理流水线从 CSV 到微调就绪张量【免费下载链接】Made-With-MLLearn how to develop, deploy and iterate on production-grade ML applications.项目地址: https://gitcode.com/gh_mirrors/ma/Made-With-ML本文围绕 Made-With-ML 项目的 data 模块API 参考页对应源码 madewithml/data.py展开系统讲解该项目如何用一个文本分类任务为 GitHub 开源项目预测领域标签构建完整的分布式数据处理流水线从ray.data.read_csv加载、按类别分层切分stratify_split、文本清洗clean_text、SciBERT 分词tokenize/preprocess到可持久化、可复用的CustomPreprocessor。读完本文你可以掌握在 Ray Data 上实现「训练集拟合 → 全量转换 → 与训练/评估/服务各阶段共享同一份预处理状态」的工程实践并能直接复用这套模式处理自己的非结构化数据。1. 任务背景与数据集Made-With-ML 是一个「设计 · 开发 · 部署 · 迭代」生产级 ML 应用的教学仓库README.md其核心案例是基于 GitHub 项目标题与描述文本预测该项目所属的领域标签。数据文件 datasets/dataset.csv 的表结构为列名说明id项目唯一标识created_on项目创建时间title项目标题description项目描述tag领域标签取值于computer-vision、natural-language-processing、mlops、other四类从 tests/data/test_dataset.py 的 Great Expectations 用例可以看出标签集合恰好是这 4 个值且测试还校验了id唯一性、(title, description)复合键唯一性防数据泄漏、tag非空等数据质量约束。仓库另提供 datasets/holdout.csv 作为评估留出的独立集合。data.py中的全部函数都围绕这条链路设计加载 → 分层切分 → 清洗/分词/标签编码 → 以 Ray Dataset 形式流转给训练、评估与服务。所有依赖版本在 requirements.txt 中锁定ray[air]2.7.0、transformers4.28.1、scikit-learn1.2.2、pandas2.0.1、torch2.0.0以下行为均以该版本组合为准。2. load_dataRay Data 加载与确定性采样def load_data(dataset_loc: str, num_samples: int None) - Dataset: ds ray.data.read_csv(dataset_loc) ds ds.random_shuffle(seed1234) ds ray.data.from_items(ds.take(num_samples)) if num_samples else ds return dsmadewithml/data.py#L14-L27三个关键设计点入口即 Ray Datasetray.data.read_csv支持本地路径或 URLREADME 中的训练命令即用DATASET_LOC指向 CSV 的远程地址数据以 block 形式分布式持有而不是整表载入单机内存固定种子seed1234的全局 shuffle让「前 N 条」采样不依赖文件顺序降低偏置num_samples采样实现ds.take(num_samples)在 shuffle 后取前 N 条再经ray.data.from_items重新包成 Ray Dataset。这在开发迭代时非常实用——train.py中通过train_loop_config[num_samples]透传该参数madewithml/train.py#L215小样本即可跑通全流程。对应测试 tests/code/test_data.py#L21-L24 验证了load_data(..., num_samples10)后ds.count() 10。3. stratify_split在分布式数据集上做分层切分单机场景下分层切分通常用sklearn.model_selection.train_test_split的stratify参数一步完成但 Ray Dataset 是分布式惰性结构不能直接套用。stratify_split的做法是按类别分组后在组内切分madewithml/data.py#L30-L74def stratify_split(ds, stratify, test_size, shuffleTrue, seed1234): def _add_split(df): train, test train_test_split(df, test_sizetest_size, shuffleshuffle, random_stateseed) train[_split] train test[_split] test return pd.concat([train, test]) def _filter_split(df, split): return df[df[_split] split].drop(_split, axis1) # 1) 按类别分组每组内独立做 train/test 切分并打标记 grouped ds.groupby(stratify).map_groups(_add_split, batch_formatpandas) # 2) 两条 map_batches 流水线分别抽取出 train / test 子集 train_ds grouped.map_batches(_filter_split, fn_kwargs{split: train}, batch_formatpandas) test_ds grouped.map_batches(_filter_split, fn_kwargs{split: test}, batch_formatpandas) # 3) 各自再 shuffleRay 要求 train_ds train_ds.random_shuffle(seedseed) test_ds test_ds.random_shuffle(seedseed) return train_ds, test_ds参数说明stratify是用于分层的列名本仓库固定传tagtest_size是测试集比例train.py传0.2shuffle/seed透传给 sklearn 的train_test_split。实现上分三步groupby(tag).map_groups让每个类别块独立落进_add_split组内调用 sklearn 按test_size切分并写入临时列_split随后对同一份grouped结果做两次map_batches(_filter_split)按_split值过滤出训练集与测试集并删掉临时列最后两个 split 各做一次random_shuffle源码注释标注这是 Ray 侧的必需步骤。分层正确性由 tests/code/test_data.py#L27-L34 保证构造c1/c2各 10 条的样本test_size0.5切分后断言两个 split 的value_counts完全相等——即每一类的训练/测试占比一致。4. clean_text正则驱动的文本清洗clean_textmadewithml/data.py#L77-L101对单个字符串执行一条固定的正则流水线def clean_text(text: str, stopwords: List STOPWORDS) - str: text text.lower() # 去停用词词边界匹配 pattern re.compile(r\b( r|.join(stopwords) r)\b\s*) text pattern.sub( , text) # 符号两侧加空格 → 去非字母数字 → 压缩连续空格 → 去首尾空白 → 去链接 text re.sub(r([!\#$%()*\,-./:;?\\\[\]^_{|}~]), r \1 , text) text re.sub([^A-Za-z0-9], , text) text re.sub( , , text) text text.strip() text re.sub(rhttp\S, , text) return text执行顺序值得注意先小写化停用词用单词边界\b...\b编译成一条大正则统一替换避免误伤子串例如停用词you不会删除yours之外的词干测试用例(hi yous, [you], hi yous)专门验证了这一点见 tests/code/test_data.py#L37-L46随后符号加空格、剥离非字母数字字符、压缩多余空格。从源码结构看最后一步http\S链接移除位于非字母数字过滤之后此时 URL 中的://等字符已被转为空格该步骤更多起到兜底防御作用。停用词表不是内联在data.py而是集中定义在 madewithml/config.py#L70-L250 的STOPWORDS约 180 个常见英语词并作为clean_text的默认参数注入——这样配置与逻辑解耦需要替换词表时只改一处。5. tokenize 与 preprocess清洗 → 特征工程 → 分词 → 标签编码清洗后的文本要交给模型。tokenizemadewithml/data.py#L104-L115使用与后续微调目标一致的 SciBERT 分词器def tokenize(batch: Dict) - Dict: tokenizer BertTokenizer.from_pretrained(allenai/scibert_scivocab_uncased, return_dictFalse) encoded_inputs tokenizer(batch[text].tolist(), return_tensorsnp, paddinglongest) return dict(idsencoded_inputs[input_ids], masksencoded_inputs[attention_mask], targetsnp.array(batch[tag]))要点paddinglongest表示 batch 内只 pad 到最长序列而非统一max_len减少无效计算输出统一为input_ids、attention_mask与目标数组三个键。notebooks/madewithml.ipynb 中说明选择该 tokenizer 是因为模型阶段会直接微调同一预训练模型scibert保证词表一致。preprocessmadewithml/data.py#L118-L134则是单个 pandas batch 上的完整处理函数def preprocess(df: pd.DataFrame, class_to_index: Dict) - Dict: df[text] df.title df.description # 特征工程拼接标题与描述 df[text] df.text.apply(clean_text) # 清洗 df df.drop(columns[id, created_on, title, description], errorsignore) df df[[text, tag]] # 只保留必要列 df[tag] df[tag].map(class_to_index) # 标签编码 outputs tokenize(df) return outputs # {ids, masks, targets}其中class_to_index是「类别名 → 整数下标」的映射来自CustomPreprocessor.fit下一节。测试 tests/code/test_data.py#L49-L52 断言输入只有title/description/tag时输出恰好为{ids, masks, targets}。6. CustomPreprocessorfit 一次、处处 transform 的可复用状态裸函数无法携带「从训练集学来的状态」仓库因此封装了 CustomPreprocessorclass CustomPreprocessor: Custom preprocessor class. def __init__(self, class_to_index{}): self.class_to_index class_to_index or {} # mutable defaults self.index_to_class {v: k for k, v in self.class_to_index.items()} def fit(self, ds): tags ds.unique(columntag) self.class_to_index {tag: i for i, tag in enumerate(tags)} self.index_to_class {v: k for k, v in self.class_to_index.items()} return self def transform(self, ds): return ds.map_batches(preprocess, fn_kwargs{class_to_index: self.class_to_index}, batch_formatpandas)设计要点fit只依赖训练集ds.unique(columntag)取出训练集中出现的类别并枚举为class_to_index同时构建反向映射index_to_class。notebooks/madewithml.ipynb 中明确指出这类「只在训练 split 上学习的局部预处理」必须先切分、后 fit否则会造成数据泄漏transform是纯函数式映射把preprocess以fn_kwargs注入map_batches在 Ray 的分布式 batch 上并行执行返回一个「惰性」的新 Dataset此时并未真正计算可序列化重建构造器支持直接传入class_to_index使得下游阶段无需重新 fit 即可还原同一份映射。这个类在项目内被完整复用形成「fit 一次、处处 transform」的调用链训练madewithml/train.py#L214-L240 中load_data→stratify_split(ds, stratifytag, test_size0.2)→preprocessor.fit(train_ds)→ 对 train/val 分别transform并materialize()最后把{class_to_index: preprocessor.class_to_index}作为metadata传入TorchTrainer随 checkpoint 一并保存调参流程 madewithml/tune.py#L79-L105 采用完全相同的步骤推理/服务madewithml/predict.py#L72-L74 的from_checkpoint从 checkpoint metadata 中取回class_to_index重建CustomPreprocessor再用preprocessor.transform(ds)得到ids/masks/targets最终通过index_to_class把概率 argmax 还原为类别名评估madewithml/evaluate.py#L130-L136 同样用transform预处理后select_columns(cols[targets])取真值并按class_to_index输出分类别指标。端到端正确性由 tests/code/test_data.py#L55-L60 守护fit后len(class_to_index) 4与数据集的 4 类一致且transform前后ds.count()不变不丢样本。7. 与训练循环衔接为什么需要 collate_fn 重新 padding预处理输出的ids/masks是按 Ray Data 的大 batch 做paddinglongest的而训练时的 batch 更小train.py默认--batch-size 256不同样本的 padding 长度不一致无法直接堆叠成张量。notebooks/madewithml.ipynb 的解法是自定义collate_fndef pad_array(arr, dtypenp.int32): max_len max(len(row) for row in arr) padded_arr np.zeros((arr.shape[0], max_len), dtypedtype) for i, row in enumerate(arr): padded_arr[i][:len(row)] row return padded_arr def collate_fn(batch): batch[ids] pad_array(batch[ids]) batch[masks] pad_array(batch[masks]) dtypes {ids: torch.int32, masks: torch.int32, targets: torch.int64} return {k: torch.as_tensor(v, dtypedtypes[k], deviceget_device()) for k, v in batch.items()}训练步通过ds.iter_torch_batches(batch_size..., collate_fncollate_fn)消费数据即由 Ray 负责分布式产出 batch、collate_fn负责右侧重 padding 并转换到目标设备与 dtype。此外train.py通过ray.data.ExecutionOptions(preserve_orderTrue)设置DataConfigmadewithml/train.py#L220-L222保证样本顺序在算子重排后依然确定。这也解释了为什么stratify_split结尾必须random_shuffle——Ray Data 要求参与训练的数据经过 shuffle。8. 测试体系与可复现性小结data模块的行为由两层测试保障代码层tests/code/test_data.py覆盖load_data的采样条数、stratify_split的类分布均衡性、clean_text的参数化用例含停用词词边界、preprocess的输出键集合以及fit/transform的类别数与样本守恒数据层tests/data/test_dataset.py用 Great Expectations 校验 schema、标签取值域、复合键唯一性泄漏检查等运行方式为pytest --dataset-loc$DATASET_LOC tests/data见 README.md Testing 一节。全模块的可复现性依赖若干固定约定加载与切分统一使用seed1234类别映射由训练集unique结果的枚举顺序决定因此同一训练集多次运行会得到一致编码编码结果随 checkpoint metadata 持久化使训练、评估、推理、服务四个阶段共享同一份预处理状态而不需要重新拟合。适用前提以上结论基于当前仓库锁定的依赖组合ray[air]2.7.0、transformers4.28.1等见 requirements.txtray.data的 API 在不同 Ray 大版本间可能有差异若在升级 Ray 的环境中复用这套代码需要先核对map_groups/map_batches的行为是否一致。整体而言data.py展示了一个可直接迁移的模式把清洗、分词、编码封装进 batch 级函数用fit/transform隔离训练集专属状态再用 Ray Data 的惰性流水线把预处理天然分布化。【免费下载链接】Made-With-MLLearn how to develop, deploy and iterate on production-grade ML applications.项目地址: https://gitcode.com/gh_mirrors/ma/Made-With-ML创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表