发布于: Aug 8, 2022

在学习数据采集和预处理之前,我们先来了解一下机器学习的生命周期。如图,一个典型的ML工作流包含从数据标注到最后部署监控,一个复杂的工作流程。从这个图里我们还可以观测到这个ML工作流中有两个迭代,中间的一个迭代发生在模型训练和evaluation之间,这个过程是用来做超参(hyperparameter)调整,避免过拟合(overfitting);另外一个迭代是当模型部署后,持续的获取新的数据,然后根据这些新的数据更新模型的过程。下面我们借助样例,一步一步分开来讲。

ETL 和数据标注

ETL 严格上来讲并不能算 ML 范畴的工作,但是由于数据是 ML 的必需品,同时由于当前企业IT系统复杂,各业务系统通常采用微服务的方式来搭建,导致各个版块乃至各个版块里的不同微服务都用了不同的存储方式来满足各自的业务需求,这导致的结果就是ML 所需要的数据分散在各个不同的业务版块中的不同存储里。想要开展 ML 的工作的第一步就是要把这些散落在不同地方的数据按照 ML 的要求聚合起来。举例来说,我们现在要做的一个房价评估的模型,假设我们认为,房屋的海拔信息、周围的人口密度、收入结构、建筑物的年龄、移动基站的覆盖、手机用户的分配、企业类型等等信息都对这个评估有帮助。显然这些数据都是散落在不同的数据源里的,要把他们聚合,同时按照房屋来进行分组和汇总才行,这样我们最终得到的一张表格中的每一行的不同字段就包含了房屋的海拔、周边人口密度等等信息。这个就是在 ML 的场景下,ETL 要做的事情。

但由于这里我们使用 XGBoost 来评估房价,这属于一种监督学习(supervised learning),所以,仅仅有上面的表格信息还是不够的,我们还缺少一个重要信息就是房屋价格的标注(label 或者 annotation,也有叫target)。这个房价信息的来源可以是实际的成交价,也可以是通过经验丰富的业务人员,人工标注产生的。如果我们能从系统中获取到真实的成交价信息,那么这个标注信息就可以通过上面的ETL来实现,否则,这个标注信息只能依靠有经验的人来实现,ML就是要学习这些有经验的人深藏的、潜在的经验从而实现自动化的评估房价。

如果业务系统是运行在 Amazon Web Services 上的,则可以通过 Amazon Web Services 提供的一些产品来完成这个ETL,这类服务包括 Amazon EMR、Amazon Web Services Glue 等。这类工作本质上属于大数据处理,只不过这个大数据处理服务的对象是 ML,怎么做这个大数据的处理不是 ML 本身要应对的问题,所以,我们假设这个ETL工作已经完成,ETL 以及数据标注的结果已经上传到了 S3 上,如图:

这里有两个文件,一个是 train.csv,一个是 test.csv。train.csv 是真正用来做训练和模型评估的数据。 

数据预处理和特征工程

拿到训练数据为什么不能直接把数据输入到算法里面去做训练呢,这里大致有两种原因:

  • 数据不那么完美。比如训练数据的个别行中,成交价缺失,或者成交价异常,比如单价 50 万/平米
  • 机器本质只能处理数字,现实中的很多数据机器并不能理解它的语义含义,比如不能理解图像的二进制表示的图形化意义;对于业务系统里的一个时间戳1585711438,机器也不能理解它代表的是几月份,是星期几。然而这些时间戳背后代表的星期几、图像二进制数据背后的图形化意义,才是对ML学习和归纳训练数据最有价值的东西。

克服上面这两种问题,对于改善ML最终的推理效果是有特别重要的意义的。克服第一类问题采取的方法叫数据预处理(data preprocessing),克服第二类问题采取的方法叫做特征工程(feature engineering)。

数据探索

在做数据预处理前,我们可以先通过一些手段做一些数据探索(data exploration),这样可以让我们对数据的分布能有一个更好的理解,从而决定如何更好的来利用某一些 feature 来做训练。下面我们来看一在 SageMaker 中如何一步一步来做:

  • 点击 Create notebook instance

给 notebook 设置一个名字,然后选择 Create a new role,给 notebook 绑定一个 IAM Role,这个 role 决定了 notebook 能访问 Amazon Web Services 里面的哪些资源。

  • 创建一个 notebook 使用的 role,然后点击 Create notebook instance
  • 新建 notebook 完成后,点击 Open Jupyter
  • 进入 notebook 后,点击右上角的 New,然后新建一个 conda_python3 的脚本
  • 完成新建后就可以在 notebook 里面开展我们后续的一系列工作了,首先我们安装基本的库:
