diff --git a/Day81-90/90.机器学习实战.md b/Day81-90/90.机器学习实战.md index 485949c..88d4253 100755 --- a/Day81-90/90.机器学习实战.md +++ b/Day81-90/90.机器学习实战.md @@ -1 +1,414 @@ -## 机器学习实战 +## 说走就走的AI之旅第10课:机器学习实战 + +本章我们通过两个经典项目为大家讲解机器学习算法的应用,两个项目都来自于 [Kaggle](https://www.kaggle.com/)。Kaggle 是全球知名的数据科学竞赛平台,也是数据科学、机器学习、人工智能领域的重要社区之一。Kaggle 为数据科学家和算法工程师提供了一个实践、分享和竞争的空间。无论是新手还是经验丰富的专家,Kaggle 都能为其提供丰富的资源和挑战,通过平台提供的数据集、竞赛、课程等资源,用户可以根据自身的需求提升相关的数据科学技能并与全球的数据科学家互动。 + +### 1. 泰坦尼克号生存预测项目 + +泰坦尼克号生存预测是 Kaggle 上最著名的入门级机器学习项目之一。该项目要求你根据乘客的基本信息(如年龄、性别、舱位、登船地等)预测他们是否能够在泰坦尼克号沉船事件中幸存下来,属于典型的分类任务。我们可以在 Kaggle 网站的 Competitions 搜索到这个名为“[*Titanic - Machine Learning from Disaster*](https://www.kaggle.com/competitions/titanic/)”的项目,点击进入可以看到项目的背景介绍并找到下载数据文件`train.csv`和`test.csv`的链接,显然前者是训练集而后者是测试集。我们可以下载数据文件到本地并训练模型,然后再提交自己的预测结果;当然,如果愿意也可以在线创建 Notebook 文件并编写代码。 + +#### 数据探索 + +我们首先加载训练模型的数据,假设数据文件保存在当面路径下名为`data`的目录中,代码如下所示。 + +```python +import numpy as np +import pandas as pd + +df = pd.read_csv('data/train.csv', index_col='PassengerId') +df.head(5) +``` + +输出: + +``` + Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked +PassengerId +1 0 3 Braund, ... male 22.0 1 0 A/5 ... 7.2500 NaN S +2 1 1 Cumings, ... female 38.0 1 0 PC ... 71.2833 C85 C +3 1 3 Heikkinen, ... female 26.0 0 0 STON/O2 ... 7.9250 NaN S +4 1 1 Futrelle, ... female 35.0 1 0 113803 ... 53.1000 C123 S +5 0 3 Allen, ... male 35.0 0 0 373450 ... 8.0500 NaN S +``` + +> **注意**:加载数据的时候,我们直接将 PassengerID (乘客编号)列处理成了行索引。 + +下面的数据字典对`DataFrame`每个列的含义进行了说明。 + +| 列名 | 含义 | 说明 | +| ---------- | ---------------- | --------------------------------------------------- | +| `Survived` | 是否幸存 | 目标类别(`1`表示幸存,`0`表示遇难) | +| `Pclass` | 舱位等级 | 整数,取值为`1`、`2`、`3`,表示三种不同的等级 | +| `Name` | 乘客姓名 | 字符串 | +| `Sex` | 性别 | 字符串,`male`为男性,`female`为女性 | +| `Age` | 年龄 | 浮点数 | +| `SibSp` | 平辈亲属登船人数 | 整数,包括兄弟、姐妹、配偶总共登船的人数 | +| `Parch` | 父母小孩登船人数 | 整数 | +| `Ticket` | 船票号 | 字符串 | +| `Fare` | 船票价格 | 浮点数 | +| `Cabin` | 客舱号 | 字符串 | +| `Embarked` | 登船港口 | 字符串,`C`表示瑟堡,`Q`表示皇后镇、`S`表示南安普顿 | + +我们对数据进行可视化操,通过统计图表实训对数据的初步探索,代码如下所示。 + +```python +import matplotlib.pyplot as plt + +# 修改配置添加中文字体 +plt.rcParams['font.sans-serif'].insert(0, 'SimHei') +plt.rcParams['axes.unicode_minus'] = False + +# 定制画布 +plt.figure(figsize=(16, 12), dpi=200) + +# 遇难和获救人数分布 +plt.subplot(3, 4, 1) +ser = df.Survived.value_counts() +ser.plot(kind='bar', color=['#BE3144', '#3A7D44']) +plt.xticks(rotation=0) +plt.title('图1.获救情况分布') +plt.ylabel('人数') +plt.xlabel('') +for i, v in enumerate(ser): + plt.text(i, v, v, ha='center') + +# 客舱等级人数分布 +plt.subplot(3, 4, 2) +ser = df.Pclass.value_counts().sort_index() +ser.plot(kind='bar', color=['#FA4032', '#FA812F', '#FAB12F']) +plt.xticks(rotation=0) +plt.ylabel('人数') +plt.xlabel('') +plt.title('图2.客舱等级分布') +for i, v in enumerate(ser): + plt.text(i, v, v, ha='center') + +# 性别人数分布 +plt.subplot(3, 4, 3) +ser = df.Sex.value_counts() +ser.plot(kind='bar', color=['#16404D', '#D84040']) +plt.xticks(rotation=0) +plt.ylabel('人数') +plt.xlabel('') +plt.title('图3.性别分布') +for i, v in enumerate(ser): + plt.text(i, v, v, ha='center') + +# 登船港口人数分布 +plt.subplot(3, 4, 4) +ser = df.Embarked.value_counts() +ser.plot(kind='bar', color=['#FA4032', '#FA812F', '#FAB12F']) +plt.xticks(rotation=0) +plt.ylabel('人数') +plt.xlabel('') +plt.title('图4.登船港口分布') +for i, v in enumerate(ser): + plt.text(i, v, v, ha='center') + +# 乘客年龄箱线图 +plt.subplot(3, 4, 5) +df.Age.plot(kind='box', showmeans=True, notch=True) +plt.title('图5.乘客年龄情况') + +# 船票价格箱线图 +plt.subplot(3, 4, 6) +df.Fare.plot(kind='box', showmeans=True, notch=True) +plt.title('图6.船票价格情况') + +# 不同客舱等级遇难和幸存人数分布 +plt.subplot(3, 4, (7, 8)) +s0 = df[df.Survived == 0].Pclass.value_counts() +s1 = df[df.Survived == 1].Pclass.value_counts() +temp = pd.DataFrame({'遇难': s0, '幸存': s1}) +pcts = temp.div(temp.sum(axis=1), axis=0) +temp.plot(ax=plt.gca(), kind='bar', stacked=True, color=['#BE3144', '#3A7D44']) +for i, idx in enumerate(temp.index): + plt.text(i, temp.at[idx, '遇难'] // 2, f'{pcts.at[idx, "遇难"]:.2%}', ha='center', va='center') + plt.text(i, temp.at[idx, '遇难'] + temp.at[idx, '幸存'] // 2, f'{pcts.at[idx, "幸存"]:.2%}', ha='center', va='center') +plt.xticks(rotation=0) +plt.xlabel('') +plt.title('图7.不同客舱等级幸存情况') + +# 不同性别遇难和幸存人数分布 +plt.subplot(3, 4, (9, 10)) +s0 = df[df.Survived == 0].Sex.value_counts() +s1 = df[df.Survived == 1].Sex.value_counts() +temp = pd.DataFrame({'遇难': s0, '幸存': s1}) +pcts = temp.div(temp.sum(axis=1), axis=0) +temp.plot(ax=plt.gca(), kind='bar', stacked=True, color=['#BE3144', '#3A7D44']) +for i, idx in enumerate(temp.index): + plt.text(i, temp.at[idx, '遇难'] // 2, f'{pcts.at[idx, "遇难"]:.2%}', ha='center', va='center') + plt.text(i, temp.at[idx, '遇难'] + temp.at[idx, '幸存'] // 2, f'{pcts.at[idx, "幸存"]:.2%}', ha='center', va='center') +plt.xticks(rotation=0) +plt.xlabel('') +plt.title('图8.不同性别幸存情况') + +# 不同登船港口遇难和幸存人数分布 +plt.subplot(3, 4, (11, 12)) +s0 = df[df.Survived == 0].Embarked.value_counts() +s1 = df[df.Survived == 1].Embarked.value_counts() +temp = pd.DataFrame({'遇难': s0, '幸存': s1}) +pcts = temp.div(temp.sum(axis=1), axis=0) +temp.plot(ax=plt.gca(), kind='bar', stacked=True, color=['#BE3144', '#3A7D44']) +for i, idx in enumerate(temp.index): + plt.text(i, temp.at[idx, '遇难'] // 2, f'{pcts.at[idx, "遇难"]:.2%}', ha='center', va='center') + plt.text(i, temp.at[idx, '遇难'] + temp.at[idx, '幸存'] // 2, f'{pcts.at[idx, "幸存"]:.2%}', ha='center', va='center') +plt.xticks(rotation=0) +plt.xlabel('') +plt.title('图9.不同登船港口幸存情况') + +plt.show() +``` + +输出: + + + +从上面的统计图表可以看出,一等舱的乘客有更高的存活率,女性乘客相较于男性乘客也有更高的存活率,在瑟堡登船的用户存活率较其他两地更高。如果愿意,我们还可以尝试对性别、舱位、年龄等维度进行交叉组合并绘制出对应的统计图表,甚至还可以探索乘客姓名中的称谓(`Mr`、`Miss`、`Mrs`、`Dr`、`Master`、`Major`等)跟乘客能否幸存是否存在某种关系,有兴趣的读者可以自行研究。 + +我们进一步查看`DataFrame`对象相关信息。 + +```python +df.info() +``` + +输出: + +``` + +Index: 891 entries, 1 to 891 +Data columns (total 11 columns): + # Column Non-Null Count Dtype +--- ------ -------------- ----- + 0 Survived 891 non-null int64 + 1 Pclass 891 non-null int64 + 2 Name 891 non-null object + 3 Sex 891 non-null object + 4 Age 714 non-null float64 + 5 SibSp 891 non-null int64 + 6 Parch 891 non-null int64 + 7 Ticket 891 non-null object + 8 Fare 891 non-null float64 + 9 Cabin 204 non-null object + 10 Embarked 889 non-null object +dtypes: float64(2), int64(4), object(5) +memory usage: 83.5+ KB +``` + +可以看出,训练集一共`891`条数据,其中年龄、客舱号、登船港口等列存在缺失值,乘客姓名、登船港口等是字符串类型没有办法直接用于模型训练,所以我们需要做一些准备工作。 + +#### 特征工程 + +特征工程(Feature Engineering)是机器学习中的一个重要环节,通过对原始数据的处理、转换与选择,提取出能够帮助模型更好理解数据的特征,最终提升模型的预测性能。特征工程的主要步骤包括: + +1. **数据清洗**:处理数据中的缺失值、重复值、异常值等。 + +2. **特征转换**:将原始数据转化为更适合模型输入的形式。常见的特征转换方法有: + + - 标准化和归一化:标准化将数据转换为均值为`0`、标准差为`1`的分布;归一化将数据缩放到特定的范围(如:$\small{[0, 1]}$),确保各特征在同一数量级,对应的公式如下所示: + $$ + X^{\prime} = \frac{X - \mu}{\sigma} + $$ + + $$ + X^{\prime} = \frac{X - X_{min}}{X_{max} - X_{min}} + $$ + + 其中,$\small{\mu}$表示数据的均值,$\small{\sigma}$表示数据的标准差,$\small{X_{min}}$表示数据中的最小值,$\small{X_{max}}$表示数据中的最大值。Scikit-learn 库`preprocessing`模块中的`StandardScaler`和`MinMaxScaler`可以实现数据的标准化和归一化。 + + - 类别编码:对于类别变量(如:性别、职业、地区等),常用的编码方式有独热编码(One-Hot Encoding)和标签编码(Label Encoding)。Scikit-learn 库`preprocessing`模块中的`OneHotEncoder`和`LabelEncoder`可以实现数据的独热编码和标签编码。 + + - 对数变换:对数变换可以帮助处理具有偏态分布的特征,使其更接近正态分布。 + +3. **特征选择**:从大量的特征中筛选出对模型预测有用的特征,去除冗余或无关的特征,减少维度并提高模型的性能。常用的方法有: + + - **过滤法**:基于统计检验方法(如:卡方检验、皮尔逊相关系数等)选择最具区分度的特征。 + - **包裹法**:通过模型的性能来评估特征子集的效果(如:递归特征消除法)。 + - **嵌入法**:通过训练模型并从模型中提取特征的重要性(如:决策树、L1正则化等),从而进行特征选择。 + +4. **特征降维**:通过某种数学变换将数据从高维空间映射到低维空间,以便保留数据的主要信息,简化模型,降维后的新特征是原始特征的某种组合。降维可以减少模型训练的运算量,减少不必要的噪声和冗余特征来降低复杂性,从而提高模型的泛化能力。特征降维常用的方法有: + + - **PCA**(主成分分析):通过线性变换将原始特征映射到新的坐标轴上,这些新的坐标轴(主成分)是原始特征的线性组合。主成分按方差大小排序,保留较大方差的主成分,丢弃方差较小的主成分。 + - **LDA**(线性判别分析):主要用于分类任务,通过寻找最大化类间散度和最小化类内散度的方式进行降维。 + - **t-SNE**(t-Distributed Stochastic Neighbor Embedding):t-SNE 是一种非线性的降维方法,主要用于数据可视化,特别适用于高维数据(如图像、文本等)的降维。t-SNE的目标是将高维空间中的相似点(邻近点)映射到低维空间中的相近位置,同时尽量避免不同类别之间的重叠。 + +5. **特征构造**:通过原始特征组合、变换或派生出新的特征。 + +特征工程不仅是提高模型准确性的关键因素,也是机器学习中一个需要持续优化的过程。通过对数据进行精心的清洗、转换、选择和构造,可以为模型提供更有价值的信息,提高模型的表现。尽管特征工程看似复杂,但它的效果在很多情况下超出了模型本身的改进,因此在实际应用中,我们应投入足够的精力和时间来进行特征工程的优化。 + +对于我们加载的数据集,我们可以将年龄字段的缺失值处理为中位数,将登船港口的缺失值处理为众数,客舱号字段的缺失值比较多,一种处理方式是直接删除这个列,另一种处理方式是二值化,有客舱号的记为`1`,没有客舱号的记为`0`,代码如下所示。 + +```python +df['Age'] = df.Age.fillna(df.Age.median()) +df['Embarked'] = df.Embarked.fillna(df.Embarked.mode()[0]) +df['Cabin'] = df.Cabin.replace(r'.+', '1', regex=True).replace(np.nan, 0).astype('i8') +``` + +处理完缺失值后,我们对年龄和船票价格两个字段进行特征缩放,通过`MinMaxScaler`实现线性归一化,代码如下所示。 + +```python +from sklearn.preprocessing import MinMaxScaler + +scaler = MinMaxScaler() +df[['Fare', 'Age']] = scaler.fit_transform(df[['Fare', 'Age']]) +``` + +接下来,还需要对性别和登船港口两个字段进行独热编码处理,可以使用 pandas 库中的`get_dummies`函数或 scikit-learn 库的`OneHotEncoder`处理,两者处理的结果类似,代码如下所示。 + +```python +df = pd.get_dummies(df, columns=['Sex', 'Embarked'], drop_first=True) +``` + +我们继续处理乘客姓名字段,根据姓名中的称谓衍生出一个新的特征;此外,对于`SibSp`和`Parch`两个字段,我们可以将其衍生为家庭成员数量的新特征,代码如下所示。 + +```python +title_mapping = { + 'Mr': 0, 'Miss': 1, 'Mrs': 2, 'Master': 3, 'Dr': 4, 'Rev': 5, 'Col': 6, 'Major': 7, + 'Mlle': 8, 'Ms': 9, 'Lady': 10, 'Sir': 11, 'Jonkheer': 12, 'Don': 13, 'Dona': 14, 'Countess': 15 +} +df['Title'] = df['Name'].map( + lambda x: x.split(',')[1].split('.')[0].strip() +).map(title_mapping).fillna(-1) +df['FamilySize'] = df['SibSp'] + df['Parch'] + 1 +``` + +> **说明**:我们将每个称谓映射到一个数字,这里不必过于纠结称谓跟数字的对应关系,而且有些称谓在我们的训练集中并没有出现。对于那些未知的称谓,我们统一处理为`-1`。 + +完成上面的操作之后,我们可以把不必要的字段全部删除掉,数据的准备工作就基本完成了。 + +```python +df.drop(columns=['Name', 'SibSp', 'Parch', 'Ticket'], inplace=True) +``` + +#### 模型训练 + +首先我们还是把数据集划分为训练集和测试集,注意这里的测试集不是官方提供的`test.csv`文件,而是把`train.csv`中的一部分数据划分出来对模型进行验证,所以将其理解为验证集更加合适,所以下面的代码中我们将其命名为`X_valid`和`y_valid`。 + +```python +from sklearn.model_selection import train_test_split + +X, y = df.drop(columns='Survived'), df.Survived +X_train, X_valid, y_train, y_valid = train_test_split(X, y, train_size=0.9, random_state=3) +``` + +我们尝试逻辑回归模型,代码如下所示。 + +```python +from sklearn.linear_model import LogisticRegression +from sklearn.metrics import classification_report + +model = LogisticRegression(penalty='l1', tol=1e-6, solver='liblinear') +model.fit(X_train, y_train) +y_pred = model.predict(X_valid) +print(classification_report(y_valid, y_pred)) +``` + +输出: + +``` + precision recall f1-score support + + 0 0.88 0.80 0.84 56 + 1 0.72 0.82 0.77 34 + + accuracy 0.81 90 + macro avg 0.80 0.81 0.80 90 +weighted avg 0.82 0.81 0.81 90 +``` + +我们再试一试大杀器 XGBoost,代码如下所示。 + +```python +import xgboost as xgb + +# 将数据处理成数据集格式DMatrix格式 +dm_train = xgb.DMatrix(X_train, y_train) +dm_valid = xgb.DMatrix(X_valid) + +# 设置模型参数 +params = { + 'booster': 'gbtree', # 用于训练的基学习器类型 + 'objective': 'binary:logistic', # 指定模型的损失函数 + 'gamma': 0.1, # 控制每次分裂的最小损失函数减少量 + 'max_depth': 10, # 决策树最大深度 + 'lambda': 0.5, # L2正则化权重 + 'subsample': 0.8, # 控制每棵树训练时随机选取的样本比例 + 'colsample_bytree': 0.8, # 用于控制每棵树或每个节点的特征选择比例 + 'eta': 0.05, # 学习率 + 'seed': 3, # 设置随机数生成器的种子 + 'nthread': 16, # 指定了训练时并行使用的线程数 +} + +model = xgb.train(params, dm_train, num_boost_round=200) +y_pred = model.predict(dm_valid) +# 将预测的概率转换为类别标签 +y_pred_label = (y_pred > 0.5).astype('i8') +print(classification_report(y_valid, y_pred_label)) +``` + +输出: + +``` + precision recall f1-score support + + 0 0.89 0.91 0.90 56 + 1 0.85 0.82 0.84 34 + + accuracy 0.88 90 + macro avg 0.87 0.87 0.87 90 +weighted avg 0.88 0.88 0.88 90 +``` + +当然,在条件允许的情况下,强烈建议大家通过网格搜索交叉验证的方式对模型的超参数进行调优。 + +#### 模型评估 + +接下来我们加载真正的测试数据`test.csv`,通过前面训练好的模型来做出预测。我们可以将预测的结果保存成一个 CSV 文件,该文件共有两列,一列是 PassengerID,一列是我们预测的结果。我们将该文件提交到 Kaggle 平台,可以获得最终模型的准确率评分。 + +```python +test = pd.read_csv('data/test.csv', index_col='PassengerId') +# 处理缺失值 +test['Age'] = test.Age.fillna(test.Age.median()) +test['Fare'] = test.Fare.fillna(test.Fare.median()) +test['Embarked'] = test.Embarked.fillna(test.Embarked.mode()[0]) +test['Cabin'] = test.Cabin.replace(r'.+', '1', regex=True).replace(np.nan, 0).astype('i8') +# 特征缩放 +test[['Fare', 'Age']] = scaler.fit_transform(test[['Fare', 'Age']]) +# 处理类别 +test = pd.get_dummies(test, columns=['Sex', 'Embarked'], drop_first=True) +# 特征构造 +test['Title'] = test['Name'].apply(lambda x: x.split(',')[1].split('.')[0].strip()).map(title_mapping).fillna(-1) +test['FamilySize'] = test['SibSp'] + test['Parch'] + 1 +# 删除多余特征 +test.drop(columns=['Name', 'Ticket', 'SibSp', 'Parch'], inplace=True) + +# 逻辑回归模型 +passenger_id, X_test = test.index, test +# XGBoost模型 +# passenger_id, X_test = test.index, xgb.DMatrix(test) + +y_test_pred = model.predict(X_test) +# XGoost模型 - 将预测的概率转换成类别标签 +# y_test_pred = (model.predict(X_test) > 0.5).astype('i8') + +# 生成提交文件 +result = pd.DataFrame({ + 'PassengerId': passenger_id, + 'Survived': y_test_pred +}) +result.to_csv('submission.csv', index=False) +``` + +大家可以试一试,不同的模型预测结果是否存在较大的差异,哪个是你心目中最理想的模型,然后大家可以通过实验验证我们之前提到的一个观点,看看特征工程是不是对预测的结果有重要的影响。 + +###2. Zillow平台房价预测项目 + +Zillow 是一个广泛使用的房地产信息平台,其提供了各种与房地产市场相关的数据集。在 Kaggle 上的 **Zillow House Price Prediction** 比赛中,参与者需要预测美国地区住宅的市场价值,属于典型的回归任务。这个数据集包含了大量的房屋信息,如房屋的物理属性、位置、历史数据、市场趋势等。 + +#### 数据探索 + +#### 特征工程 + +#### 模型训练 + +#### 模型评估 + diff --git a/Day81-90/res/10_dataset_visualization.png b/Day81-90/res/10_dataset_visualization.png new file mode 100644 index 0000000..33572af Binary files /dev/null and b/Day81-90/res/10_dataset_visualization.png differ