误差计算
均方根误差(RMSE)计算对应欧几里得范数的平方和的根,也称作 l2 范数。
$$RMSE(X, h) = \sqrt{\frac{1}{m}\sum_{i=1} ^m(h(x)^{(i)} - y^{(i)})^2} $$
平均绝对误差(MAE)计算对应 l1 范数的绝对值和,也成为曼哈顿范数,因为其测量了城市中的两点,沿着矩形的边行走的距离。
$$MAE(X, h) = \frac{1}{m}\sum_{i=1}^m|h(x^{(i)}) - y^{(i)}|$$
lk 范数定义如下,其中 l0 显示向量的基数(非零元素个数),l∞ 向量中最大的绝对值。
$$||V||_j = (|v_0|^k + |v_1|^k + \cdots + |v_n|^k)^{\frac{1}{k}}$$
范数的指数越高,就越关注大的值而忽略小的值,这就解释了为什么 RMSE 比 MAE 对异常值更敏感。当异常值是指数分布(类似正态曲线),RMSE 就会表现很好。
创建测试集
datapath = "C://Users/LENOVO/Desktop/book_need_reading/sklearn&tensorflow/data/housing.csv" housing = pd.read_csv(datapath)
使用 python 的 np.random.permutation 方法可以保证原数组顺序的情况下打乱生成新数组,然后取前 20% 作为测试集,剩余的做训练集。但是这个方法在每次重新运行时,会生成新的测试集,这样机器会记住整个数据集,不利于模型泛化。
def split_train_test(data, test_ratio): shuffled_indices = np.random.permutation(len(data))#np.random.shuffl 无返回值会将原来数据打乱 test_set_size = int(len(data) * test_ratio) #np.random.permutation 返回打乱的新数组的编号,原数组不变 test_indices = shuffled_indices[:test_set_size] #这里只取出了数据在data中的位置值,并没有取出实际的数据内容[1,5,7] train_indices = shuffled_indices[test_set_size:] return data.iloc[train_indices], data.iloc[test_indices] #这里才从data中根据数字索引取出实际值返回 train_set, test_set = split_train_test(housing, 0.2) print(len(train_set), "train +", len(test_set), "test")
当然可以将生成的测试集另外保存下来或者生成随机数生成器的种子 np.random.seed(42) 来避免以上问题。但是当数据集更新后,此方法又失效了。
可以使用每个实例 ID 的哈希值来解决,只保留最后一个字节,若值小于等于 51 (约为256的20%)就放入测试集。这样即使数据集更新了,新的测试集也会包含新实例中的 20%,但不会又之前位于训练集中的实例。
def test_set_check(identifier, test_ratio, hash): return hash(np.int64(identifier)).digest()[-1] < 256 * test_ratio #返回最后一字节小于256为真,大于等于为假的索引 def split_train_test_by_id(data, test_ratio, id_column, hash=hashlib.md5): ids = data[id_column] in_test_set = ids.apply(lambda id_:test_set_check(id_, test_ratio, hash)) #lambda原型为:lambda 参数:操作(参数), map(f,["变量依次输入的数组"]) print(type(in_test_set)) return data.loc[~in_test_set], data.loc[in_test_set] #apply(f,*args,**kwargs,axis=1)args是一个包含按照函数所需参数传递的位置参数的一个元组,默认以列为单位,axis=1按行。 housing_with_id = housing.reset_index() train_set, test_set = split_train_test_by_id(housing_with_id, 0.2, "index")
sklearn 中提供了类似作用函数 train_test_split ,其中 random_state 参数可以设定随机生成器种子,且可以将种子传递给多个行数相同的数据集,可在相同索引上分割数据集,这对于标签和数据分布在不同的 DataFrame 中的数据集来说很方便。
train_set, test_set = train_test_split(housing, test_size=0.2, random_state=42)
数据规律探索和可视化
#分层采样,且不能分太多层 housing["income_cat"] = np.ceil(housing["median_income"] / 1.5) housing["income_cat"].where(housing["income_cat"] < 5, 5.0, inplace=True) #where函数在pandas中的用法,和在numpy中有区别 #根据收入分类,进行分层采样 split = StratifiedShuffleSplit(n_splits=1, test_size=0.2, random_state=42) for train_index, test_index in split.split(housing, housing["income_cat"]): strat_train_set = housing.loc[train_index] strat_test_set = housing.loc[test_index] #恢复数据 for set in (strat_train_set, strat_test_set): set.drop(["income_cat"], axis=1, inplace=True) #创建副本,避免污染训练集 hous = strat_train_set.copy()
地理数据可视化,分别以经纬度为坐标轴,可以看出数据密集度的高低,便于发现规律。
#地理数据可视化 hous.plot(kind="scatter", x="longitude", y="latitude", alpha=0.1) #画出散点图,alpha是透明度 plt.show()
房价可视化,圆圈半径表示街区人口,颜色表示价格(从蓝色低价到红色高价)。从图中可以看出房价和位置以及人口密切相关,但北加州海岸区的房价不是很高,所以并不是一个简单的规则就能描述这个问题。
#房价可视化 hous.plot(kind="scatter", x="longitude", y="latitude", alpha=0.4, figsize=(10,7), s=hous["population"]/100, label="population", c="median_house_value", colormap=plt.get_cmap("jet"), colorbar=True, sharex=False) plt.legend() plt.show()
查找相关性
使用 corr() 方法计算出每对属性间的皮尔逊相关系数。可看出,纬度和房价轻微负相关,越往北,房价可能越低。相关系数只测量线性关系,可能会完全忽略非线性关系。
#查找每个属性和房价的相关性 corr_matrix = hous.corr() scc = corr_matrix["median_house_value"].sort_values(ascending=False) #相关系数的范围是-1~1,接近1是强正相关,接近-1是强负相关。 print(scc) median_house_value 1.000000 median_income 0.687160 total_rooms 0.135097 housing_median_age 0.114110 households 0.064506 total_bedrooms 0.047689 population -0.026920 longitude -0.047432 latitude -0.142724
另一种查看属性间相关系数的方法是 pandas 的 scatter_matrix 方法,用来描述每个数值属性和其他数值属性关系。数据集中共有11个数值属性,则能画出 11^2 张图。可以挑选最有可能相关的属性,以下挑选了四个属性。
#pandas查看数值属性间的关系 attributes = ["median_house_value", "median_income", "total_rooms", "housing_median_age"] scatter_matrix(hous[attributes],figsize=(12, 8)) #figsize需要自己导入 plt.show()
当属性具有长尾分布时,可以尝试计算log对数将其转换。
将收入中位数与房价相关性的图片放大。可以看出,其相关性很高,数据比较集中。
#将收入中位数和方法相关性的图片单独放出来 hous.plot(kind="scatter", x="median_income", y="median_house_value", alpha=0.1) plt.show()
属性组合实验
尝试多种属性组合,将无意义属性合并。例如若不知道房间总数,卧室总数也就没有了意义。所以将数据集进行整理创建新的属性,相关矩阵如下,相对于总房间数和总卧室数,新的属性 bedrooms_per_room 与房价中位数的相关性更强,房屋越大,房价越高。
#属性组合试验,创建新的属性 hous["rooms_per_household"] = hous["total_rooms"] / hous["households"] hous["bedrooms_per_room"] = hous["total_bedrooms"] / hous["total_rooms"] hous["population_per_household"] = hous["population"] / hous["households"] corr_matrix = hous.corr() new_scc = corr_matrix["median_house_value"].sort_values(ascending=False) print(new_scc) median_house_value 1.000000 median_income 0.687160 rooms_per_household 0.146285 total_rooms 0.135097 housing_median_age 0.114110 households 0.064506 total_bedrooms 0.047689 population_per_household -0.021985 population -0.026920 longitude -0.047432 latitude -0.142724 bedrooms_per_room -0.259984
为机器学习算法准备数据
hou = strat_train_set.drop("median_house_value", axis=1) hous_labels = strat_train_set["median_house_value"].copy()
可以准备一些函数,降低写代码的重复性。首先需要对数据进行清洗来处理特征缺失问题。比如 total_bedrooms 属性就有缺失值,可以去掉对应街区、去掉整个属性或者进行赋值(0,平均值,中位数等)。
其中 DataFrame 的 dropna(), drop(), fillna() 方法均可实现。应当注意赋值时应该保存中位数,因为需要保证测试集中的缺失值也为该中位数。
sklearn 提供了类 Imputer 专门来处理缺失值,当其 strategy 选为中位数时,每个属性的中位数保存在实例变量 statistics_ 中,使用方法 median().values 就可以看到。
#dataframe 处理数据中的缺失值 drop_streets = hou.dropna(subset=["total_bedrooms"]) #去掉对应街区 drop_colums = hou.drop("total_bedrooms", axis=1) #去掉整个属性 median = hou["total_bedrooms"].median() replace_num = hou["total_bedrooms"].fillna(median) #缺失部分赋值中位数 #sklearn 处理数据中的缺失值 imputer = Imputer(strategy="median") hou_num = hou.drop("ocean_proximity", axis=1) #只有数值属性才有中位数,去掉文本属性 imputer.fit(hou_num) #将imputer实例拟合到训练数据 X = imputer.transform(hou_num) #numpy数组 hou_tr = pd.DataFrame(X, columns=hou_num.columns)#将数组放到dataframe中 print(imputer.statistics_) #这两种都可以打印对应缺值部分生成的中位数值 print(hou_num.median().values)
处理文本和类别属性
在上面的例子中给缺失值赋值时,需要删除文本类别属性 ocean_proximity, 显然文本属性不可能存在中位数的。但是大多机器学习算法都是直接处理数字的,所以需要将文本数据转换成数字。sklearn 的转换器 LabelEncoder 可以实现针对标签的转换,本例中也可以使用是因为数据只有一列文本特征值,当有多个文本特征值时需要使用 factorize() 方法。
当然这种做法也是有缺陷的,ML 算法会认为邻近的值比邻远的值更相似(0和4比0和1更相似)。当然可以通过独热编码来解决,sklearn 的编码器 OneHotEncoder, 用于将整数分类值变为独热向量。但是这样很浪费内存,因为生成的是一个有许多零的稀疏矩阵,可以调用 toarray() 方法生成密集的 NumPy 数组。
类 LabelBinarizer 可以实现一步转换(从文本分类到整数分类再到独热编码),该类也应用于标签列的转换。类 CategoricalEncoder 用于多特征文本。
# 处理文本和类别属性,针对只有一列文本特征的标签数据 encoder = LabelEncoder() housing_cat = hous["ocean_proximity"] housing_cat_encoded = encoder.fit_transform(housing_cat) # print(housing_cat_encoded) #打印文本转换后的数值 #处理文本和类别属性,针对不止一列的文本特征标签数据 housing_cat_encoded, housing_categories = housing_cat.factorize() # print(housing_cat_encoded[:10]) # print(encoder.classes_) #打印所有的本文类别 #文本属性值的独热编码方式 encoder = OneHotEncoder() housing_cat_1hot = encoder.fit_transform(housing_cat_encoded.reshape(-1,1)) # print(housing_cat_1hot) #生成numpy密集数组 # print(housing_cat_1hot.toarray()) #使用LabelBinarizer一步操作,使用与标签列一个文本特征 encoder = LabelBinarizer() #默认使用密集NumPY数组,将 sparse_output参数设为True就得到稀疏矩阵。 housing_cat_1hot = encoder.fit_transform(housing_cat) # print(housing_cat_1hot) #处理多文本特征 encoder = OrdinalEncoder() housing_cat_reshaped = housing_cat.values.reshape(-1, 1) housing_cat_1hot = encoder.fit_transform(housing_cat_reshaped) # print(housing_cat_1hot)
自定义转换器
sklearn 提供了很多种转换器,但有时候还需要自己定义需要用到的,自己定义时注意 sklearn 的规则,因为最后还得一块结合使用。一般时创建一个类三个方法:fit()( 返回self ), transform() 和 fit_transform。添加 TransformerMixin 为基类,也可以使用BaseEstimator 作为基类(有 get)params() 和 set_params() 两个方法,可以自动微调超参数)。
#自定义转换器,超参数add_bedrooms_per_room,默认值为True。用来调试添加的属性是否对机器学习算法有帮助 #可以为每个不能完全确保的数据准备步骤添加一个超参数,这样就容易发现更好用的组合。 rooms_ix, bedrooms_ix, population_ix, household_ix = 3, 4, 5, 6 class CombinedAttributesAdder(BaseEstimator, TransformerMixin): def __init__(self, add_bedrooms_per_room = True): self.add_bedrooms_per_room = add_bedrooms_per_room def fit(self, X, y=None): return self def transform(self, X, Y=None): rooms_per_household = X[:, rooms_ix] / X[:, household_ix] population_per_household = X[:, population_ix] / X[:, household_ix] if self.add_bedrooms_per_room: bedrooms_per_room = X[:, bedrooms_ix] / X[:, rooms_ix] return np.c_[X, rooms_per_household, population_per_household, bedrooms_per_room] else: return np.c_[X, rooms_per_household, population_per_household] def add_extra_features(X, add_bedrooms_per_room=True): rooms_per_household = X[:, rooms_ix] / X[:, household_ix] population_per_household = X[:, population_ix] / X[:, household_ix] if add_bedrooms_per_room: bedrooms_per_room = X[:, bedrooms_ix] / X[:, rooms_ix] return np.c_[X, rooms_per_household, population_per_household, bedrooms_per_room] attr_adder = FunctionTransformer(add_extra_features, validate=False, kw_args={ "add_bedrooms_per_room":False}) ##可以用FunctionTransformer代替CombinedAttributesAdder housing_extra_attribs = attr_adder.transform(hous.values) # print(housing_extra_attribs)
特征缩放
房产数据中总房间数分布范围是 6 到 39320,而收入中位数只分布在 0 到 15。常见的有两种方法让属性有相同量度,线性函数归一化(Min-Max scaling)和标准化(standardization)。
线性函数归一化:转变值,重新缩放直到范围变成 0 到 1。可以减去最小值,再除以极值来进行归一化。Sklearn 提供 MinMaxScaler,超参数为 feature_range,可以改变值的范围。
标准化:首先减去(平均值就为0了),再除以方差,这样得到的分布就有单位方差。没有用来改变值范围的超参数,而神经网络常常限制输入值在0和1之间,但是标准化方法不容易受到异常值的影响。Sklearn 提供 StandardScaler 来进行标准化。
转换流水线
数据处理存在很多转换步骤且需要按照一定步骤进行。Sklearn 提供了类 Pipeline 来进行一系列的转换操作。
pipeline 构造器需要一个定义步骤顺序的名字。除了最后一个是估计器,其余均是转换器(即要有 fit_transform() 方法)。当调用流水线 fit() 方法,就会对所有转换器依次调用 fit_transform() 方法,每次调用的输出作为参数传递给下一个调用,直到最后的估计器(只执行 fit() 方法)。
估计器 StandardScale 也是一个转换器,所以流水线会存在 transfrom() 方法,依次对所有数据做各种转换。
sklearn 还提供了可以并发执行的类 FeatureUnion,调用 transform() 方法时,所有转换器的 transform() 会并行执行(fit() 方法同理),然后合并输出返回结果。当然最新的类 ColumnTransformer 功能更强大。
#转换流水线,数值属性的小流水线 num_pipeline = Pipeline([ ('imputer', Imputer(strategy="median")), ('attribs_adder', FunctionTransformer(add_extra_features, validate=False)), ('std_scaler', StandardScaler()) #这是一个估计器 ]) housing_num_tr = num_pipeline.fit_transform(hou_num) # print(housing_num_tr) #并行处理数据并合并数据返回结果 class DataFrameSelector(BaseEstimator, TransformerMixin): #自定义转换器,将输出的DataFrame转变成一个Numpy数组。下面的新方法可以直接进行。 def __init__(self, attribute_names): self.attribute_names = attribute_names def fit(self, X, y=None): return self def transform(self, X): return X[self.attribute_names].values num_attribs = list(hou_num) cat_attribs = ["ocean_proximity"] old_num_pipeline = Pipeline([ ('selector', DataFrameSelector(num_attribs)), ('imputer', Imputer(strategy="median")), ('attribs_adder', FunctionTransformer(add_extra_features, validate=False)), #可以用FunctionTransformer代替CombinedAttributesAdder ('std_scaler', StandardScaler()) ]) old_cat_pipeline = Pipeline([ ('selector', DataFrameSelector(cat_attribs)), ('cat_encoder', OneHotEncoder(sparse=False)) ]) old_full_pipeline = FeatureUnion(transformer_list=[ ("old_num_pipeline", old_num_pipeline), ("old_cat_pipeline", old_cat_pipeline) ]) old_housing_prepared = old_full_pipeline.fit_transform(hous) # print(old_housing_prepared) #用ColumnTransformer来代替复杂的DataFrameSelector和FeatureUnion full_pipeline = ColumnTransformer([ ("num", num_pipeline, num_attribs), ("cat", OneHotEncoder(), cat_attribs), ]) housing_prepared = full_pipeline.fit_transform(hous) # print(housing_prepared) # print(housing_prepared.shape) #在前五个训练集上训练和评估 lin_reg = LinearRegression() lin_reg.fit(housing_prepared, hous_labels) some_data = hous.iloc[:5] some_labels = hous_labels[:5] some_data_prepared = full_pipeline.transform(some_data) # print("predictions:\t", lin_reg.predict(some_data_prepared)) # print("Labels:\t\t", list(some_labels)) #全部训练集上 housing_predictions = lin_reg.predict(housing_prepared) lin_mse = mean_squared_error(hous_labels, housing_predictions) lin_rmse = np.sqrt(lin_mse) # print(lin_rmse) #误差在68628美元 #使用更强大的模型 tree_reg = DecisionTreeRegressor(random_state=42) tree_reg.fit(housing_prepared, hous_labels) housing_predictions = tree_reg.predict(housing_prepared) tree_mse = mean_squared_error(hous_labels, housing_predictions) tree_rmse = np.sqrt(tree_mse) # print(tree_rmse) #误差为0美元,过拟合了
选择并训练模型
使用交叉验证做最佳的评估
评估决策树模型的一种方法是用函数 train_test_split 来分割训练集,得到一个更小的训练集和一个验证集,然后用更小的训练集来训练模型,用验证集来评估。
另一种更好的方法是使用 scikit-learn 的交叉验证功能。
还有一种是随机森林模型:RandomForestRegressor. 随机森林是通过用特征的随机子集训练许多决策树。
为了对比那个模型效果最好,需要保存每次训练的超参数,训练参数,交叉验证评分以及实际预测值等数据。可以使用 python 的模块 pickle,非常方面的保存 sklearn 模型,或使用 sklearn.externals.joblib,后者在序列化大型 NumPy 数组更有效率。
#使用交叉验证测量准确性 #k折交叉验证(k-fold cross-validation):将训练集随机分成十个不同的子集,然后训练评估决策树模型10次,每次选一个不用的折来做评估,用其他9个来做训练。结果 #一个包含10个评分的数组。 from sklearn.model_selection import cross_val_score scores = cross_val_score(tree_reg, housing_prepared, hous_labels, scoring = "neg_mean_squared_error", cv = 10) tree_rmse_scores = np.sqrt(-scores) #sklearn 交叉验证功能期望的是效用函数(越大越好)而非损失函数(越小越好),因此得分函数实际上与MSE相反(即负值)。 def display_scores(scores): print("Scores:", scores) print("Mean:", scores.mean()) print("Standard deviation:", scores.std()) display_scores(tree_rmse_scores) #使用线性回归模型来和交叉验证做个对比 lin_scores = cross_val_score(lin_reg, housing_prepared, hous_labels, scoring="neg_mean_squared_error", cv=10) lin_rmse_scores = np.sqrt(-lin_scores) display_scores(lin_rmse_scores) #随机森林方法 from sklearn.ensemble import RandomForestRegressor forest_reg = RandomForestRegressor(n_estimators=10, random_state=42) forest_reg.fit(housing_prepared, hous_labels) housing_predictions = forest_reg.predict(housing_prepared) forest_mse = mean_squared_error(hous_labels, housing_predictions) forest_rmse = np.sqrt(forest_mse) forest_scores = cross_val_score(forest_reg, housing_prepared, hous_labels, scoring="neg_mean_squared_error", cv=10) forest_rmse_scores = np.sqrt(-forest_scores) display_scores(forest_rmse_scores)
模型微调
- 网格搜索:使用 sklearn 的 GridSearchCV 来做逐个搜索最佳超参数组合的工作。网格搜索还可以自动判断是否添加一个不确定特征(例如前面使用的超参数 add_bedrooms_per_room)。用相似的办法可以处理异常值、确实特征、特征选择等任务。
- 随即搜索:当搜索相对较少的组合时可以使用网格搜索,当搜索空间较大时,最好使用 RandomizedSearchCV。使用方法类似,通过选择每个超参数的一个随机值的特定数量的随机组合。
- 集成方法:将表现最好的模型组合起来,特别时误差类型不同的模型。
- 分析最佳模型和它们的误差:比如随机森林可以指出每个属性对于做出准确预测的相对重要性。
#网格搜索最佳随机森林模型最佳超参数组合 from sklearn.model_selection import GridSearchCV param_grid = [ { 'n_estimators':[3, 10, 30], 'max_features':[2, 4, 6, 8]}, #3*4=12种组合 { 'bootstrap':[False], 'n_estimators':[3, 10], 'max_features':[2, 3, 4]} #2*3=6种组合 ] #总共探索12+6=18种随机森林的超参数组合,每个模型训练5次,总共需要训练18*5=90轮。 forest_reg = RandomForestRegressor(random_state=42) grid_search = GridSearchCV(forest_reg, param_grid, cv=5, scoring='neg_mean_squared_error', return_train_score=True) grid_search.fit(housing_prepared, hous_labels) # print(grid_search.best_params_) #最佳参数组合 # print(grid_search.best_estimator_) #最佳估计器 cvres = grid_search.cv_results_ #评估得分 # for mean_score, params in zip(cvres["mean_test_score"], cvres["params"]): # print(np.sqrt(-mean_score), params) pd.DataFrame(grid_search.cv_results_) #分析最佳模型随机森林每个属性对于做出准确预测的相对重要性 feature_importances = grid_search.best_estimator_.feature_importances_ # print(feature_importances) #将重要性分数和属性名放在一起 extra_attribs = ["rooms_per_household", "population_per_household", "bedrooms_per_room"] cat_encoder = full_pipeline.named_transformers_["cat"] cat_one_hot_attribs = list(cat_encoder.categories_[0]) attributes = num_attribs + extra_attribs + cat_one_hot_attribs im = sorted(zip(feature_importances,attributes), reverse=True) # print(im) final_model = grid_search.best_estimator_ X_test = strat_test_set.drop("median_house_value", axis=1) y_test = strat_test_set["median_house_value"].copy() X_test_prepared = full_pipeline.transform(X_test) final_predictions = final_model.predict(X_test_prepared) final_mse = mean_squared_error(y_test,final_predictions) final_rmse = np.sqrt(final_mse) # print(final_rmse) # print(final_mse)
用测试集评估系统
用调节完系统后,就可以使用测试集评估系统了。