python
!pip install sagemaker pandas
安装 Amazon SageMaker Python SDK 和 panda(用于处理表格数据)。Amazon SageMaker Python SDK 类似 Amazon Web Services SDK for Python (Boto3),不同之处在于 Boto3 是可以调用 Amazon Web Services 的各种服务的,然而 Amazon SageMaker Python SDK 是专供在 Amazon SageMaker notebook 里面开展一系列的 ML 工作使用的,能利用 Amazon SageMaker 里面更丰富的一些功能,这类功能是不能直接使用 Boto3 来使用的。
之后,我们 import 一些基本的 module,并把 S3 的数据加载到当前目录,做一个简单的数据探索:
import timeimport boto3import sagemakerimport urllibimport pandas as pdfrom sklearn.model_selection import train_test_split
# 获取当前Amazon Web Services region和notebook所绑定的role
role = sagemaker.get_execution_role()
region = boto3.Session().region_name
# 设置数据存放的桶和工作目录
bucket = 'YOUR_S3_BUCKET_NAME'
prefix = 'xgb-housing'
# 可以使用 SageMaker session 来上传下载数据,这里把S3的训练数据下载到当前目录
sm_session = sagemaker.Session()
sm_session.download_data("./", bucket, prefix + '/train.csv')
sm_session.download_data("./", bucket, prefix + '/test.csv')
# 通过describe来对查看一下训练数据feature的构成和分布
X_full=pd.read_csv("./train.csv", index_col='Id')
X_full.describe()
describe 的结果如图所示,我们可以看到训练数据有很多字段构成,同时可以看到每个字段的取值分布情况,比如 LotArea 这个字段我们可以看到取值范围从 1300 到 215245,平均值 10516.828082,以及 25%,50%,75% 的分位数(Quantile)取值,也可以借助一些 matplotlib,seaborn 等图形化的库来更好做这种数据探索,好的数据探索对我们后续做数据预处理、改善模型的准确度都是很有帮助的。

数据预处理

从上面的 describe 结果来看,里面一些 feature 的取值有一些不寻常的特点,比如 BsmtFinSF2 在 75% 的分位数取值是 0,但是最大值是 1474,对于这类情况,我们可以通过进一步的数据探索来评估这种字段的对于我们 ML 的意义,从而评估是否可以在训练的时候丢弃这种 feature,或者说只是针对有奇异值(outlier)的样本做丢弃处理。这里对行或列的丢弃行为就属于数据预处理,除了丢弃这种操作,我们可以尝试对缺失值(missing value)做补全(imputation)。类似的这种数据预处理还包括:

  • 在 NLP 场景中或者图像处理的情况中,如何把数据用向量来表示,比如 text vectorization
  • 在深度学习(Deep Learning)里,我们经常要把一个取值范围很大的 feature 做 Feature scaling。可以根据情况选择做 Standardization(数据转换成 0 到 1  的取值)或Normalization(把数据转换成均值为 0,方差为1的分布)。背后的原因是取值范围过大的数据会影响 loss function 的工作。

上面提到的这些 imputation,vectorization,feature scaling 都是要在数据预处理中完成的,对于当前的数据我们做一个简单的预处理:丢弃没有 SalePrice 的 sample:

# 丢弃没有 SalePrice 的sample
X = pd.read_csv("./train.csv", index_col='Id') 
X.dropna(axis=0, subset=['SalePrice'], inplace=True)
# 提取训练数据中的label列,在原始数据中丢弃label列
y = X.SalePrice
X.drop(['SalePrice'], axis=1, inplace=True)

你可能注意到了,除了丢弃没有 SalePrice 的 sample 以外,我们还做了另外一个处理”提取训练数据中的 label 列,在原始数据中丢弃 label 列“,这是因为 Amazon SageMaker 的内置 XGBoost 算法对于输入数据有两个要求:

  • 训练数据要是把label作为首列,我们这里先把label从原始数据提取了出来,后面还会再把它作为第一列拼接回训练数据中;
  • 训练数据里不能有表头信息(就是诸如 MSSubClass,LotFrontage 这些列名)

