ARTICLE DETAIL

资讯详情

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

机器学习分类数据编码实战:独热编码原理与应用

机器学习分类数据编码实战:独热编码原理与应用 先把结论放在前面只要你的数据里出现“性别”“城市”“颜色”“学历”这类文本型字段又打算把它们直接丢给机器学习模型训练那十有八九会遇到一个报错或者模型效果奇差。原因是大多数算法只认数值不认字符串。今天这篇是 100 天机器学习计划的第 27 天我们专门来把分类数据的编码问题讲透重点放在高频使用的独热编码上。本文的学习目标很明确理解分类数据的本质、掌握独热编码的底层原理、能手写实现独热编码、熟练使用 pandas 和 scikit-learn 完成编码并且知道训练集和测试集类别不一致、高基数特征、稀疏矩阵这些常见坑怎么绕开。1. 为什么要处理分类数据1.1 分类数据与数值数据的区别在机器学习任务里特征大体可以分成两类数值型特征和类别型特征。数值型特征好理解比如年龄 25 岁、房价 300 万、温度 36.5°C它们本身就有大小和远近的概念。你告诉模型“25 比 30 小”模型能直接通过数值大小参与计算。类别型特征就不是这样了。它描述的是“属于哪个组”比如性别男、女所在城市北京、上海、广州学历高中、本科、硕士、博士商品类目数码、服饰、食品天气晴、多云、雨、雪这些值本身没有数值含义“北京”并不比“上海”大“男”也不等于 1“女”也不等于 0它们只是不同的类别标签。1.2 机器学习为什么不直接吃字符串绝大多数机器学习算法包括线性回归、逻辑回归、支持向量机、神经网络底层都依赖矩阵运算、距离计算和梯度下降。你输入的特征矩阵必须是一个数值矩阵每一个位置上的值都要求是数字否则计算无法进行。举个例子sklearn 的线性回归模型 fit 数据时如果你传入一个包含字符串的 DataFrame通常会直接抛错报错信息大致是ValueError: could not convert string to float: red因为特征矩阵在进入模型前会被强制转成 float 类型字符串无法完成这个转换。所以把一个类别特征转成模型能理解的数值形式是数据预处理里绕不开的一步。而“编码”就是完成这种转换。什么是好的编码方式核心原则是要让模型从数字中读出的规律尽量符合这类数据的真实含义避免人为引入不存在的顺序关系。1.3 分类数据的两种类型名义型与有序型正式开始编码之前需要先分清两个概念名义型数据Nominal类别之间没有天然的大小顺序。比如颜色、城市、品牌。你不能说“红色 蓝色”也不能说“北京 上海”。有序型数据Ordinal类别之间有天然的等级顺序。比如学历“高中 本科 硕士 博士”评分“差 中 好 优秀”。这两种数据适合的编码方式不同。名义型数据适合独热编码有序型数据既可以做独热编码也可以使用标签编码但要保证映射的数字顺序合理。这一点后面会详细展开。2. 环境准备与版本说明2.1 开发环境本文所有示例代码基于 Python 3运行时使用 Jupyter Notebook 或 VS Code 都可以。如果你还没有配置机器学习环境可以按照下面命令安装基础依赖版本不必严格固定以实际能安装成功为准。建议创建独立的虚拟环境避免和系统环境产生依赖冲突python -m venv ml_envWindows 环境下激活虚拟环境ml_env\Scripts\activatemacOS / Linux 环境下激活虚拟环境source ml_env/bin/activate激活后命令行提示符前会出现(ml_env)表示当前已经进入虚拟环境。2.2 依赖库安装本文用到的核心库是 pandas、numpy、scikit-learn。执行如下命令安装pip install pandas numpy scikit-learn安装完毕后可以在 Python 交互环境中验证版本import pandas as pd import numpy as np import sklearn print(pandas:, pd.__version__) print(numpy:, np.__version__) print(scikit-learn:, sklearn.__version__)版本需要根据你的项目实际情况调整本文示例以常见环境为例重点演示编码思路和实现方法。3. 分类编码的常用方式在进入独热编码之前有必要把另外两种编码方式也放在一起对比。因为实际项目中并不是所有分类数据都适合用独热编码你需要能根据业务场景做选择。3.1 标签编码Label Encoding标签编码最简单思路就是把每个类别映射成一个整数。比如颜色列里有 red、green、blue直接映射成 0、1、2。color_map {red: 0, green: 1, blue: 2} text [red, green, blue, green] encoded [color_map[item] for item in text] print(encoded)输出结果[0, 1, 2, 1]标签编码的问题很明显模型会认为 2 比 1 大、1 比 0 大并且 2 和 0 的差距是 2。如果类别本质上是无序的这种人为产生的“大小关系”会误导模型导致拟合结果出现偏差。比如线性模型会认为“蓝色是红色的两倍”这完全没有实际含义。那么标签编码适合什么时候用答案是有序型数据或者树模型场景。决策树、随机森林这类基于划分的模型对特征值的绝对大小不敏感标签编码不会带来严重问题。但线性模型、神经网络、距离类模型就要慎重。3.2 独热编码One-Hot Encoding独热编码的思路完全不同。它不再给类别赋一个单独的数值而是把每个类别扩展成一个二值特征向量向量中只有一个位置是 1其余位置都是 0。还是用颜色举例。原始数据列颜色redgreenbluegreen独热编码后变成颜色_red颜色_green颜色_blue100010001010可以看到每一行只会有一个 1其他全部是 0。这种做法的好处是类别之间不再有大小关系模型不会误以为 red 和 green 之间存在“数值距离”。独热编码的缺点是维度会膨胀。如果某个特征有 100 个类别就会生成 100 个新特征导致数据变稀疏、内存占用增加。这个问题在高基数特征场景下尤其明显后面会单独讨论。3.3 序数编码Ordinal Encoding如果你的分类数据本身存在明确顺序比如学历、评分等级可以考虑使用 Ordinal Encoding它本质上就是标签编码但要求你显式指定顺序。from sklearn.preprocessing import OrdinalEncoder education [[高中], [本科], [硕士], [博士], [本科]] encoder OrdinalEncoder(categories[[高中, 本科, 硕士, 博士]]) encoded encoder.fit_transform(education) print(encoded)输出结果[[0.] [1.] [2.] [3.] [1.]]注意这里的 0、1、2、3 是有业务含义的顺序必须和真实等级一致。如果你把顺序搞反模型学到的东西就是错的。下面用一张表总结三种编码方式的核心区别编码方式适用数据是否引入顺序关系特征维度变化标签编码 Label Encoding有序型数据会引入不变独热编码 One-Hot Encoding名义型数据不会引入维度膨胀序数编码 Ordinal Encoding有序型数据按给定顺序引入不变4. 独热编码的实现方式独热编码看着简单但写起来有不少细节。这一节我们先手写一个最小实现再从 pandas 和 sklearn 两个角度给出标准做法。4.1 手写实现独热编码为了真正理解独热编码先不要急着调库。假设我们有一个简单列表data [red, green, blue, green]手写实现的思路分三步找到所有唯一的类别。为每个类别创建一列特征。遍历原始数据在当前样本所属类别对应的列填 1其余填 0。def one_hot_encode(data): unique_values sorted(set(data)) result [] for item in data: row [0] * len(unique_values) row[unique_values.index(item)] 1 result.append(row) return unique_values, result data [red, green, blue, green] categories, encoded one_hot_encode(data) print(类别:, categories) for original, row in zip(data, encoded): print(original, row)输出结果类别: [blue, green, red] red [0, 0, 1] green [0, 1, 0] blue [1, 0, 0] green [0, 1, 0]这个实现可以帮助你理解独热编码的本质类别被映射成一个高维稀疏向量。4.2 pandas.get_dummies 快速编码实际数据处理中我们很少手写而是直接使用 pandas 提供的 get_dummies 函数。它可以把 DataFrame 里的所有类别列一次性转成独热编码。import pandas as pd df pd.DataFrame({ color: [red, green, blue, green], size: [S, M, M, L], price: [100, 150, 200, 120] }) print(原始数据) print(df)输出结果原始数据 color size price 0 red S 100 1 green M 150 2 blue M 200 3 green L 120调用 get_dummiesdf_encoded pd.get_dummies(df, columns[color, size]) print(df_encoded)输出结果price color_blue color_green color_red size_L size_M size_S 0 100 0 0 1 0 0 1 1 150 0 1 0 0 1 0 2 200 1 0 0 0 1 0 3 120 0 1 0 1 0 0这里有几个关键参数值得说明columns 参数用来指定哪些列需要编码。如果不指定pandas 会自动把所有 object 类型或 category 类型列都转成独热编码容易造成意外维度膨胀。dtype 参数可以控制输出类型。默认输出 uint8内存占用比 int64 小很多。drop_first 参数可以删除第一列用于处理多重共线性问题后面会细说。4.3 sklearn.OneHotEncoder 标准方案pandas 的 get_dummies 适合快速分析和探索性数据处理但如果你想构建一个可复用的机器学习流水线更推荐使用 sklearn 的 OneHotEncoder。它支持 fit 和 transform 分离可以保证训练集和测试集使用完全相同的编码规则。基础用法import pandas as pd from sklearn.preprocessing import OneHotEncoder df pd.DataFrame({ color: [red, green, blue, green] }) encoder OneHotEncoder() encoded encoder.fit_transform(df[[color]]) print(encoded.toarray())输出结果[[0. 0. 1.] [0. 1. 0.] [1. 0. 0.] [0. 1. 0.]]注意fit_transform 返回的是一个稀疏矩阵打印前需要调用 toarray() 转成普通数组。这个设计是为了节省内存因为独热编码后的矩阵绝大多数位置都是 0。查看编码后的类别print(encoder.categories_)输出结果[array([blue, green, red], dtypeobject)]OneHotEncoder 还有几个重要参数handle_unknown默认为 error表示 transform 遇到训练集里没出现过的类别时直接报错改成 ignore 后会忽略未知类别将整行向量全部置为 0。这个参数在后面讲类别不一致问题时会用到。drop可以用来去掉某一列常用于避免多重共线性。传入 first 表示删除每类编码中的第一列效果和 get_dummies 的 drop_first 一致。5. 完整机器学习实战案例概念掌握之后我们用一个完整的案例把编码放进机器学习流程里这样可以更直观地看到分类数据编码前后模型效果的变化。5.1 案例背景假设你拿到一份员工离职预测数据特征是部门 department、职级 level、城市 city标签是是否离职 resign。模型使用逻辑回归因为逻辑回归属于线性模型对特征数值很敏感非常适合用来演示独热编码的作用。5.2 创建项目结构在本地创建一个示例项目目录结构如下onehot_demo/ ├── data.py ├── train.py └── requirements.txt其中 data.py 用来构造一个可复现的模拟数据集train.py 用来完成编码和模型训练。如果是在 Jupyter Notebook 中运行也可以把两个文件的内容合并到同一个 Notebook 单元格中不影响学习效果。5.3 数据准备与编码先构造模拟数据。这里为了便于演示只生成少量样本但已经足够说明编码流程# 文件路径onehot_demo/data.py import pandas as pd import numpy as np np.random.seed(42) departments [技术部, 市场部, 产品部, 运营部] cities [北京, 上海, 广州, 深圳] levels [1, 2, 3, 4, 5] n_samples 200 data { department: np.random.choice(departments, sizen_samples), city: np.random.choice(cities, sizen_samples), level: np.random.choice(levels, sizen_samples), resign: np.random.choice([0, 1], sizen_samples, p[0.7, 0.3]) } df pd.DataFrame(data) print(df.head())运行后你会看到一个包含 5 列的 DataFrame其中 department 和 city 是字符串level 是整数resign 是二分类标签。这里有个容易混淆的点level 本身用数字表示 1 到 5但它在业务上属于有序型类别不是连续数值。你可以选择将它当成数值特征也可以当成有序类别处理。为了突出分类编码我们暂时把它当成数值特征保留只对 department 和 city 做独热编码。接着进行编码from sklearn.preprocessing import OneHotEncoder from sklearn.compose import ColumnTransformer from sklearn.model_selection import train_test_split X df.drop(resign, axis1) y df[resign] X_train, X_test, y_train, y_test train_test_split(X, y, test_size0.2, random_state42) categorical_cols [department, city] preprocessor ColumnTransformer( transformers[ (cat, OneHotEncoder(handle_unknownignore), categorical_cols) ], remainderpassthrough ) X_train_encoded preprocessor.fit_transform(X_train) X_test_encoded preprocessor.transform(X_test) print(训练集编码后形状:, X_train_encoded.shape) print(测试集编码后形状:, X_test_encoded.shape)ColumnTransformer 的作用是只对指定的类别列做编码其他列原样保留。这样写的好处是数据和编码逻辑分离代码维护起来更清爽。输出结果训练集编码后形状: (160, 9) 测试集编码后形状: (40, 9)这里的 9 个特征由 8 个独热编码列和 1 个 level 列组成。5.4 构建机器学习流水线接下来把预处理和模型训练过程串成一个完整的 Pipeline# 文件路径onehot_demo/train.py from sklearn.pipeline import Pipeline from sklearn.linear_model import LogisticRegression from sklearn.compose import ColumnTransformer from sklearn.preprocessing import OneHotEncoder from sklearn.model_selection import train_test_split from sklearn.metrics import accuracy_score from data import df categorical_cols [department, city] preprocessor ColumnTransformer( transformers[ (cat, OneHotEncoder(handle_unknownignore), categorical_cols) ], remainderpassthrough ) model Pipeline(steps[ (preprocessor, preprocessor), (classifier, LogisticRegression(max_iter1000)) ]) X df.drop(resign, axis1) y df[resign] X_train, X_test, y_train, y_test train_test_split(X, y, test_size0.2, random_state42) model.fit(X_train, y_train) y_pred model.predict(X_test) print(测试集准确率:, accuracy_score(y_test, y_pred))Pipeline 的好处是fit 时先对训练集做编码拟合predict 时自动对测试集做相同的变换不会出现手工处理测试集时遗漏类别映射的问题。运行这个脚本预期输出测试集准确率: 0.725准确率不高是正常的因为模拟数据本身没有刻意构造特征和标签之间的强关系这里重点看流程能不能跑通。5.5 结果说明如果做一个对比实验把 department 和 city 直接删掉只用 level 训练逻辑回归准确率会下降直接把字符串特征丢给模型则会直接报错。通过这个对比可以看出独热编码在分类特征进入线性模型的过程中起到了不可替代的作用。你也可以继续优化更换模型、调整类别粒度、增加更多有效特征这些在真实项目中都属于特征工程的一部分。编码只是第一步特征的质量才是决定模型上限的关键。6. 常见问题与排查思路独热编码写起来不难但实际项目中经常会遇到下面这些情况。我整理了一份高频问题排查表你可以直接对照处理。问题现象常见原因解决思路sklearn 报错could not convert string to float特征矩阵中包含未处理的字符串列对类别列执行标签编码或独热编码确认 DataFrame 中不再有 object 列测试集准确率明显偏低训练集和测试集类别分布不一致或者测试集出现训练集没有的类别使用 handle_unknownignore并保证编码器只用训练集 fit维度爆炸内存不足类别特征基数太高独热编码后生成过多稀疏列降频合并、特征哈希、嵌入编码或改用树模型训练时出现多重共线性警告独热编码保留了 k 列模型输入特征之间存在线性相关使用 dropfirst 或 drop_firstTrue删除冗余列模型出现“未知类别”报错transform 时遇到训练集中未出现的类别设置 handle_unknownignore或者提前清洗数据pandas 自动把所有字符串列编码了get_dummies 未指定 columns 参数手动指定 columns避免无意义列被编码下面再补充几个场景的详细说明。场景一训练集和测试集类别不一致这是编码阶段最常见的问题。假设训练集颜色列只有 red、green测试集出现了 blue。使用 OneHotEncoder 时默认 handle_unknownerror会直接报错ValueError: Found unknown categories [blue] in column 0 during transform解决办法有两种。第一种是设置 handle_unknownignore编码器会自动把未知类别对应的向量全部置为 0。这种处理适合类别本身无顺序、且未知类别出现频率较低的场景。第二种是清洗数据把测试集中的未知类别映射成“其他”。场景二get_dummies 后特征列对不齐如果你在训练集和测试集上分别调用 get_dummies新测试数据里某个类别没出现编码后的列就会比训练集少。解决方法是先 concat 再拆分或者直接用 sklearn 的 OneHotEncoder因为 OneHotEncoder 会对训练集和测试集使用相同的 categories。场景三稀疏矩阵转稠密后内存溢出独热编码后的稀疏矩阵在调用 toarray() 之后会变成大量 0 组成的高维矩阵样本量大时很容易把内存打满。如果只是训练模型可以保持稀疏格式很多 sklearn 模型都支持直接输入稀疏矩阵只有在需要做特征可视化或人工查看时才转稠密。7. 最佳实践与工程建议7.1 什么时候用独热编码独热编码不是万能方案。我的建议是线性模型、神经网络、距离类模型优先使用独热编码因为这类模型希望看到真实的类别分量不希望人为引入顺序关系。树模型决策树、随机森林、XGBoost、LightGBM可以不用独热编码直接用标签编码或原始类别特征。树模型基于条件划分独热编码会带来特征分裂不均衡的问题还可能无谓地增加计算量。有序型特征优先使用 Ordinal Encoding而不是独热编码。因为独热编码会把“学历”拆成多个无关维度丢失顺序信息。7.2 高基数特征怎么处理当一个类别特征有几十上百个类别时独热编码生成的特征列会非常多稀疏度也急剧上升。这种情况下有几种常见处理思路。第一种方式是低频类别合并。统计每个类别的出现频率把出现次数很少的类别统一合并为“其他”。这样可以在保留主要类别信息的同时控制特征维度。def merge_low_frequency(df, col, threshold10): value_counts df[col].value_counts() rare_categories value_counts[value_counts threshold].index df[col] df[col].apply( lambda x: 其他 if x in rare_categories else x ) return df第二种方式是特征哈希。把类别通过哈希函数映射到固定维度的向量解决维度爆炸问题但可解释性会下降。sklearn 的 HashingVectorizer 就是这种思路它主要面向文本数据但思想可以迁移到类别特征上。第三种方式是目标编码Target Encoding用类别对应的目标均值替代类别本身。这种方式编码方便但很容易造成数据泄漏必须配合交叉验证使用新手不建议优先使用。7.3 防止数据泄漏这是特征工程里特别容易踩的坑。无论使用哪种编码器都只能对训练集进行 fit再对测试集进行 transform。如果先对全量数据 fit 编码器再把数据分成训练集和测试集测试集的信息就会通过编码器泄漏到训练过程中导致模型评估结果虚高。正确做法是使用 sklearn 的 Pipeline把编码和建模放进同一个流程让 fit 和 transform 的职责自然分离。7.4 避免多重共线性线性模型对特征共线性比较敏感。独热编码如果保留所有 k 个类别列就会产生 k-1 重共线性因为“某一类为 1”可以从其他列推导出来。解决方法是删除一列encoder OneHotEncoder(dropfirst)或者使用 pandaspd.get_dummies(df, columns[color], drop_firstTrue)7.5 编码后特征命名与可解释性在真实项目里我们往往需要知道每个特征对应原始数据的哪个属性。使用 pandas 的 get_dummies 时列名默认是“原始列名_类别值”比如 color_red比较直观。使用 sklearn 时可以调用 get_feature_names_out 方法查看feature_names encoder.get_feature_names_out([color, size]) print(feature_names)这样做的好处是当你需要把模型结果输出给业务方或者排查某个特征的重要性时可以明确对应关系。8. 第27天小结与下一步学习方向今天重点掌握了三件事第一分类数据必须先编码成数值才能进入机器学习模型。名义型数据推荐独热编码有序型数据可以使用序数编码。第二独热编码的核心思想是把每个类别扩展成独立二值特征保证类别之间没有大小关系。实现方式上有手写实现、pandas.get_dummies、sklearn.OneHotEncoder工程推荐使用 OneHotEncoder 配合 Pipeline。第三实操中要重点防范类别不一致、维度爆炸、多重共线性、数据泄漏这几个高频问题。接下来你可以进一步学习 ColumnTransformer 在复杂特征工程中的应用也可以对比实验一下在决策树模型上使用原始标签编码和使用独热编码的效果差异。建议把你自己的一个小数据集拿出来先做一遍独热编码再尝试换成标签编码观察模型效果变化。只有亲手跑一遍才能真正理解编码方式对模型的影响。如果这篇文章对你有帮助记得收藏备用也可以把它分享给同样正在学习机器学习的朋友。第 28 天我们继续。
返回列表