另外,为了后面模型训练过程中验证模型的合理性我们还要对原始训练数据做一个拆分。把原始的训练数据拆分成 training set 和 validation set,之所以要这样做,是为了在后续训练过程的调参迭代中不断的优化超参。超参优化的目标就是要使得在 training set 上训练得到的模型能够在 validation set 上有着良好的表现,从而确定最终训练要使用的超参组合。

你也许还听说,除了要拆分出上面那两份以外,还要在拆分出第三份 test set。之所以要有 test set,是因为随着你不断的做超参优化,会导致你不断的把 validation set 中的潜在的信息泄露给算法,从而导致模型对 validation set 形成过拟合,如果出现了这种情况,导致的后果是虽然模型在 validation set 上的表现非常好,然而一旦是用真实数据做预测效果却很差。为了避免这种情况的发生,就需要引入 test set,然后在整个迭代的训练过程中,算法完全不会看到 test set 里面的数据,最后当超参确定后,再用 test set 做一次测试。当我们尝试使用多种算法来解决同一个 ML 问题的时候,使用 test set 做最后的一次测试可以帮我们决定最后到底用哪个算法作为最终的解决方案。由于我们这里并不需要在多种算法中做出选择,我们这里不划分test set,只划分出 training set 和 validation set。sklearn 提供了很多用来做数据拆分的工具,我们这里直接调用它的一个函数来实现这个拆分:

# 拆分原始训练数据为四份,X_train_full, X_valid_full, y_train, y_valid
X_train_full, X_valid_full, y_train, y_valid = train_test_split(X, y, train_size=0.8, test_size=0.2, random_state=0)

特征工程

特征工程的引入是为了让我们的算法更好的理解特征而引入的。使用什么样的手段来做特征工程,跟我们选择什么样的算法也有一定关系,由于这里我们采用的是 XGBoost 算法,我们知道对属性做 one-hot encoding 是有助于算法学习的。你或许会对 one-hot encoding 这个词感到陌生,简单来说,对于一个取值范围可枚举,并且取值是非数值型的属性来说,当我们把它转换成机器可以识别的编码的时候可以考虑使用数字编码的方式来表示。但问题在于通过这种方式,不同的枚举值之间有隐形的大小关系,这种暗含的关系会影响模型的准确性,所以我们表示有 n 个枚举值的属性时,使用 n 个二进制位来表示,当需要表示其中任意一个枚举值 m 的时候,仅仅使第 m 个二进制位是 1,其他的 n-m 个二进制位都是 0。由于 n 个二进制位只有一个不是 0,所以我们把这种转换方式叫做 one-hot encoding。采用 one-hot encoding 来对可枚举的属性编码时,如果属性的取值个数过多,会导致产生过多的数据,在这里我们选择只对取值个数少于10的属性来做 one-hot encoding,具体如下:

# Cardinality 代表的就是某个枚举类型的属性取值范围个数# 这里是要筛选出 Cardinality 小于9的属性
low_cardinality_cols = [cname for cname in X_train_full.columns if X_train_full[cname].nunique() < 10 and X_train_full[cname].dtype == "object"]
# 筛选出数字类型的列
numeric_cols = [cname for cname in X_train_full.columns if X_train_full[cname].dtype in ['int64', 'float64']]
# 把Cardinality小和数字类型的列拼在一起作为全部的列
my_cols = low_cardinality_cols + numeric_cols
# 在training set 和 validation set 中只保留通过上面方法筛选出来的列
X_train = X_train_full[my_cols].copy()
X_valid = X_valid_full[my_cols].copy()
# 做 one-hot encoding
X_train = pd.get_dummies(X_train)
X_valid = pd.get_dummies(X_valid)

这一部分,我们对数据预处理和特征工程做了一个介绍,同时对示例数据做了简单的处理。在这个过程中我们顺带的介绍了数据探索,训练数据集拆分等概念。在实际操作中,数据探索、数据预处理、特征工程这类工作都是最需要发挥数据科学家智慧的过程。要解决的问题不同,要使用的算法不同,都会影响到这类工作采取的具体方法。同时,这类工作也是整个 ML 工作流中最花时间的部分,数据科学家要在 notebook 中使用各种方法来完成训练前的处理,各种方法会运用也会不拘一格,在预处理、特种工程之间也没有明显的界限,也有人把这些工作统统称为数据预处理。作为这些预处理后的下一步就是要把他们送进算法里面来训练了。

相关文章