BIM 和人工智能相结合。用例。
利用深度神经网络优化 BIM 模型能源性能
相对而言,最近 AEC(建筑、工程、施工)行业遇到了一项名为 BIM 的新技术。建筑信息建模允许在实际施工开始之前虚拟地建造一座建筑;这带来了许多有形和无形的好处:减少成本超支,更有效的协调,授权决策等。对于一些公司来说,采用 BIM 是一个很大的障碍,许多公司仍在挣扎。但现在我们看到行业中出现了另一个新趋势:人工智能。我们不要害怕,仔细看看。这比你想象的要简单!
在本文中,我将展示我的硕士论文 使用深度神经网络 优化 BIM 模型能源性能。
提出问题
许多不同的估计表明大约 70–80%的设施成本是运营成本。
设施成本分配。作者图片
这些当然也是因为保养;但请记住,建筑业有时被称为“40%的产业”,因为它占据了世界自然资源和二氧化碳排放量的 40%。我们应该更好地保护自然!
我将尝试提出一个优化建筑能耗的框架,它被称为 EUI,或能源使用强度,以 MJ(或 kWh)/m/年为单位。能源分析将由绿色建筑工作室执行,该工作室使用 DOE-2 引擎和从 Revit 导出的 gbXML 文件。
获取数据
首先,我们需要做一些假设。让我们为我将要测试的每个模型保持相同的 HVAC 系统(即,Revit 提供的用于单户住宅的标准 HVAC 模型将用于每个 Revit 模型)。事实上,在真实的设施中,随着时间的推移,它可以被更高效的装置和系统所取代,否则我们可能不会那么早就知道 HVAC 的类型。
相反,让我们把注意力集中在建筑物更持久的特征上,比如地板、墙壁和屋顶的热传导率;窗墙比;计划中的旋转。这些都是我将要研究的特性。
另一个假设是,我们的 Revit 模型将是一个普通的盒子,里面只有一个房间,没有隔墙和窗户(窗墙比将在后面指定)。这是为了简化分析。
首个 Revit 模型。作者图片
因此,让我们尝试以下参数范围的所有组合:
设计参数。作者图片
10368 种组合太多了,但 Revit API 会有所帮助。Green Building Studio 使用 Revit 可以导出的 gbXML 文件。该脚本将热阻值和平面旋转的组合应用于模型,并将每个组合模型导出为 gbXML 格式。通过改变热资产的热导率来获得不同的热阻值。改变厚度会在分析中引入另一个因素:分析表面始终位于元素的中间,因此总面积会随着壁厚而变化。
在解析 gbXML 目录以获得所有文件的路径之后,我们准备将我们的 3456 (121212*2) gbXML 文件上传到 Green Building Studio。使用发电机包对发电机进行能量分析。
发电机节点能量分析。作者图片
分析完成后,我们可以开始在 Green Building Studio 中指定窗墙比。不幸的是,Dynamo 包没有这个功能,GBS API 只对开发者开放,所以我不得不求助于浏览器自动化来分配 wwr。然而,这只需要做一次,我们稍后会看到为什么。当能量分析完成后,我们可以解析 GBS 的数据并彻底检查它。
用密谋密谋。作者图片
现在,让我们用另一个简单但不同的 Revit 模型重复上述所有步骤。
第二个 Revit 模型。作者图片
我们需要这些数据供以后使用。
训练神经网络
对于每个机器学习项目,数据检查和准备是必须的。但是在这种情况下,我们没有缺失数据或异常值:我们的数据是人工创建的。因此我们可以安全地跳过许多检查步骤。我将在另一篇文章中更详细地描述神经网络。有兴趣的话,这里有剧本。但长话短说,给定大量数据,神经网络能够推导出管理数据的规则。与传统编程相比,我们给出规则和数据来获得答案。
图片作者。灵感来自 DataLya
当规则难以编码时,神经网络就派上了用场:人脸或语音识别、自然语言处理、翻译、情感分析等。
我设计的网络有这样的架构:
原始神经网络。作者图片
输入层(绿色)有 5 个单元。这些是我们的参数:WWR,平面旋转和三个热阻值。反过来,输出图层(黄色)是 EUI 值。将这个网络(蓝色层)视为一个巨大的矩阵,在第一步中只包含随机数。为了训练网络,我们的输入层(向量)乘以一系列矩阵,以获得 EUI 值的预测。然后将预测值与实际 EUI 值进行比较,并更新网络中的数字以更好地预测输出。这个循环一直重复,直到我们对表现满意为止。
现在是时候根据第一个盒子模型的数据点来训练我们的网络了。其中 94%将用于训练我们的网络,6%用于验证网络并调整影响网络的一些参数以获得更好的性能。
训练之后,我们使用网络来预测 10368 个 EUI 值:
网络的预测。作者图片
误差保持在 0.2%以内,还不错。除此之外,网络把我们的数据从离散变成了连续。换句话说,我们现在可以获得以前无法获得的参数的 EUI 值;例如 21% WWR 或 R=2.45。
好了,这让我们进入下一步。
转移学习
还记得我们在第一步中制作的第二个 Revit 模型吗?我们现在将把它与一种叫做“迁移学习”的技术结合起来使用。让我们将前一步中训练好的网络设置为不可训练的前四层:
冻结网络。作者图片
或者,换句话说,让我们只关注最后两层。
此时,网络“知道”主要模式和趋势以及每个参数如何影响 EUI。但仅适用于第一个 Revit 模型。
现在,让我们通过使用新数据重新训练最后两层,将新的 Revit 模型“引入”我们的网络。但是有一个重要的区别:这次只有 6%的数据用于训练,94%用于验证。不执行超参数调整。我们在训练后得到这样的结果:
再训练网络的预测。作者图片
注意:与第一个 box Revit 模型的 2-3 小时相比,训练花费的时间大约为 1 或 2 分钟,预测几乎一样准确。
一个小实验
为什么坚持 6%的培训-验证比例?让我们再尝试一些,看看效果如何。
4%的训练数据。作者图片
1%的训练数据。作者图片
0.25%训练数据。作者图片
事实证明,在大约 1-2%的训练-验证分裂时,性能开始显著下降。
请注意,在功能丧失达到稳定水平后,训练停止
结果和比较
恭喜你!现在,我们有了一个训练有素的网络,可以用少量的数据预测 Revit 模型在大范围参数下的能耗。该模型甚至可能没有任何窗口。我们做的最后一步展示了它:通过从我们想要分析的模型中引入一些数据点,我们得到了一个非常准确的估计,误差很小。
为了强调迁移学习效应,让我们以刚刚训练的网络为例,预测两个模型的 EUI。
一个网络-两个 Revit 模型预测。作者图片
这就是说,我们不能采取随机神经网络,并期望它与我们的模型一起工作:应该做一点点能量分析。然而,它可以像第一部分一样自动完成。
未来工作
现在,我们已经有了一个神经网络来建立一个准确的能耗预测,除了用复杂的 Revit 模型进行测试之外,还必须向实际优化迈出一步。为了找到参数的最优组合,需要建立一个成本模型。成本模型应包括材料成本、人工成本、可能的维护成本、能源成本,并应考虑建筑的生命周期、建筑和物理限制。
这将产生一个现实的框架,只需很少的努力就可以在项目的概念阶段选择最佳的参数组合。
谢谢你读到这里!
我总是乐于参与讨论或收到反馈。给我留言LinkedIn或电子邮件:mikhail1dem@gmail.com
二进制和多类文本分类(模型测试管道中的自动检测)
来源:作者图片
介绍
在我之前的文章(文本分类中的模型选择)中,我针对二进制文本分类问题,提出了一种通过比较经典机器学习和深度学习来选择模型的方法。
笔记本的结构是自动运行,交叉验证所有算法,并显示不同指标的结果,让用户根据自己的需要自由选择算法。
在这里,笔记本被创建来显示二元或多类问题的不同度量和学习曲线。笔记本将自动定义数据集的配置。
笔记本和脚本可以在这里找到: GitHub
信息
目标是使用不同的模型,并在训练和测试期间观察它们的行为。当可能时,算法执行提前停止以避免过拟合。对于深度学习,可以在训练期间使用预训练模型来减少训练时间。
开头有两件事很重要:
-文本列名归类
-标签列名
创建流水线是为了考虑到二进制分类或多类分类,而无需人工参与。管道提取标签的数量,并确定它是二元问题还是多类问题。所有算法和指标将自动从一个切换到另一个。
笔记本以用于测试您想要的模型的参数列表开始。下面的 要诀 显示了这个列表。 Lang 决定笔记本是否需要从 Google 调用Translator()API 来检测数据集的语言(默认为英语)。
相应的 python 函数:
I —数据清理、文本处理
在这篇文章中使用的数据与上一篇文章相同,IMDB 数据集(因为它是一个开源数据集,更多详细信息在此)。
数据集不是完全干净的,在自然语言处理任务中,你需要为你的问题清理你的数据。这一步将影响算法的性能。
关于 IMDB 数据集,我做了什么?
- 去掉一个单词的所有大写字母**,只保留第一个字母(这对 NER 提取很重要)**
- 移除网址(如果有)
- 移除 html 应答器
- 删除表情符号
- 用有有代替有有
- 用而非 替换n的缩写
II —极性
使用库 TextBlob 估计文本的极性,并绘制成图表(在 5000 raws 的样本上制作)。
III —文本信息
所有的评论都没有相同的长度,相同的字数。研究这些信息很有趣:
- 提取字数
- 提取字符数
- 计算密度(字符数/字数)
- 提取首字母大写的单词数
我们可以很容易地用图表标出 char 的数量:
评论大多由 550-600 个字符组成。
看类(标签)的分布也很有意思。
标签
数据集是平衡的,大约有 2500 个标签类别的评论(二进制)。
六元语法
N-grams 是一种将句子分割成 n 个单词的技术,例如:
我学机器学习是为了成为一名数据科学家
- Unigram :【我,学习,机器,学习,到,成为,a,数据,科学家】
- 二元模型 : [(我学习),(学习机),(机器学习),(学习到),(成为),(成为一个),(一个数据),(数据科学家)]
- 三元组 : [(我学习机器),(学习机器学习),(机器学习到),(学习成为),(成为一个),(成为一个数据),(一个数据科学家)]
- …
我是 n 元语法的粉丝,因为我可以通过它们展示预处理是否正确。在笔记本上,你会发现单词重要性对于单个词、两个词、有和没有停用词的三个词(一个没有重要性的单词)以及对 5 个词的测试。
对于带停用词的三元模型:
没有停止词:
这种差异非常重要。一般来说,文本包含许多停用词,这对于像 TF-IDF 或 单词嵌入 这样的方法并不重要,但是对于 One-Hot 编码 来说却是如此,因为每个单词在一个句子中具有相同的重要性。
我们使用什么模型和指标?
管道被配置为使用不同的模型。表 1 给出了机器学习和深度学习算法以及使用的指标。
看起来怎么样
我给大家看两个例子,第一个, 随机梯度推进 和 浅层神经网络 。
训练经典分类器代码如下:
该函数输入分类器和数据以拟合模型。
对于指标,已经创建了相同类型的函数:
该函数将显示不同的曲线(精确召回率、真假阳性率、ROC AUC)、混淆矩阵、Cohen’s Kappa(模型之间的比较以及两个标注器将如何做)和准确度。
随机梯度推进:
用 TF-IDF 方法获得了 SGD 算法(函数 1)的最佳结果。
if sgd: # does the computation if sgd = True
print("\nStochastic Gradient Descent with early stopping for TF-IDF\n")
print("Early Stopping : 10 iterations without change")
metrics_ML(SGDClassifier(loss='modified_huber', max_iter=1000, tol=1e-3, n_iter_no_change=10, early_stopping=True, n_jobs=-1 ),xtrain_tfidf, train_y, xvalid_tfidf, valid_y, gb=True)
metrics_ML()函数将调用 classifier_model()函数来训练模型并计算指标。训练分类器和指标的最简单方法。
结果是:
Stochastic Gradient Descent with early stopping for TF-IDF
Early Stopping : 10 iterations without change
Execution time : 0.060 s
Score : 84.7 %
Classification Report
precision recall f1-score support
negative 0.87 0.81 0.84 490
positive 0.83 0.88 0.85 510
accuracy 0.85 1000
macro avg 0.85 0.85 0.85 1000
weighted avg 0.85 0.85 0.85 1000
混淆矩阵
Model: f1-score=0.855 AUC=0.923
精确召回曲线
ROC AUC=0.919
真假阳性率
还不错,84.7%的分数。现在我们能做得更好吗?
浅层神经网络:
浅层神经网络的代码已在上一篇文章中介绍过。这里再说一遍:
每个深度学习算法都以相同的方式实现。所有的代码都可以在笔记本和对应的 GitHub 中找到。
如何使用它:
es = tf.keras.callbacks.EarlyStopping(monitor='val_loss', mode='auto', patience=3)
if shallow_network:
model_shallow = shallow_neural_networks(word_index, pre_trained=pre_trained)
history = model_shallow.fit(train_seq_x, train_y,
epochs=1000, callbacks=[es],
validation_split=0.2, verbose=True)
results = model_shallow.evaluate(valid_seq_x, valid_y)
输出:
Train on 3200 samples, validate on 800 samples
Epoch 1/1000
3200/3200 [==============================] - 2s 578us/sample - loss: 0.7117 - accuracy: 0.4837 - val_loss: 0.7212 - val_accuracy: 0.5175
...
Epoch 98/1000
3200/3200 [==============================] - 1s 407us/sample - loss: 0.4991 - accuracy: 0.9991 - val_loss: 0.5808 - val_accuracy: 0.88501000/1000 [==============================] - 0s 383us/sample - loss: 0.5748 - accuracy: 0.8590
指标:
浅层神经网络的历史
precision recall f1-score support
negative 0.86 0.85 0.86 490
positive 0.86 0.87 0.86 510
accuracy 0.86 1000
macro avg 0.86 0.86 0.86 1000
weighted avg 0.86 0.86 0.86 1000
The balanced accuracy is : 85.88%
The Zero-one Loss is : 14.1%
Explained variance score: 0.436
ROC AUC=0.931
Model: f1-score=0.863 AUC=0.932
Cohen's kappa: 71.78%
所以,神经网络的结果更好。但是,不能对每种方法的单次运行进行比较。要正确选择模型,必须使用交叉验证等评估方法(参见:文本分类中的模型选择)。
结论
流水线是自动的,你只需要在开始时配置参数,选择你想要测试的不同算法并等待结果。
目标是通过算法和方法(One-Hot encoding、TF-IDF、TF-IDF n-grams、TF-IDF char n-grams 和单词嵌入)显示不同的指标,并选择您希望用于解决问题的一类算法。下一步将是调整超参数并享受结果。
这项工作有助于在不了解类的情况下,针对文本分类、二元或多元类快速测试 NLP 用例。管道可以接受法语文本或者英语文本。
笔记本和课程可在 GitHub 上获得。
后续步骤
- 实现不平衡的方法来自动平衡数据集
- 实现变压器分类模型
- 实施预先培训的变压器
- 用强化学习测试 NLP
- 知识图谱
- 使用分布式深度学习
- 使用 TensorNetwork 加速神经网络
- 用正确的方法选择一类模型并进行超参数调整
- 使用量子 NLP (QNLP)
二元分类示例
预测阿片类药物的使用
里卡多·罗查在 Unsplash 上的照片
这场全球危机以这样或那样的方式影响了我们所有人的生活,但这是一个磨练你的手艺的绝佳机会。我碰巧更新了我的 Coursera 账户,专门尝试网络分析。加密货币/区块链媒介文章已经成为我日常生活的一部分。最后,我想发布一个 ML 分类的例子来帮助那些寻找一个详细的“从开始到结束”的用例的人,并从整个社区中寻求建设性的反馈。这将是一个漫长而详细的旅程,我希望你已经准备好了。
我们将利用一个保险数据集,该数据集概述了一系列以患者为中心的特征,最终目标是正确预测阿片类药物滥用是否已经发生。
功能定义
ClaimID Unique: Identifier for a claim
Accident DateID: Number of days since the accident occurred from an arbitrary date
Claim Setup DateID: Number of days since the Resolution Manager sets up the claim from an arbitrary date
Report To DateID: Number of days since the employer notifies insurance of a claim from an arbitrary date
Employer Notification DateID: Number of days since the claimant notifies employer of an injury from an arbitrary date
Benefits State: The jurisdiction whose benefits are applied to a claim
Accident State: State in which the accident occurred
Industry ID: Broad industry classification categories
Claimant Age: Age of the injured worker
Claimant Sex: Sex of the injured worker
Claimant State: State in which the claimant resides
Claimant Marital Status: Marital status of the injured worker
Number Dependents: Number of dependents the claimant has
Weekly Wage: An average of the claimant’s weekly wages as of the injury date.
Employment Status Flag:
F — Regular full-time employee
P — Part-time employee
U — Unemployed
S — On strike
D — Disabled
R — Retired
O — Other
L — Seasonal worker
V — Volunteer worker
A — Apprenticeship full-time
B — Apprenticeship part-time
C — Piece worker
RTW Restriction Flag: A Y/N flag, used to indicate whether the employees responsibilities upon returning to work were limited as a result of his/her illness or injury.
Max Medical Improvement DateID: DateID of Maximum Medical Improvement, after which further recovery from or lasting improvements to an injury or disease can no longer be anticipated based on reasonable medical probability.
Percent Impairment: Indicates the percentage of anatomic or functional abnormality or loss, for the body as a whole, which resulted from the injury and exists after the date of maximum medical improvement
Post Injury Weekly Wage: The weekly wage of the claimant after returning to work, post-injury, and/or the claim is closed.
NCCI Job Code: A code that is established to identify and categorize jobs for workers’ compensation.
Surgery Flag: Indicates if the claimant’s injury will require or did require surgery
Disability Status:
— Temporary Total Disability (TTD)
— Temporary Partial Disability (TPD)
— Permanent Partial Disability (PPD)
— Permanent Total Disability (PTD)
SIC Group: Standard Industry Classification group for the client
NCCI BINatureOfLossDescription: Description of the end result of the bodily injury (BI) loss occurrence
Accident Source Code: A code identifying the object or source which inflicted the injury or damage.
Accident Type Group: A code identifying the general action which occurred resulting in the loss
Neurology Payment Flag: Indicates if there were any payments made for diagnosis and treatment of disorders of the nervous system without surgical intervention
Neurosurgery Payment Flag: Indicates if there were any payments made for services by physicians specializing in the diagnosis and treatment of disorders of the nervous system, including surgical intervention if needed
Dentist Payment Flag: Indicates if there were any payments made for prevention, diagnosis, and treatment of diseases of the teeth and gums
Orthopedic Surgery Payment Flag: Indicates if there were any payments made for surgery dealing with the skeletal system and preservation and restoration of its articulations and structures.
Psychiatry Payment Flag: Indicates if there were any payments made for treatment of mental, emotional, or behavioral disorders.
Hand Surgery Payment Flag: Indicates if there were any payments made for surgery only addressing one or both hands.
Optometrist Payment Flag: Indicates if there were any payments made to specialists who examine the eye for defects and faults of refraction and prescribe correctional lenses or exercises but not drugs or surgery
Podiatry Payment Flag: Indicates if there were any payments made for services from a specialist concerned with the care of the foot, including its anatomy, medical and surgical treatment, and its diseases.
HCPCS A Codes — HCPCS Z Codes: Count of the number of HCPCS codes that appear on the claim within each respective code group
ICD Group 1 — ICD Group 21: Count of the number of ICD codes that appear on the claim w/in each respective code group
Count of the number of codes on the claim
— CPT Category — Anesthesia
— CPT Category — Eval_Mgmt
— CPT Category — Medicine
— CPT Category — Path_Lab
— CPT Category — Radiology
— CPT Category — Surgery
Count of the number of NDC codes on the claim within each respective code class
— NDC Class — Benzo
— NDC Class — Misc (Zolpidem)
— NDC Class — Muscle Relaxants
— NDC Class — Stimulants
Opioids Used: A True (1) or False (0) indicator for whether or not the claimant abused an opioid
探索性数据分析
让我们从导入所有需要的库和数据集开始。
from math import sqrt
import pandas as pd
import numpy as np
import scipy.stats as stats
import matplotlib.pyplot as plt
import seaborn as sns
from IPython.display import displayfrom sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipelinefrom feature_engine import missing_data_imputers as mdi
from feature_engine import categorical_encoders as ce
from sklearn.preprocessing import RobustScaler
from imblearn.over_sampling import SMOTEfrom sklearn.model_selection import cross_val_score
from sklearn.metrics import roc_auc_score
from sklearn.metrics import roc_curve
from sklearn.metrics import recall_score
from sklearn.metrics import classification_report
from sklearn.metrics import confusion_matrix
from sklearn.model_selection import GridSearchCVfrom sklearn.ensemble import GradientBoostingClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.naive_bayes import GaussianNB
from sklearn import svm%matplotlib inline
pd.pandas.set_option('display.max_columns', None)with open('opioid_use_data.csv') as f:
df = pd.read_csv(f)
f.close()SEED = 42df.shape
df.head(10)
df.info()
我们的数据集包含刚刚超过 16,000 个观察值以及 92 个特征,包括目标(即使用的阿片类药物)。我们还有各种各样的特征类型,包括整数、浮点、字符串、布尔和混合类型。
删除初始特征
在我们处理缺失数据、离群值或基数之前,让我们看看是否可以快速删除任何功能以简化我们的进一步分析。
for var in df.columns:
print(var, df[var].nunique(), len(df))df.drop('ClaimID', axis=1, inplace=True)
当我们滚动输出时,可以看到每个要素的唯一值的数量以及整个数据集的总长度。具有与数据帧总长度相似数量的唯一值的特征可以被移除,因为它们不提供太多的预测能力(即方差)。“ClaimID”是唯一符合此标准的功能,可以删除。
df_copy = df.copy()
corr_matrix = df_copy.corr().abs()
upper = corr_matrix.where(np.triu(np.ones(corr_matrix.shape), k=1).astype(np.bool))upper
to_drop = [var for var in upper.columns if any(upper[var] > .90)]
to_drop
df.drop(df[to_drop], axis=1, inplace=True)
接下来,我们可以检查数字特征之间的相关性,并删除相关性非常高的特征。由您决定什么被认为是“高度相关”,但在这种情况下,我们将选择 90 及以上的相关性。请注意,在代码中,我们构建了一个相关矩阵,并将相关性转换为绝对值,以便处理负相关性。可以删除索赔设置日期标识、报告日期标识、雇主通知日期标识和最大医疗改善日期标识。
for var in df.columns:
print(var, 'percent of missing values', df[var].isnull().mean().round(3))
我们可以检查剩余要素的缺失值百分比,并移除任何缺失数据过多的要素。一个特性“事故源代码”有超过 50%的缺失值,但这不足以保证删除。
states = pd.DataFrame(df[['Claimant State', 'Accident State', 'Benefits State']])states
df.drop(['Claimant State', 'Accident State'], axis=1, inplace=True)
考察“索赔国”、“事故国”和“福利国”,我们发现绝大多数值是相同的。我们将保留“福利状态”,因为它包含最少的缺失值。
for var in df.columns:
print(var, df[var].unique(), '\n')
# Converting nulls to np.nan
for var in df.columns:
df[var].replace(to_replace=' ', value=np.nan, inplace=True)# Converting values of "X" to np.nan
for var in df.columns:
df[var].replace(to_replace='X', value=np.nan, inplace=True)# Splitting out "Accident Type Group" (ie. mixed type feature) into separate features
df['Accident Type Group num'] = df['Accident Type Group'].str.extract('(\d+)')
df['Accident Type Group str'] = df['Accident Type Group'].str[0:5]
df.drop(['Accident Type Group'], axis=1, inplace=True)
当我们检查每个特性的唯一值时,我们可以开始看到一些需要我们注意的差异。首先,我们注意到未转换为 Np.nan 的空白值或空值。接下来,我们看到许多要素的“X”值,这似乎是记录差异,记录数据的个人记录了带有“X”的缺失值。最后,名为“事故类型组”的特征是我们所说的混合类型,因为它包含字符串和数值。让我们将字符串和数值分离成各自的特征,并删除原来的“事故类型组”特征。
for var in df.columns:
print(var,'\n', df[var].value_counts()/len(df))
df.drop(['Neurology Payment Flag', 'Neurosurgery Payment Flag', 'Dentist Payment Flag',
'Psychiatry Payment Flag', 'Hand Surgery Payment Flag', 'Optometrist Payment Flag',
'Podiatry Payment Flag', 'Accident Type Group str', 'Post Injury Weekly Wage'], axis=1, inplace=True)df.drop(['HCPCS B Codes','HCPCS C Codes', 'HCPCS D Codes', 'HCPCS F Codes', 'HCPCS H Codes', 'HCPCS I Codes',
'HCPCS K Codes', 'HCPCS M Codes', 'HCPCS N Codes', 'HCPCS O Codes', 'HCPCS P Codes',
'HCPCS Q Codes', 'HCPCS R Codes', 'HCPCS S Codes', 'HCPCS T Codes', 'HCPCS U Codes',
'HCPCS V Codes', 'HCPCS X Codes', 'HCPCS Y Codes', 'HCPCS Z Codes', 'ICD Group 1',
'ICD Group 2', 'ICD Group 3', 'ICD Group 4', 'ICD Group 5', 'ICD Group 7', 'ICD Group 8',
'ICD Group 9', 'ICD Group 10', 'ICD Group 11', 'ICD Group 12', 'ICD Group 14', 'ICD Group 15',
'ICD Group 16', 'ICD Group 17', 'ICD Group 20', 'NDC Class - Benzo', 'NDC Class - Misc (Zolpidem)',
'NDC Class - Stimulants'], axis=1, inplace=True)
现在让我们把注意力转向基数或每个特性的唯一值/类别的数量。像“周工资”这样的连续特征无疑会有数百甚至数千个独特的类别。标称和离散特征(即性别和受抚养人人数)的种类数量要少得多。本练习的目标是确定是否有任何类别拥有大多数(90%+)的值。如果一个特性包含一个或两个包含 90%以上值的类别,那么数据中就没有足够的可变性来保留该特性。最终由你来决定截止值,但我们认为 90%或更高是一个安全的假设。
特征特性
现在,我们已经成功地消除了许多由于高度相关性、重复值和缺乏可变性而导致的特征,我们可以专注于检查特征特性并决定如何解决每个问题。
你会注意到,在这一部分,我们只是简单地识别问题并做一个心理记录。我们将不会实际应用讨论的变化,直到笔记本结束到一个功能工程管道。
categorical = ['Benefits State', 'Industry ID', 'Claimant Sex', 'Claimant Marital Status', 'Employment Status Flag', 'RTW Restriction Flag', 'NCCI Job Code',
'Surgery Flag', 'Disability Status', 'SIC Group', 'NCCI BINatureOfLossDescription', 'Accident Source Code',
'Orthopedic Surgery Payment Flag','Accident Type Group num']discrete = ['Claimant Age', 'Number Dependents', 'Percent Impairment', 'HCPCS A Codes',
'HCPCS E Codes', 'HCPCS G Codes', 'HCPCS J Codes','HCPCS L Codes', 'HCPCS W Codes',
'ICD Group 6', 'ICD Group 13','ICD Group 18', 'ICD Group 19', 'ICD Group 21',
'CPT Category - Anesthesia', 'CPT Category - Eval_Mgmt', 'CPT Category - Medicine',
'CPT Category - Path_Lab', 'CPT Category - Radiology', 'CPT Category - Surgery',
'NDC Class - Muscle Relaxants']continuous = ['Accident DateID', 'Weekly Wage']df.columns,'Number of Features:',len(df.columns)
缺少值
for var in df.columns:
if df[var].isnull().sum() > 0:
print(var, df[var].isnull().mean())
根据我们之前对缺失值的观察,我们发现只有一个要素包含超过 50%的缺失值,而绝大多数要素不包含任何缺失数据。也就是说,我们确实有一些包含缺失数据的要素,我们需要确定如何处理这个问题,因为许多 ML 算法需要完全干净的数据集。这是一个广泛的话题,我们不希望在这个博客中涵盖错综复杂的内容,但读者应该熟悉这个话题。首先,人们必须适应各种类型的缺失数据,如“完全随机缺失”、“随机缺失”和“非随机缺失”。这些主题有助于确定应该如何以及何时处理丢失的数据。此外,进一步解读插补技术,如均值/中值/众数、任意值插补、添加缺失数据指标、随机样本插补、最大似然插补等。将提供一个很好的概述。
我们有 9 个缺失数据的特征。对于缺失值少于 5%的特征(即申请人性别、申请人婚姻状况、就业状况标志、环球旅行限制标志)我们将用其分布模式替换缺失值。由于缺失值的百分比较低,模式插补不会对分布造成太大的改变。“事故日期”是我们唯一具有缺失数据的连续特征,我们将使用任意数字-99999 来估算缺失值。所有其他特征本质上都是分类的,由于它们有超过 5%的缺失值,我们将用字符串“missing”来估算缺失值。这最终不会改变发行版,只会给他们的发行版增加一个新的类别。
事故日期 ID:连续 w/ -99999
索赔人性别:分类 w/模式
索赔人婚姻状况:分类 w/模式
就业状况标志:分类 w/模式
RTW 限制标志:分类 w/模式
残疾状况:分类 w/‘失踪’
NCCI binatureofloss 描述:分类 w/‘失踪’
事故来源代码:分类 w/‘失踪’
事故类型组号:分类 w/‘失踪’
分类和离散特征的基数
for var in categorical:
print(var, 'has', df[var].nunique(), 'unique categories')
for var in discrete:
print(var, 'has', df[var].nunique(), 'unique categories')
for var in categorical:
print(df[var].nunique(),(df.groupby(var)[var].count()/len(df)))
for var in discrete:
print(var, 'has', df[var].nunique(), 'unique categories')
在上一节中,我们查看了基数,以便删除可变性低的特性(即具有包含大部分数据的类别的特征)。我们需要更深入地检查基数,并识别“稀有”类别。换句话说,哪些类别只包含非常小百分比的数据(=<1%)。我们将把所有的类别聚合成一个“稀有”的类别,从而减少每个特性的基数,简化模型。当我们讨论编码分类和离散特征时,这种方法将非常有用。
例如,功能“就业状态标志”目前有 13 个类别(包括 np.nan ),但正如您所见,“F =全职”和“P =兼职”类别几乎占了数据的 96%。所有其他类别仅出现 0.5%的时间,它们都将被汇总为“罕见”类别。任何类别出现次数少于 1%的分类或离散特征都将这些类别编码为“稀有”。
分布和异常值
该数据集仅包含两个连续特征“事故日期”和“周工资”。我们需要确定这些特征是否包含偏斜分布,以及它们是否包含任何异常值。
for var in continuous:
plt.figure(figsize=(10,5))
plt.subplot(1,2,1)
fig = df.boxplot(column=var)
fig.set_title('outliers')
fig.set_ylabel(var)
plt.subplot(1,2,2)
fig = df[var].hist(bins=20)
fig.set_ylabel('number of cases')
fig.set_xlabel(var)
plt.show()
这两个特征当然保持了偏态分布,但只有“周工资”包含任何异常值。最大似然算法对数据有某些假设,我们需要遵循这些假设来增加它们的预测能力。例如,线性回归假设预测值和目标值之间的关系是线性的(线性)。它还假设数据中没有异常值。对于高度相关的要素(多重共线性),这尤其困难。最后,它假设您的要素呈正态分布。
正态分布的要素遵循高斯分布,您可能还记得高中统计课程中的类似钟形的分布。如你所见,“事故日期”和“周工资”都不是正态分布的。有几种常用的方法来修复偏斜分布,如对数、倒数、平方根、box-cox 和 yeo-johnson 变换。
df_copy = df.copy()df_copy['Accident DateID'].skew(skipna = True)df_copy['Accident DateID_log'] = df_copy['Accident DateID'].map(lambda x: np.log(x))df_copy['Accident DateID_rec'] = df_copy['Accident DateID'].map(lambda x: np.reciprocal(x))df_copy['Accident DateID_sqrt'] = df_copy['Accident DateID'].map(lambda x: np.sqrt(x))df_copy['Accident DateID_log'].skew(skipna = True)
df_copy['Accident DateID_rec'].skew(skipna = True)
df_copy['Accident DateID_sqrt'].skew(skipna = True)df['Accident DateID_rec'] = df['Accident DateID'].map(lambda x: np.reciprocal(x))df.drop(['Accident DateID'], axis=1, inplace=True)
“事故日期”的校正偏差如下:
事故日期:0.137
事故日期 _ 日志:0.111
事故日期 _ 记录:0.00
事故日期 ID_sqrt: 0.124
我们可以看到“事故日期”的初始偏差为 0.137,从技术上讲,这并不是很大的偏差,因为正态分布的偏差为零(0)。也就是说,应用倒数变换将我们的偏斜调整为零(0)。
def diagnostic_plot(df, variable):
plt.figure(figsize=(16, 4))
sns.distplot(df[variable], bins=30)
plt.title('Histogram')diagnostic_plot(df_copy, 'Accident DateID_rec')
df_copy['Weekly Wage'].skew(skipna = True)df_copy['Weekly Wage_log'] = df_copy['Weekly Wage'].map(lambda x: np.log(x) if x > 0 else 0)df_copy['Weekly Wage_rec'] = df_copy['Weekly Wage'].map(lambda x: np.reciprocal(x) if x > 0 else 0)df_copy['Weekly Wage_sqrt'] = df_copy['Weekly Wage'].map(lambda x: np.sqrt(x))
df_copy['Weekly Wage_log'].skew(skipna = True)
df_copy['Weekly Wage_rec'].skew(skipna = True)
df_copy['Weekly Wage_sqrt'].skew(skipna = True)df['Weekly Wage_sqrt'] = df['Weekly Wage'].map(lambda x: np.sqrt(x))df.drop(['Weekly Wage'], axis=1, inplace=True)
修正后的“周工资”偏差如下:
周工资:2.57 英镑
周工资 _ 日志:-3.74
周工资 _ 收入:54.37 英镑
周工资 _sqrt: 0.407
“周工资”的初始偏差要大得多,为 2.56,但平方根变换使偏差显著下降(0.40)。
现在我们已经修复了偏斜度,让我们来解决位于“Weekly Wage_sqrt”中的异常值,因为我们已经放弃了原来的特性。由于“周工资 _sqrt”呈正态分布,我们可以使用“平均值的 3 倍标准差”规则来识别异常值。如果您的分布是偏态的,您最好先计算分位数,然后计算 IQR 来确定您的上下边界。
值得注意的是,连续特征,如“事故日期”和“每周工资 _sqrt”通常可以从离散化或宁滨中受益。离散化需要将特征值分成组或箱。这种方法也是处理异常值的有效方法,因为它们通常更接近分布的平均值。这将最终将特征从连续变为离散,因为最终结果将是每个箱中的观察数量(即 0–1000、1000–2000、2000–5000 等。).
def find_boundaries(df, variable):# calculate the boundaries anything outside the upper and lower boundaries is an outlier
upper_boundary = df[variable].mean() + 3 * df[variable].std()
lower_boundary = df[variable].mean() - 3 * df[variable].std()return upper_boundary, lower_boundaryupper_boundary, lower_boundary = find_boundaries(df, 'Weekly Wage_sqrt')upper_boundary, lower_boundary
上限为 51.146,下限为-0.763。换句话说,任何超出这些边界的值都将被视为异常值。请注意,我们使用三(3)标准偏差规则来确定异常值。我们可以将该值更改为 2,我们的边界会缩小,从而导致更多的异常值。
print('We have {} upper boundary outliers:'.format(len(df[df['Weekly Wage_sqrt'] > upper_boundary])))print('We have {} lower boundary outliers:'.format(len(df[df['Weekly Wage_sqrt'] < lower_boundary])))
# Identify the outliers in 'weekly wage_sqrt'
outliers_weekly_wage = np.where(df['Weekly Wage_sqrt'] > upper_boundary, True, np.where(df['Weekly Wage_sqrt'] < lower_boundary, True, False))# trim the df of the outliers
df = df.loc[~(outliers_weekly_wage)]
不平衡的目标分布
df['Opiods Used'].value_counts()/len(df)
我们的目标是“使用过的 Opiods ”,和大多数分类问题一样,就纯粹的数量而言,错误类往往占大多数。正如你在上面看到的,几乎 90%的案例都是假的或者没有滥用阿片类药物。一些最大似然算法如决策树倾向于使它们的预测偏向多数类(即在我们的情况下为假)。有许多技术可以用来解决这个问题。过采样是一种尝试将少数类的随机副本添加到数据集直到不平衡消除的技术。欠采样与过采样相反,因为它需要移除多数类观测值。主要的缺点是删除数据的想法会导致模型的欠拟合。最后但同样重要的是,合成少数过采样技术(SMOTE)使用 KNN 算法生成新的观测值来消除不平衡。我们将在这个用例中使用 SMOTE 技术来生成新的(合成的)观察结果。
需要注意的是,所使用的任何技术都应该仅用于生成或消除训练集中的观察值。
预处理流水线
我们需要将我们的数据分成训练和测试数据集,但首先让我们将我们的特征转换成适当的格式,以便符合我们管道的要求。
df[discrete] = df[discrete].astype('O')
df['Industry ID'] = df['Industry ID'].astype('O')
df['Surgery Flag'] = df['Surgery Flag'].astype('O')
df['Accident Source Code'] = df['Accident Source Code'].astype('O')
df['Orthopedic Surgery Payment Flag'] = df['Orthopedic Surgery Payment Flag'].astype('O')
df['Orthopedic Surgery Payment Flag'] *= 1
df['Opiods Used'] *= 1X_train, X_test, y_train, y_test = train_test_split(
df.drop('Opiods Used', axis=1),
df['Opiods Used'],
test_size=0.3,
random_state=SEED)
为了简化缺失数据、稀有值、基数和编码的数据处理任务,我们将利用 Scikit-Learn 的 make_pipeline 库。管道允许我们将多个进程应用到一段代码中,这段代码将依次运行每个进程。使用管道使我们的代码更容易理解,可重复性更好。
**# Imputing missing values for continuous features with more than 5% missing data with -99999.**
feature_transform = make_pipeline(mdi.ArbitraryNumberImputer(arbitrary_number = -99999, variables='Accident DateID_rec'),
**# Imputing categorical (object) features w/ more than 5% of nulls as 'missing'** mdi.CategoricalVariableImputer(variables=['Disability Status',
'NCCI BINatureOfLossDescription','Accident Source Code','Accident Type Group num'], imputation_method='missing'),
**# Imputing categorical features w/less than 5% of missing values with the mode** mdi.CategoricalVariableImputer(variables=['Claimant Sex', 'Claimant Marital Status','Employment Status Flag','RTW Restriction Flag'],
imputation_method='frequent'),
**# Encoding rare categories for categorical and discrete features (Less than 1% is rare)** ce.RareLabelCategoricalEncoder(tol=0.01, n_categories=6,
variables=['Benefits State', 'Industry ID', 'NCCI Job Code','Employment Status Flag', 'SIC Group', 'NCCI BINatureOfLossDescription','Accident Type Group num', 'Claimant Age', 'Number Dependents', 'Percent Impairment', 'HCPCS A Codes', 'HCPCS E Codes', 'HCPCS G Codes', 'HCPCS J Codes', 'HCPCS L Codes','HCPCS W Codes', 'ICD Group 6', 'ICD Group 13','ICD Group 18', 'ICD Group 19', 'ICD Group 21','CPT Category - Anesthesia', 'CPT Category - Eval_Mgmt','CPT Category - Medicine', 'CPT Category - Path_Lab','CPT Category - Radiology', 'CPT Category - Surgery',
'NDC Class - Muscle Relaxants']),
**# We will use one_hot_encoding for categorical features**
ce.OneHotCategoricalEncoder(variables=['Benefits State', 'Industry ID', 'Claimant Sex','Claimant Marital Status','Employment Status Flag','RTW Restriction Flag','Disability Status','SIC Group','NCCI Job Code','NCCI BINatureOfLossDescription','Accident Source Code','Accident Type Group num'],drop_last=True),
**# We are going to use ordinal encoding according to the target mean to the target feature** ce.OrdinalCategoricalEncoder(encoding_method='ordered',variables=['Claimant Age','Number Dependents','Percent Impairment','HCPCS A Codes', 'HCPCS E Codes','HCPCS G Codes','HCPCS J Codes', 'HCPCS L Codes', 'HCPCS W Codes','ICD Group 6', 'ICD Group 13', 'ICD Group 18', 'ICD Group 19','ICD Group 21', 'CPT Category - Anesthesia',
'CPT Category - Eval_Mgmt','CPT Category - Medicine','CPT Category - Path_Lab','CPT Category - Radiology','CPT Category - Surgery','NDC Class - Muscle Relaxants']))
我们的数据处理管道广泛使用了“特征引擎”库。我们可以使用 Scikit-Learn 来完成这些任务,但是我们想指出的是,feature-engine 具有某些优势。首先,基于 scikit-learn、pandas、numpy 和 SciPy 构建的 feature-engine 能够返回 pandas 数据帧,而不是像 scikit-learn 那样返回 Numpy 数组。其次,特征引擎转换器能够学习和存储训练参数,并使用存储的参数转换您的测试数据。
关于特征引擎已经说得够多了,让我们更详细地讨论一下管道。理解流水线中的步骤是连续运行的,从顶部的变压器开始,这一点很重要。学生通常会犯这样的错误,即应用第一步最终会改变数据的结构或第二步无法识别的特征的名称。
第一个转换器“ArbitraryNumberImputer”使用值-99999 对丢失数据超过 5%的连续要素进行估算。第二个转换器“CategoricalVariableImputer”使用字符串值“missing”对缺失数据超过 5%的分类数据进行估算。第三个转换器“FrequentCategoryImputer”用特征的模式估算缺失数据少于 5%的分类数据。第四个转换器“RareLabelCategoricalEncoder”将出现时间不到 1%的分类和离散特征观察值编码到一个名为“稀有”的新类别中。
第五个转换器“OneHotCategoricalEncoder”将每个分类要素的每个唯一值转换为存储在新要素中的二进制形式。比如“性别”有“M”、“F”、“U”的值。一个热编码将产生三个(或两个“k-1”取决于您的设置)新功能(即性别 _M,性别 _F,性别 _U)。如果在原始“性别”特征下观察值为“M ”,则“性别 _M”的值为 1,“性别 _F”和“性别 _U”的值为 0。由于这种方法极大地扩展了特征空间,现在您理解了为什么将稀有观察值(< 1%)归类为“稀有”非常重要。
最终的转换器“OridinalCategoricalEncoder”专门用于对离散特征进行编码,以保持它们与目标特征的有序关系。利用该编码器将会为测试集中存在的、没有在训练集中编码的类别生成缺失值或抛出错误。这也是在编码序数/离散特征之前处理稀有值的另一个原因。为了更好地理解这个顺序编码器,让我们检查一下“申请人年龄”特性。我们有三个潜在值“F”,“M”和“U”。让我们假设“F”的阿片类药物滥用率平均为 10%,“M”为 25%,“U”为 5%。编码器将根据他们平均阿片类药物滥用的程度将“M”编码为 1,“F”编码为 2,“U”编码为 3。
注意扩展到 155 个特征的特征空间
feat_transform.fit(X_train, y_train)
X_train_clean = feat_transform.transform(X_train)
X_test_clean = feat_transform.transform(X_test)X_train_clean.head()
接下来,将管道安装到 X_train 和 y_train 上,并转换 X_train 和 X_test。请注意,我们的特征空间已经大大增加到 155 个特征,这是由于我们对分类特征使用了一次性编码器。
特征缩放
最后,我们必须缩放特征,以使它们的所有值都在相同的范围或量级上。这个步骤必须完成,因为一些 ML 分类器使用欧几里德距离,并且具有更高幅度或范围的特征将对预测有更大的影响。例如温度,32 华氏度等于 273.15 开尔文,如果我们在模型中使用这两个特征,开尔文将具有更大的权重或影响预测。
scaler = StandardScaler()
scaler.fit(X_train_clean)X_train_std = scaler.transform(X_train_clean)
X_test_std = scaler.transform(X_test_clean)
X_train_std_df = pd.DataFrame(X_train_std, columns=col_names)
X_test_std_df = pd.DataFrame(X_test_std, columns=col_names)
基线模型
models = []
models.append(('log_reg', LogisticRegression(max_iter=10000, random_state=42)))
models.append(('rf_classifer', RandomForestClassifier(random_state=42)))
models.append(('bayes', GaussianNB()))
models.append(('gbc', GradientBoostingClassifier()))base_model_train = []
base_model_test = []for name, classifier in models:
scores = cross_val_score(classifier, X_train_std_df, y_train, cv=5, scoring='recall')
base_model_train.append(scores.mean().round(4))
print(scores)
print('{}: Avg CV recall using all features on training data: {}'.format(name, scores.mean().round(4)))
classifier.fit(X_train_std_df, y_train)
y_preds = classifier.predict(X_test_std_df)
test_recall = recall_score(y_test, y_preds, average='binary')
test_class = classification_report(y_test, y_preds)
cnf_matrix = confusion_matrix(y_test, y_preds)
base_model_test.append(test_recall.round(4))
print('{}: Recall w/all features on test data {}:'.format(name, test_recall.round(4)))
print(test_class)
print(cnf_matrix)
print('-------------------------------------------------------')
我们将比较四个不同分类器的相对召回分数。我们使用召回是因为我们想尽量减少假阴性(即滥用阿片类药物但预测不会滥用)。首先,我们希望建立一个基线,可以与分类器的额外迭代进行比较,以确定相对的改进。基线模型包括具有不平衡目标的整个特征空间。为了正确评估我们的分类器,我们将使用仅应用于训练数据集的 5 重分层交叉验证,从而大大减少数据泄漏。换句话说,每个分类器将在训练数据的五个唯一分割上被训练和测试 5 次。将为每个分类器计算五个独特的召回分数,并一起平均以产生最终召回分数。分类器在训练期间不会“看到”任何测试数据。最后,每个分类器将在保留的测试数据集上进行测试,以确定可推广性和过度拟合。
逻辑回归、随机森林和梯度推进分类器已经实现了很高的整体准确性。然而,朴素贝叶斯设法实现了最高的召回率,因为它只有 331 个假阴性预测。
如果您希望阅读有关分类指标的更多信息( 链接)
模型 1:完整的功能集和平衡的 SMOTE
sm = SMOTE(sampling_strategy='auto', k_neighbors=5, random_state=42)
X_train_std_sm, y_train_sm = sm.fit_resample(X_train_std_df, y_train)model1_train = []
model1_test = []for name, classifier in models:
pipeline = make_pipeline(sm, classifier)
scores = cross_val_score(pipeline, X_train_std_df, y_train, cv=5, scoring='recall')
model1_train.append(scores.mean().round(4))
print(scores)
print('{}: Avg CV Recall w/All Reatures: {}'.format(name, scores.mean().round(4)))
classifier.fit(X_train_std_sm, y_train_sm)
y_preds = classifier.predict(X_test_std_df)
test_recall = recall_score(y_test, y_preds)
test_class = classification_report(y_test, y_preds)
cnf_matrix = confusion_matrix(y_test, y_preds)
model1_test.append(test_recall.round(4))
print('{}: Recall w/All Features on test data {}:'.format(name, test_recall.round(4)))
print(test_class)
print(cnf_matrix)
print('-------------------------------------------------------')
不平衡的目标给我们的预测强加了偏见。正如预期的那样,逻辑回归得到了很大的改进,因为该算法在平衡目标的情况下表现得更好。
谈 SMOTE 及其方法论。如果你还记得我们最初对数据集的检查,目标变量是不平衡的。与导致阿片类药物滥用的观察结果(10%)相比,我们有更多未导致阿片类药物滥用的观察结果(89%)。我们还决定使用 SMOTE 方法,因为它创建了少数类的新的合成观测值,而不是复制现有的观测值。SMOTE 使用 KNN(通常 k=5 ),其中从少数类中选择一个随机观察值,并找到最近邻居的 k 。然后,随机选择 k 个邻居中的一个,并且从原始观察和随机选择的邻居之间的随机选择的点构建合成样本。
特征选择
随机森林特征重要性
自然语言处理和物联网等领域的现代数据集通常是高度多维的。看到数千甚至数百万个特征并不罕见。知道如何将特征缩小到选定的几个,不仅增加了我们找到可概括模型的机会,还减少了我们对昂贵的计算能力的依赖。通过精确减少数据中的特征/维度数量,我们最终从数据中去除了不必要的噪声。
值得注意的是,特征选择不仅包括特征空间的缩减,还包括特征创建。了解如何发现数据集中的趋势和要素之间的关系(即多项式特征)需要多年的实践,但在预测能力方面有很大的好处。有整个大学的课程致力于特性选择/工程,但那些对这个主题更感兴趣的人请研究过滤器、包装器和嵌入式方法作为介绍。
Scikit-Learn 的 RandomForestClassifier 具有“feature_importances_”属性,该属性用于确定数据集中每个要素的相对重要性。由于随机森林在平衡目标下往往表现更好,我们将使用 SMOTE 平衡 X_train_std_sm 和 y_train_sm 数据集。
rf_selector = RandomForestClassifier(n_estimators=100, random_state=SEED, n_jobs=-1)rf_selector.fit(X_train_std_sm, y_train_sm)feature_imp = pd.Series(rf_selector.feature_importances_, index=X_train_std_df.columns).sort_values(ascending=False)feature_imp[:30]
30 大特性
sum(feature_imp[0:30]
我们能够将大多数原始特性减少到 30 个,这占了性能差异的 91 % 。任何额外的特征只会增加非常小的额外预测能力。
X_train_rf = X_train_std_df[feature_imp[:30].index]
X_test_rf = X_test_std_df[feature_imp[:30].index]plt.figure(figsize=(12,8))
sns.barplot(x=feature_imp[0:30], y=feature_imp.index[0:30])
plt.xlabel('Feature Importance Score')
plt.ylabel('Features')
plt.title("Visualizing Important Features")
plt.grid(b=False, which='major', color='#666666', linestyle='-', alpha=0.2)
plt.show()
模型 2:射频特征和不平衡目标
models = []
models.append(('log_reg', LogisticRegression(max_iter=10000, random_state=42)))
models.append(('rf_classifer', RandomForestClassifier(random_state=42)))
models.append(('bayes', GaussianNB()))
models.append(('gbc', GradientBoostingClassifier()))model2_train = []
model2_test = []for name, classifier in models:
scores = cross_val_score(classifier, X_train_rf, y_train, cv=5, scoring='recall')
model2_train.append(scores.mean().round(3))
print(scores)
print('{}: Avg CV Recall on RF Features: {}'.format(name, scores.mean().round(3)))
classifier.fit(X_train_rf, y_train)
y_preds = classifier.predict(X_test_rf)
test_recall = recall_score(y_test, y_preds, average='binary')
test_class = classification_report(y_test, y_preds)
cnf_matrix = confusion_matrix(y_test, y_preds)
model2_test.append(test_recall.round(3))
print('{}: Recall w/RF features on test data {}:'.format(name, test_recall.round(3)))
print(test_class)
print(cnf_matrix)
print('-------------------------------------------------------')
模型 3: RF 特性和平衡 w/SMOTE
X_train_rf_sm, y_train_sm = sm.fit_resample(X_train_rf, y_train)models = []
models.append(('log_reg', LogisticRegression(max_iter=10000, random_state=42)))
models.append(('rf_classifer', RandomForestClassifier(random_state=42)))
models.append(('bayes', GaussianNB()))
models.append(('gbc', GradientBoostingClassifier()))sm = SMOTE(sampling_strategy='auto', k_neighbors=5, random_state=42)
skf = StratifiedKFold(n_splits=5)model3_train = []
model3_test = []for name, classifier in models:
pipeline = make_pipeline(sm, classifier)
scores = cross_val_score(pipeline, X_train_rf, y_train, cv=skf, scoring='recall')
model3_train.append(scores.mean().round(3))
print(scores)
print('{}: Avg CV Recall w/RF Reatures: {}'.format(name, scores.mean().round(3)))
classifier.fit(X_train_rf_sm, y_train_sm)
y_preds = classifier.predict(X_test_rf)
test_recall = recall_score(y_test, y_preds, average='binary')
test_class = classification_report(y_test, y_preds)
cnf_matrix = confusion_matrix(y_test, y_preds)
model3_test.append(test_recall.round(3))
print('{}: Recall w/RF on test data {}:'.format(name, test_recall.round(3)))
print(test_class)
print(cnf_matrix)
print('-------------------------------------------------------')
摘要
classifiers = ['Log_Regression', 'Random_Forest', 'Naive_Bayes', 'Gradient_Boosting_clf']idx = ['All_Feat_Imbalance_Train', 'All_Feat_Imbalance_Test','All_Feat_Smote_Train',
'All_Feat_Smote_Test','RF_Imbalance_Train',
'RF_Imbalance_Test','RF_Smote_Train','RF_Smote_Test']combined_results = pd.DataFrame([base_model_train,base_model_test,
model1_train, model1_test, model2_train,
model2_test, model3_train, model3_test],
columns=classifiers, index=idx)test_results = pd.DataFrame([base_model_test, model1_test, model2_test, model3_test], columns=classifiers, index=idx[1:8:2])print(test_results)
plt.figure(figsize=(15,10))
sns.lineplot(data=test_results[['Log_Regression', 'Random_Forest',
'Naive_Bayes', 'Gradient_Boosting_clf']])
plt.xlabel('Iterations', fontsize=17, labelpad=15)
plt.ylabel('Recall Scores', fontsize=17, labelpad=15)
plt.title('Classifier Recall Scores on Test Data',fontsize=25, pad=15)
plt.show()
特征的减少导致召回性能的轻微下降。这是意料之中的,因为仅使用 30 个特性就造成了 91%的性能差异。增加功能的数量肯定会提高召回率。只有朴素贝叶斯没有受到特征减少的影响。其次,我们数据集中的不平衡也影响了分类器的召回性能。一般来说,一旦不平衡得到纠正,大多数分类器都会提高它们的性能。逻辑回归受不平衡的影响最大。
就整体分类器性能而言,必须说所有分类器在特征减少以及目标平衡的情况下表现最佳。当然,有人可能会认为朴素贝叶斯表现最好,因为它设法实现了最好的测试召回率(0.949),但我会认为是逻辑回归胜过了领域。它实现了非常相似的召回率 0.945,相比之下,朴素贝叶斯的召回率为 0.949,而朴素贝叶斯仅解释了 14%的假阴性增加。更令人印象深刻的是,与朴素贝叶斯相比,它多了 2276 个正确的真阴性预测。在下一节中,我们将尝试超参数调优,看看我们能否提高逻辑回归模型的分类召回率。
具有 RF 特征和 SMOTE 的逻辑回归
超参数调谐
什么是超参数?
认为超参数是“调谐”旋钮。想象你正在编辑一张图片,以达到某种效果。您可以使用曝光、高光、阴影、对比度、亮度、饱和度、温暖度、色调等“旋钮”来调节画面。好吧,超参数是相同类型的“旋钮”,但用于分类器。
除了求解器、惩罚和 c 之外,逻辑回归没有太多的超参数需要调整。
解算器试图优化参数权重或θ,这将引出最小误差或成本函数。Sklearn 自带了几个解算器:newton-cg,lbfgs,liblinear。
惩罚参数也被称为“正则化”,其目的是帮助模型避免过度拟合训练数据,从而产生更通用的模型。它通过“惩罚”被认为是噪声或对模型贡献很小的特征来做到这一点。
- **L1 惩罚或套索:**将惩罚的绝对幅度或特征的权重之和添加到成本函数中。L1 将噪声特征减少到零,从而简化了模型。
- **L2 惩罚或岭:**将惩罚的平方幅度或特征权重的平方和添加到成本函数中。L2 也降低了噪声特征的影响,但是它们的权重或θ非常接近于零,但并不完全是零。
最后,“C”参数决定正则化惩罚的强度。C 参数越大,正则化程度越低,模型变得越复杂,过拟合增加。C 参数越小,应用的正则化越多,欠拟合增加。
clf_lr = LogisticRegression(max_iter=10000, random_state=SEED)penalty = ['l1','l2']
C = [0.001,0.002,0.003,0.005,1,10,100,1000]skf = StratifiedKFold(n_splits=5)
pipeline = make_pipeline(sm, clf_lr)
param_grid = dict(logisticregression__penalty=penalty,
logisticregression__C=C)grid = GridSearchCV(pipeline,
param_grid=param_grid,
scoring='recall',
verbose=1, cv=skf)grid_results = grid.fit(X_train_rf, y_train)
print('Best Score: ', grid_results.best_score_)
print('Best Params: ', grid_results.best_params_)
X_train_rf_sm, y_train_sm = sm.fit_resample(X_train_rf, y_train)
clf_lr = LogisticRegression(max_iter=10000, penalty='l2', C=0.0001, random_state=42)clf_lr.fit(X_train_rf_sm, y_train_sm)
y_preds = clf_lr.predict(X_test_rf)
test_recall = recall_score(y_test, y_preds).round(3)
test_class = classification_report(y_test, y_preds)
cm = confusion_matrix(y_test, y_preds)print('Log Regression Recall w/RF on test data {}:'.format(test_recall.round(3)))
print(test_class)
print(cm)
为了减少训练时间和警告数量,我们将重点调整“惩罚”和“C”参数。不仅要了解每个超参数的作用,还要了解参数之间如何相互作用,这对它们的调整至关重要。此外,调整过程通常是迭代的,因为我们从每个参数的宽范围开始,然后开始缩小范围,直到选择特定的参数值。
GridSearchCV 应用了一种穷举方法,因为它考虑了所提供参数的所有组合。由于平衡目标在分类器评估期间产生了最佳的召回,我们选择将其与传递到 gridsearchcv 的对数回归分类器一起包含到我们的管道中。这样,每个交叉验证训练/测试分割仅与其数据平衡。换句话说,在交叉验证期间,SMOTE 和分类器都没有遭受数据泄漏。
不幸的是,我们没有增加我们的训练召回率,但是我们能够将我们的测试召回率从 0.945(对数回归 w/RF 特征和 SMOTE)增加到 0.954。此外,我们将假阴性计数减少了 42。我们的假阳性计数增加了 269,但是再一次,当你实际上没有使用阿片类药物时,预测使用阿片类药物更好。
结论
作为一名数据科学家,理解数据科学分类问题的全部本质是你走向成熟的关键。我希望你发现这个教程信息丰富,容易理解。我欢迎任何反馈和建议,因为我们都只是在磨练我们的手艺。
所有代码都可以在我的 GitHub 上找到。链接
二元分类模型:澳大利亚信贷审批
预测信贷决策的二元分类器的 Python 实现。
作为我不断提高数据科学技能的努力的一部分,我在 UCI 机器学习知识库的随机数据集上建立机器学习模型。你可以在这里查看我以前的车型 。
纽约公共图书馆在 Unsplash 拍摄的照片
在这个挑战中,要解决的随机数据集是 澳大利亚信贷审批数据集。 如果你想跟随或者自己尝试数据集,你可以 下载 我已经清理过的 CSV 版本。
这些随机挑战的额外任务之一是将原始数据格式化为有用的 CSV 格式。因此,我将在我的 GitHub 上留下清理过的数据集的链接。
关于数据集:
数据集由 14 个特征变量和 1 个量化批准决定的类别标签组成。为了保密和便于处理统计算法,对 14 个特征本身知之甚少。
这意味着甚至特征名称都不存在,但是它们是连续的还是分类的是已知的。我同样标记了哪些列是连续的(N)、分类的©以及它们是否需要在数据集中进一步编码(C_enc)。
构建二元分类模型来预测信贷是被批准给特定客户还是被拒绝是非常简单的。
它包括:
数据预处理:一次热编码&标准化
构建分类器+评估模型性能:
查看每列直方图的类可分性:
下面的代码用每个独立变量和类标签绘制了一个直方图。这是为了辨别每个单独的变量在多大程度上可以解释类别的分离。
可以看出,几乎所有变量本身都不能解释批准决定。这意味着目标变量依赖于所有变量的非线性组合。下面是实现相同功能的代码。
度量&决定剧情:
以下是在测试集上评估时模型的精确度、10 倍交叉验证精确度的平均值和标准偏差。
模型精度
10 倍交叉验证后的准确度平均值
10 倍交叉验证后准确度范围内的标准偏差
此外,以下是如何用 seaborn 绘制决策计数图。该图有助于直观地显示已经提交了多少拒绝和批准。首先,您必须为绘图构建一个单独的数据框。
pred = list()
reject_count = approved_count = 0for i in range(len(y_pred)):
if y_pred[i] == 0:
pred.append('Rejections')
else:
pred.append('Approvals')pred = pd.DataFrame(pred)
pred.columns = ['Decisions']# Visualization of Decision Counts
plt.Figure(figsize = (8, 8))
sb.set_style('darkgrid')sb.countplot(pred['Decisions'], data = pred, edgecolor = 'black', linewidth=1.5, palette = 'dark')plt.title('Predicted Credit Approvals')
plt.xlabel('Approval Decision')
plt.ylabel('Count')
plt.show()
批准决定的计数图
原来如此。虽然这是一个简单的算法,模型的准确率高达 89%,但你可以看到变量之间的相关性是高度非线性的。
如果你有关于改进模型的建议,或者你可能会用不同的方式来处理它,请在下面的评论中留下。
感谢您的阅读,祝您愉快,我们会再见的。
灾难微博的二元分类
预测哪些推文是关于真实灾难的,哪些不是。
照片由 Yosh Ginsu 在 Unsplash 上拍摄
近年来,社交媒体因其在时空事件中的潜在用途而受到广泛关注。Twitter 是其中之一,它已经成为不同情况下的重要沟通渠道,例如在紧急情况下。智能手机使人们能够实时宣布他们看到的紧急情况。正因为如此,越来越多的机构对监控 Twitter 感兴趣(例如,救灾组织)。这项工作背后的想法是,我们可以从大量的推文中提取有用的摘要,这在灾难情况下可能是有用的。
数据集可以从这里下载。我们有 7613 条观察结果,我们正在预测一条给定的推文是否是关于一场真正的灾难。如果是,预测一个 1。如果没有,预测 0。功能及其描述如下所述。
id
-每条推文的标识符
text
-推文的文本location
——推文发出的地点(可能是南)keyword
-推文中的特定关键词(可能是 NaN)target
——判断一条推文是否是关于一场真正的灾难(1
)的输出结果(0
)
但是我们如何分析这类问题呢?
数据
让我们看看数据集中包含的要素:
There are 7613 observations and 5 features in this dataset.
如图所示,该数据集由 7613 条评论组成。在这个数据集中有五种不同的特性,但是我们只分别使用“文本”和“目标”列作为输入/输出。现在,让我们看看数据集中包含的要素:
在关键字和位置列中有许多丢失的值,但是我们不需要担心它们。为了使我们的生活更容易,我们需要清除文本特征中的噪声。我们将在下面的部分中讨论它。
数据探索
如果我们将灾难性推文的数量与非灾难性推文的数量对比如下,很明显数据集是不平衡的。
为了解决这个问题,我们将使用分层抽样,其中数据集被划分为称为地层的同质子组,并且从每个地层中抽样正确数量的实例,以保证测试集能够代表总体。我们可以看到,训练和测试数据集都有近 41%的灾难推文。分层抽样的代码如下。
训练和测试数据集不平衡。但是有几种方法可以处理不平衡的数据集,如过采样和欠采样。幸运的是,这里的许多应用算法可以为您解决这个问题。有一个名为“ class_weight ”的超参数使用目标的值来自动调整与数据集中的类频率成反比的权重。
让我们看看熊猫的每个特征的类型。info()函数,它也可以让您对特性有一个大致的了解。了解每一列是否有任何缺失值会很有帮助,这样您将能够有效地处理它。
train.info()
由于我们只处理两列,即(“文本”和“目标”),并且没有缺失值,因此我们不需要移除任何观察值或使用插补转换器来完成缺失值。
文本表示
下面显示了几条推文。请记住,我们将使用它们来预测是否有灾难推文。究竟什么能帮助我们去掉多余的单词、数字等。不会给文本增加任何价值,例如,标签、https、数字。因此,我们将删除干扰我们的文本分析的字符数字和文本片段。
使用下面的代码,我删除了数字、非 ASCII 字符、标点符号和 https,并将所有文本改为小写。
下面,你可以看到几个干净的文本,我们将用于文本分析。
我们的目标是找到输入和输出之间的关系。我使用“已清理”特征作为输入,使用“目标”作为输出。所以我的第一步是把推文转换成矢量。
我们需要将 tweets 转换为令牌计数矩阵,以便从文本内容中提取数字特征。这样,计数的稀疏表示就产生了。这是非常有益的,因为你可以想象独特的映射词到向量创建一个矩阵的巨大规模。CountVectorizer 用于此目的,它可以按如下方式导入。
**from** **sklearn.feature_extraction.text** **import** CountVectorizer
有许多像“the”、“them”、“are”这样的词对上下文的意思没有任何影响,这些词被称为停用词。它们不提供信息,可以通过选择 step_words='english '作为 CountVectorizer 函数中的超参数来删除。
下一步是使用 tf-idf 表示来规范化计数矩阵。标准化频率而不是使用原始频率的主要原因是为了减少在文本中出现几次的标记的影响,并且没有出现几次的标记信息量大。例如,单词“document”在给定的语料库中出现一千次,而“awesome”出现两次。tf-idf 在这种情况下工作,就像预处理数据,将原始特征向量变成更适合机器学习算法的表示。
**from** **sklearn.feature_extraction.text** **import** TfidfTransformer
管道
我使用管道功能来完成所有步骤。顺序应用变换列表和最终估计器。因此,它开始询问 CountVectorizer 和 Tfidf。您可以拥有管道工作所需的任意数量的变压器。
我只在“cleaned”特性上应用了管道,因为目标是二进制的。
模型评估
我使用了 20%的数据进行测试,其余的用于训练。这里涉及到很多超参数。因此,你的工作就像一个建筑师,寻找最佳的价值,以最大限度地提高准确性。一些技术是有益的,包括 Hyperopt、GridSearchCV 和 RandomizedSearchCV。在这个问题中,我随机选取了超参数。
我使用了下面代码片段中提到的十二种不同的监督算法。曲线下面积(AUC)分数和训练时间用于比较。
一个接一个地,每个监督算法都在训练数据集上训练,然后在测试数据集上推广。最后,根据 AUC 值对结果进行排序。还显示了每种方法的培训时间。我已经用这个内核写下了下面的分类代码。
我们可以看到,线性支持向量机有一个最大的 AUC 分数,但没有最小的训练时间。由于我们没有优化超参数,您可以更改它们来平衡精度和速度。
结论
SVM 没有在最快的时间内训练出来。尽管超参数没有被优化,文本清理可以减少文本数据集中的非索引词、标点符号和数字等形式的噪声。在现实世界的问题中,速度比准确性更重要。因此,随着数据量的增加,我们可能需要关注速度。深度学习也可以用于比较。一些算法也使用像 Lightgbm 这样的 GPU,所以如果你有大量的数据,你可以考虑使用这种方法。
本文使用的所有代码都可以从我的 GitHub 中访问。我期待听到反馈或问题。
IMDB 电影评论的二元分类
使用 Keras 根据情感对评论进行分类。
二元分类是指将样本分为两类。
在这个例子中,我们将设计一个神经网络来执行来自 IMDB 电影评论数据集的评论的两类分类,或二元分类,以确定评论是正面的还是负面的。我们将使用 Python 库 Keras。
IMDB 电影评论分类。(来源:GitHub)
如果您正在寻找一个更基本的问题,请查看解决 MNIST 数据集。接下来的内容主要建立在解决 MNIST 问题上,即“你好,世界!深度学习。
“你好,世界!”深度学习和 Keras
towardsdatascience.com](/solve-the-mnist-image-classification-problem-9a2865bcf52a)
IMDB 数据集
IMDB 数据集是一组来自互联网电影数据库的 50,000 条高度极化的评论。它们被分成 25000 条评论,每条评论用于训练和测试。每组包含相同数量(50%)的正面和负面评论。
IMDB 数据集与 Keras 打包在一起。它由评论及其对应的标签组成(0 代表负面,1 代表正面评论)。评论是一个单词序列。它们被预处理成整数序列,每个整数代表字典中的一个特定单词。
IMDB 数据集可以直接从 Keras 加载,通常会在您的机器上下载大约 80 MB。
加载数据
让我们从 Keras 加载预打包的数据。我们将只包括 10,000 个最频繁出现的单词。
加载和分析输入数据
为了好玩,我们来解码第一篇评论。
解读一篇评论
准备数据
我们无法将整数列表输入到我们的深层神经网络中。我们需要把它们转换成张量。
为了准备我们的数据,我们将对我们的列表进行一次性编码,并将其转换为 0 和 1 的向量。这将把我们的所有序列放大为 10,000 维向量,在对应于该序列中存在的整数的所有索引处包含 1。这个向量在所有索引处都有元素 0,这在整数序列中是不存在的。
简单来说,每个评论对应的 10000 维向量会有
- 每个索引对应一个单词
- 每一个值为 1 的索引都是评论中出现的一个词,用它的整数对应物来表示。
- 每一个包含 0 的索引都是评论中没有的词。
我们将手动矢量化我们的数据,以获得最大的清晰度。这将产生一个形状张量(25000,10000)。
预处理输入数据
构建神经网络
我们的输入数据是需要映射到定标器标签(0 和 1)的向量。这是最简单的设置之一,一个简单的全连接、密集的层与 relu 激活的堆栈表现相当好。
隐藏层
在这个网络中,我们将利用隐藏层。我们将这样定义我们的层。
Dense(16, activation**=**'relu')
传递给每个Dense
层的参数(16)
是一层的隐藏单元的数量。
在一系列张量运算之后,生成了激活了 relu 的密集层的输出。这个操作链实现为
output = relu(dot(W, input) + b)
其中,W
是权重矩阵,b
是偏差(张量)。
具有 16 个隐藏单元意味着矩阵 W 将具有(输入 _ 尺寸、 16 )的形状。在这种情况下,输入向量的维数是 10,000,权重矩阵的形状将是(10000,16)。如果你把这个网络用图表来表示,你会在这个隐藏层看到 16 个神经元。
通俗地说,这一层会有 16 个球。
这些球或隐藏单元中的每一个都是层的表示空间中的一个维度。表示空间是数据的所有可行表示的集合。由它的隐藏单元组成的每一个隐藏层的目的是从数据中学习一个特定的数据变换或一个特征/模式。
DeepAI.org 有一篇关于隐藏层的非常翔实的报道。
简单地说,隐藏层是数学函数的层,每一层都被设计成产生特定于预期结果的输出。
隐藏层允许将神经网络的功能分解为特定的数据转换。每个隐藏层函数都专门用于产生定义的输出。例如,用于识别人的眼睛和耳朵的隐藏层函数可以与后续层结合使用,以识别图像中的面部。虽然单独识别眼睛的功能不足以独立识别物体,但它们可以在神经网络中共同发挥作用。
ReLU 激活功能。这是最常用的激活功能之一。
模型架构
对于我们的模型,我们将使用
- 两个中间层,每个中间层有 16 个隐藏单元
- 将输出标量情感预测的第三层
- 中间层将使用 relu 激活功能。 relu 或整流线性单位函数将负值清零。
- 最终层或输出层的 Sigmoid 激活。sigmoid 函数"*将"*任意值压缩到[0,1]范围内。
乙状结肠激活函数。(来源:维基百科,Qef)
在选择模型的架构属性时,有正式的原则指导我们的方法。这些不包括在本案例研究中。
定义模型架构
编译模型
在这一步,我们将选择一个优化器、一个损失函数和指标进行观察。我们将继续前进
- 二元 _ 交叉熵损失函数,常用于二元分类
- rmsprop 优化器和
- 准确性作为绩效的衡量标准
我们可以将我们对优化器、损失函数和指标的选择作为字符串传递给compile
函数,因为rmsprop
、binary_crossentropy
和accuracy
是与 Keras 打包在一起的。
model.complie(optimizer**=**'rmsprop',
loss **=** 'binary_crossentropy',
metrics **=** ['accuracy'])
人们可以通过将定制的类实例作为参数传递给loss
、optimizer
或mertics
字段来使用定制的损失函数或优化器。
在这个例子中,我们将实现我们的默认选择,但是我们将通过传递类实例来实现。如果我们有定制的参数,这正是我们要做的。
编译模型
设置验证
我们将留出一部分训练数据,用于在训练时验证模型的准确性。一个验证集使我们能够监控我们的模型在训练期间经历历元时在以前看不到的数据上的进展。
验证步骤帮助我们微调model.fit
函数的训练参数,以避免数据的过拟合和欠拟合。
为模型定型设置验证集
训练我们的模型
最初,我们将在 512 个样本的小批量中为 20 个时期训练我们的模型。我们还将把我们的验证集传递给fit
方法。
调用fit
方法返回一个History
对象。该对象包含一个成员history
,它存储了训练过程的所有数据,包括随着时间的推移可观察到的或监控到的量的值。我们将保存该对象,以确定更好地应用于训练步骤的微调。
训练模型。Google Colab GPU 对应的时间。在 CPU、i7 上通常需要大约 20 秒
在训练结束时,我们达到了 99.85%的训练准确率和 86.57%的验证准确率
现在我们已经训练了我们的网络,我们将观察存储在History
对象中的性能指标。
调用fit
方法返回一个History
对象。这个对象有一个属性history
,这是一个包含四个条目的字典:每个被监控的指标一个条目。
培训过程的历史。
history_dict
包含以下值
- 培训损失
- 训练准确性
- 验证损失
- 验证准确性
在每个时期结束时。
让我们使用 Matplotlib 并排绘制训练和验证损失以及训练和验证准确性。
从训练历史中得到的损失和准确性数据的分析。这些数据告诉我们我们的训练策略的表现。
我们观察到最小验证损失和最大验证准确度在大约 3-5 个时期达到。之后,我们观察到两个趋势:
- 验证损失增加,培训损失减少
- 验证准确性降低,培训准确性提高
这意味着该模型在对训练数据的情绪进行分类方面越来越好,但当它遇到新的、以前从未见过的数据时,总是做出更差的预测。这是过度拟合的标志。在第 5 个时期之后,模型开始太接近训练数据。
为了解决过度拟合的问题,我们将把历元的数量减少到 3 到 5 之间。这些结果可能会因您的机器而异,并且由于不同型号之间随机分配重量的本质可能会有所不同。
在我们的情况下,我们将在 3 个纪元后停止训练。
重新训练我们的神经网络
我们重新训练我们的神经网络的基础上,我们的研究结果,从历史的损失和准确性的变化。这次我们运行它 3 个时期,以避免在训练数据上过度拟合。
从头开始再培训
最终,我们实现了 99%的训练准确率和 86%的验证准确率。这很好,考虑到我们正在使用一种非常幼稚的方法。通过使用更好的训练算法可以获得更高的准确度。
评估模型性能
我们将使用训练好的模型对测试数据进行预测。输出是一个浮点整数数组,表示评论为正面的概率。正如你所看到的,在某些情况下,网络是绝对肯定的审查是积极的。在其他情况下——没那么多!
做预测
你可以试着通过使用像均方差这样的度量来找到一些错误分类的情感数量的误差度量,就像我在这里做的。但是这样做就太傻了!对结果的分析不是我们在这里要讨论的内容。然而,我将解释为什么在这种情况下使用mse
是无用的。
来自我们模型的结果是模型感知评论的积极程度的度量。该模型不是告诉我们样本的绝对类别,而是告诉我们它认为情绪在多大程度上偏向一边或另一边。MSE 是一个过于简单的衡量标准,不能反映解决方案的复杂性。
我没有想象这个神经网络。我会的,但这是一个耗时的过程。我确实想象过我用来解决 MNIST 问题的神经网络。如果你想的话,你可以看看这个 GitHub 项目来可视化人工神经网络
一个很棒的可视化 python 库曾经与 Keras 一起工作。它使用 python 的 Graphviz 库来创建一个可展示的…
github.com](https://github.com/Prodicode/ann-visualizer)
结论
因此,我们成功地对 IMDB 上的评论进行了分类。我猜这需要重新观看矩阵或 IMDB 建议的任何东西!
我建议你配合这篇文章。您可以使用类似的策略解决大多数二元分类问题。如果你解决了这个问题,试着修改网络及其层的设计和参数。这将帮助您更好地理解您所选择的模型架构的完整性。
我在每篇文章中都详细讨论了一个话题。在这一篇中,我们深入研究了一些隐藏的图层。对任何特定主题的详尽解释都不在我的文章范围之内;然而,你会发现大量的快速旁白。
我假设读者对诸如优化器、分类编码、损失函数和度量标准之类的技术细节有着实用的理解。你可以在这里找到我关于这些概念的练习笔记。
更多内容,请查看 Francois Chollet 的《用 Python 进行深度学习的 T2》一书。
感谢阅读!
二元交叉熵和逻辑回归
有没有想过我们为什么使用它,它来自哪里,如何有效地优化它?这里有一个解释(包括代码)。
尽管逻辑回归源于统计学,但它是解决机器学习中二元分类问题的一种相当标准的方法。它实际上是如此标准,以至于在所有主要的数据分析软件(如 Excel、 SPSS ,或其开源替代软件【PSPP】)和库(如 scikit-learn 、 statsmodels 等)中都有实现。即使你只是稍微熟悉逻辑回归,你也可能知道它依赖于所谓的二元交叉熵的最小化
其中 m 为样本数, x ᵢ 为第 I 个训练样本, yᵢ 为其类(即 0 或 1), σ(z) 为逻辑函数, w 为模型的参数向量。你可能也知道,对于逻辑回归,它是一个凸函数。因此,任何最小值都是全局最小值。但是你有没有想过我们为什么要使用它,它实际上是从哪里来的,或者你如何能比简单梯度下降更有效地找到这个最小值?我们将在下面解决这些问题,并提供简单的 Python 实现。但首先,让我们快速回顾一下物流功能!
(非常)快速回顾一下物流职能
逻辑函数图。
逻辑函数 σ(z) 是一条 S 形曲线,定义为
它有时也被称为到期功能或s 形函数。它是单调的,并且在 0 和 1 之间有界,因此它被广泛用作概率模型。此外,我们还有
最后,你可以很容易地证明它对 z 的导数由下式给出
关于这个函数,这差不多就是你需要知道的全部了(至少对于这篇文章来说)。所以,事不宜迟,让我们开始吧!
逻辑回归的二元交叉熵推导
让我们考虑一个预测器 x 和一个二元(或伯努利)变量 y 。假设在 x 和 y 之间存在某种关系,理想的模型会预测
通过使用逻辑回归,这个未知的概率函数被建模为
因此,我们的目标是找到参数 w ,使得模拟的概率函数尽可能接近真实的概率函数。
从伯努利分布到二元交叉熵
评估我们的模型做得有多好的一个方法是计算所谓的可能性函数。给定 m 个例子,这个似然函数被定义为
理想情况下,我们因此希望找到使**w**)最大化的参数 w 。然而,在实践中,为了简单起见,人们通常不直接使用这个函数,而是使用它的负对数
因为对数是严格单调的函数,最小化负对数似然性将导致与直接最大化似然性函数时相同的参数。但是如何计算 P(y| x,w ) 当我们的 logistic 回归只建模 P(1| x,w ) ?鉴于
可以使用简单的求幂技巧来编写
将该表达式插入负对数似然函数中(并通过实例数量归一化),我们最终获得期望的归一化二进制交叉熵
因此,找到最小化二元交叉熵的权重 w 相当于找到最大化似然函数的权重,从而评估我们的逻辑回归模型在逼近我们的伯努利变量的真实概率分布方面做得有多好!
证明它是凸函数
如上所述,我们的目标是找到最小化二进制交叉熵的权重 w 。然而,在最一般的情况下,一个函数可能有多个极小值,寻找全局极小值被认为是一个困难的问题。尽管如此,可以表明最小化逻辑回归的二元交叉熵是一个凸问题,因此,任何最小值都是全局的。让我们快速证明这确实是一个凸问题!
有几种方法可以用来证明函数是凸的。然而,一个充分条件是其 Hessian 矩阵(即其二阶导数矩阵)对于 w 的所有可能值都是半正定的。为了便于我们的推导和后续实现,让我们考虑二进制交叉熵的矢量化版本,即
其中, X 的每一行都是我们的一个训练示例,我们利用了逻辑函数中引入的一些恒等式。使用矩阵微积分的一些元素(如果你不熟悉的话,查看这里的),可以显示我们的损失函数相对于 w 的梯度由下式给出
类似地,黑森矩阵写道
随着
从这一点,人们可以很容易地表明
因此,Hessian 矩阵对于每个可能的是正半定的,并且二元交叉熵(对于逻辑回归)是凸函数。既然我们知道我们的优化问题是行为良好的,让我们把注意力转向如何解决它!**
如何高效地找到这个最小值?
与线性回归不同,逻辑回归没有封闭形式的解。在当前情况下,二元交叉熵是一个凸函数,但是任何来自凸优化的技术都保证找到全局最小值。下面我们将使用两种这样的技术来说明这一点,即具有最佳学习速率的梯度下降和牛顿-拉夫森方法。
最优学习率梯度下降
在机器学习中,梯度下降的变化是模型训练的主力。在该框架中,权重 w 按照简单规则迭代更新
直到达到收敛。这里,α被称为学习速率或步长。使用恒定的学习速率是很常见的,但是如何选择呢?通过计算各种损失函数的 Lipschitz 常数的表达式, Yedida & Saha 最近表明,对于逻辑回归,最佳学习速率由下式给出
下面是相应算法的简单 Python 实现。假设您已经熟悉 Python,代码应该是不言自明的。
为了清晰和易用,我在我的每一篇文章中都尽量坚持使用 scikit-learn API。
牛顿-拉夫森方法
基于梯度下降的技术也被称为一阶方法,因为它们仅利用编码损失函数的局部斜率的一阶导数。当证明逻辑回归的二元交叉熵是凸函数时,我们也计算了 Hessian 矩阵的表达式,所以让我们使用它!
能够访问 Hessian 矩阵使我们能够使用二阶优化方法。这种技术使用关于由该 Hessian 矩阵编码的损失函数的局部曲率的附加信息,以在训练过程中自适应地估计每个方向上的最佳步长,从而实现更快的收敛(尽管计算成本更大)。最著名的二阶技术是牛顿-拉夫森方法,以杰出的艾萨克·牛顿爵士和不太出名的英国数学家约瑟夫·拉弗森的名字命名。使用这种方法,权重 w 的更新规则现在由下式给出
其中H***(w)是对当前进行求值的黑森矩阵。注意,Hessian 矩阵的条目明确地依赖于当前的 w 。因此,它需要在每次迭代时更新,并对其进行逆重新计算。尽管牛顿法比普通梯度下降法收敛得更快,但它的计算量更大,占用的内存也更多。对于小规模到中等规模的问题,它可能仍然比梯度下降法收敛得更快(在挂钟时间内)。对于更大的问题,人们可以看看被称为拟牛顿的方法,其中最著名的是 BFGS 方法。和以前一样,下面提供了相应算法的简单 Python 实现。***
超越传统的逻辑回归
逻辑回归为分类任务提供了一个相当灵活的框架。因此,多年来已经提出了许多变体来克服它的一些限制。
处理非线性可分离的类
通过构造,逻辑回归是一个线性分类器。正如线性回归可以扩展到建模非线性关系,逻辑回归也可以扩展到分类点,否则非线性可分。然而,这样做可能需要专业知识、对数据属性的良好理解以及特征工程(与其说是精确的科学,不如说是一门手艺)。
不平衡的阶级分布
当使用普通逻辑回归时,我们隐含地假设样本中两个类别的流行率大致相同(例如,预测一个人是男性还是女性)。然而,现实生活中有许多情况并非如此。在医学科学中尤其如此,在医学科学中,人们可能希望根据他/她的病历来预测病人在手术后是否会死亡。有希望的是,大多数已经接受治疗的患者已经存活,因此我们的训练数据集只包含相对较少的死亡患者的例子。这就是所谓的阶级不平衡。
*已经提出了不同的方法来处理这种类不平衡问题,例如对少数类进行上采样或者对多数类进行下采样。另一种方法是使用对成本敏感的培训。为了说明后者,让我们考虑以下情况:我们有 90 个样本属于比方说类别 y = 0 (例如,患者存活),只有 10 个样本属于类别 y = 1 (例如,患者死亡)*。如果我们的模型一直预测 y = 0 (即患者将存活),它将具有 90%的惊人准确性,但在预测给定患者是否可能死亡方面毫无用处。然而,提高模型的有用性和预测能力的简单技巧是如下修改二元交叉熵损失
权重α₀和α₁通常被选择作为训练集中每个类的逆频率。回到我们上面的小例子,α₀将被选为
而α₁将被设定为
这样做,当该模型将可能死亡的患者错误分类为存活的患者时,该模型受到更严重的惩罚(大约 10 倍以上)。它只需要对前面提出的算法进行微小的修改。尽管这种方法可能增加假阳性的数量(即,将存活的患者被错误地分类为可能死亡的患者),但是它减少了假阴性的数量(即,将死亡的患者被错误地分类为可能存活的患者)。这样,医生就可以把注意力集中在真正需要治疗的病人身上,尽管他们中的一些人本来是可以活下来的。
多项式分类
尽管二进制分类问题在现实生活中普遍存在,但一些问题可能需要多类方法,例如手写数字识别。
在这个问题中,人们试图分配一个标签(从 0 到 9)来表征图像中出现的数字。即使逻辑回归是通过设计二元分类模型,它也可以使用一对其余方法来解决这个任务。十个不同的逻辑回归模型被独立训练:
- 模型 1:预测数字是零还是不是零。
- 模型 2:预测数字是 1 还是不是 1。
- …
- 模式 10:预测数字是不是 9。
在部署阶段,分配给新图像的标签基于这些模型中哪个模型对其预测最有信心。然而,这种一对多的方法并不是没有限制,主要的三个限制是:
- 不确定性量化:对该模型总体预测的置信度进行估计并不简单。虽然量化预测中的不确定性对于类似 Kaggle 的竞争可能并不重要,但在工业应用中却至关重要。
- 不可判定性:当其中两个模型对它们的预测同样有信心时,如何处理这种情况?
- 不平衡学习:每个模型使用不平衡数据集学习。假设我们对每个数字都有大致相同数量的例子,一个给定的模型只有 10%的这个特定数字的训练例子和 90%的非这个特定数字的训练例子。
尽管有这些限制,一对多逻辑回归模型仍然是处理多类问题时使用的一个很好的基线,我鼓励你这样做作为一个起点。一个更合适的方法,称为 softmax 回归,将在接下来的帖子中考虑。
正则化逻辑回归
在机器学习中,处理以大量特征为特征的数据是相当常见的。然而,并非所有这些特征都可以为预测目的提供信息,因此可以瞄准稀疏逻辑回归模型。为此,例如可以使用模型权重的ℓ₁-norm 正则化。修正的损失函数由下式给出
*其中*CE(w)是二进制交叉熵的简写符号。现在众所周知,使用损失函数的这种正则化促进了参数向量 w 稀疏。然后,超参数λ控制模型的稀疏程度和最小化交叉熵的重要性之间的权衡。虽然超参数优化本身是机器学习的一个专门领域,远远超出了本文的范围,但我们最后要提到的是,scikit-learn 提供了一种基于网格搜索和交叉验证的简单启发式方法来找到λ的好值。
结论
从数学和计算的角度来看,逻辑回归的简单性和灵活性使其成为现实应用中二元分类最常用的技术。如果你是机器学习的新手,我希望你现在对它所依赖的数学以及如何在实践中使用它有更好的理解。请注意,有很多内容我们没有涉及,例如:
- 根据优势比(或对数优势)对模型的统计解释,
- 如何使用各种指标和 ROC 或精确召回曲线来量化预测的准确性(除了我们最小化训练集的交叉熵这一事实)。
然而,这些应该是在你掌握了基础知识之后的第二步。此外,网上有很多资源可以解决这些额外的问题。为了获得更好的见解,请不要犹豫!不要犹豫,也要自己推导这里给出的所有数学结果,并使用提供的代码!请告诉我这些内容是否对您有用,或者您是否发现了任何错别字:]
在接下来的几篇文章中,我们将讨论以下主题:
- 为什么你应该总是正则化逻辑回归!
- 多类问题和 softmax 回归。
- 贝叶斯逻辑回归。
其他在线资源
- Aurélien Géron ( 此处为)的精彩视频从信息论的角度解释了为什么使用交叉熵进行分类是有意义的。
**想看更多这方面的内容?查看我其他关于低秩结构和数据驱动建模 的文章或者干脆我的 机器学习基础知识 !
深度学习快速入门。
towardsdatascience.com](/rosenblatts-perceptron-the-very-first-neural-network-37a3ec09038a)**
Python 中的二进制、十六进制和八进制
十进制系统之外的漫步
由 Alexander Sinn 在 Unsplash 上拍摄的照片
在数学方面,Python 以强大和易于使用而闻名。它的本机功能和资源丰富的库,如 NumPy、Pandas 或 Scikit-learn,为开发人员提供了处理繁重数字的必要工具。但有时我们需要跳出十进制世界,使用其他常见的基数之一。
基数是计数系统用来表示数值的位数。最普遍的数字系统是十进制,也称为十进制。在十进制中,数字 0、1、2、3、4、5、6、7、8 和 9 代表每一个可能的值。但是计算机和软件开发人员经常需要使用其他基础。
因为你不能测量脏数据
towardsdatascience.com](/how-to-implement-a-successful-data-cleaning-process-701e565e6575)
在所有十进制系统中,十进制系统最受二进制、十六进制和八进制系统的欢迎。其他人属于你和朋友在一起时会避开的那种特殊的表亲。然而,如果你计划使用二进制、十六进制或八进制,你可能需要温习一下 Python。在 Python 中,它们不像 base 10 那样简洁易用。
二进制的
二进制只使用数字 0 和 1。从这两个,它可以说明每一个可能的值,同样的十进制系统。你还记得小学时的位置值吗?这就是它的工作原理。在十进制中,每个位置增加十的倍数,但在二进制中,每个位置增加二的倍数。
以 10 为基数的数位值,例如数字 10、100 和 1000
基数 2 位值
例如,101 表示值 5。
而 10101 将代表值 21。
想知道为什么您的网络子网掩码看起来像 255.255.255.0 吗?因为每一个用句点分隔的数字都是由八位二进制数 11111111 组成的。
我们可以这样开始这一部分:“二进制只使用 10 位数。”如果你不明白这个笑话,请再读一遍二进制是如何工作的解释。
在 Python 中,使用二进制数比使用十进制数要多几个步骤。当你输入一个二进制数时,以前缀“0b”开始(即一个零后面跟着一个小 b)。
0b11
与二进制 11 相同,等同于十进制 3。不难,但这是额外的工作。每当你想在一个变量中存储一个二进制值,它会为你转换成十进制。
number1 = 0b11
这导致变量number1
存储值 3。它只是存储这个值,并不表示你想用二进制来表示它。所以,当你检索它的时候,你会得到一个十进制值 3。事实上,Python 独立于 base 处理所有的数学运算符,但是它总是以十进制返回给你。
“但是如果我想让我的数字以二进制返回给我呢?”
很高兴你问了。
如果您希望代码中的数字严格保持二进制,这里有一个解决方案。
>>> num1 = "0b100"
>>> num2 = "0b110"
>>> mysum = int(num1, 2) + int(num2, 2)
>>> print(bin(mysum))
0b1010
在上面的代码片段中,我们首先将字符串“0b100”赋给变量num1
。接下来,我们将字符串“0b110”赋给变量num2
。所以我们有两个 string 类型的变量,分别存储 4 和 6 的二进制表示。
接下来,我们将这两个数字相加。但是当我们这样做的时候,我们使用函数 int()将每一个转换为基数为 10 的整数。通常 int()会抛出一个包含字母的字符串错误。通过指定第二个参数 2,我们指示 int()将字符串解释为二进制数。所以,它保持快乐。
您可以使用第二个参数来指定 2 到 36 之间的任何基数。基数 2 和 36 包括在该范围内。
如何让数据真正驱动您的决策
towardsdatascience.com](/the-illusion-of-making-data-driven-decisions-bf54a2e594c4)
在我们将两个数字相加并将结果存储在mysum
中之后,我们打印出总和。但它是独立于基地存放的。当我们回忆它的时候,它还是想用十进制呈现给我们。所以我们必须告诉 Python 我们想要二进制数。在将值打印到屏幕上之前,使用 bin()函数将值转换为二进制。
上面的代码给了你一个清晰的 Python 二进制表示。然而,你也可以使用一个更简短的版本,就像这样。
>>> num1 = 0b100
>>> num2 = 0b110
>>> mysum = num1 + num2
>>> print(bin(mysum))
你会得到同样的结果。唯一的区别是你如何在变量num1
和num2
中存储数字。如果你在第一个例子中把任何一个变量打印到屏幕上,你会看到它是二进制的,尽管从技术上讲它是一个字符串。在第二个示例中,您将看到一个小数,除非您使用 bin()来转换它。
>>> num1 = "0b100"
>>> print(num1)
"0b100">>> num1 = 0b100
>>> print(num1)
4
>>> print(bin(num1))
0b100
十六进制的
十进制用十位,二进制用两位,十六进制用十六位。因为我们只有十进制系统中的十位数可以使用,所以我们用字母代替数字 9 以上的所有数字。所以十六进制的数字是 0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F,它们代表从零到九,那么 A 值十,B 值十一,C 值十二,D 值十三,E 值十四,F 值十五。所以,十六进制有十六个数字。如果你用十六进制写,你可以说,“十六进制有 10 个数字。”
等等?我们以前没见过吗?是的。老实说,如果你在相应的数字系统中写数字,10 总是代表任何基数的总位数。但是只有二进制才有意思。
在 Python 中表示十六进制数时,在数字前加上前缀“0x”。此外,使用 hex()函数将值转换为十六进制格式以便显示。
我们的两个十六进制代码样本类似于我们用于二进制代码的样本。
>>> hnum1 = "0x10"
>>> hnum2 = "0x10"
>>> myhsum = int(hnum1, 16) + int(hnum2, 16)
>>> print(hnum1)
"0x10"
>>> print(myhsum)
32
>>> print(hex(myhsum))
0x20>>> hnum1 = 0x10
>>> hnum2 = 0x10
>>> myhsum = hnum1 + hnum2
>>> print(hnum1)
16
>>> print(myhsum))
32
>>> print(hex(myhsum))
0x20
八进制的
最后,这同样适用于八进制。任何关于八进制数字系统中有多少位数的猜测?八进制代表八。对,八进制包含八位数。在 Python 中,我们使用 oct()将数字转换为八进制,而不是 bin()或 hex()。我们在八进制数字前加一个零,后面跟着一个小写的 o,比如“0o”。
八进制的八位数是 0,1,2,3,4,5,6,7。
让我们在这里使用相同的代码示例,但是我们将为八进制代码使用正确的符号和转换函数。
>>> onum1 = "0o10"
>>> onum2 = "0o10"
>>> myosum = int(onum1, 8) + int(onum2, 8)
>>> print(onum1)
"0o10"
>>> print(myosum)
16
>>> print(oct(myosum))
0o20>>> onum1 = 0o10
>>> onum2 = 0o10
>>> myosum = onum1 + onum2
>>> print(onum1)
8
>>> print(myosum))
16
>>> print(oct(myosum))
0o20
结论
Python 的伟大之处在于,除了洗衣服,它几乎可以做任何事情。我正在努力。
要点很简单:
- 二进制使用 bin()和“0b”。
- 十六进制使用十六进制()和“0x”。
- 八进制使用 oct()和’ 0o '。
- 通过更改第二个参数,可以使用 int()函数将数字从 2 到 36 之间的任何基数转换为基数为 10 的整数。例如 int(number,30)
即使在我们喜爱的十进制系统之外使用基数需要更多的努力,Python 很容易适应并使我们能够进入十进制以前没有去过的地方——进入其他基数的最终边界。
杆蓖麻 帮助公司获得正确的分析!他帮助国际组织和小型企业改善他们的数据分析、数据科学、技术战略和技术领导力。除了咨询,Rod 还喜欢公开演讲、教学和写作。你可以在 rodcastor.com**和通过他的 邮件列表 了解更多关于 Rod 和他的工作。
Python 中的二分搜索法——更快吗?
二进制与线性搜索—实现和性能测试
这是一周的时间!你决定深入一个话题,今天我们来看看二分搜索法。
和往常一样,解决同一个问题有很多方法。当你想检查一个元素是否在一个列表中,你可以用线性搜索和二分搜索法来完成,但是猜猜哪个更快。
为什么?
如果你最近在 Youtube 上看过编程工作面试,你就会知道二分搜索法是最受欢迎的。
你为什么要花时间去了解二分搜索法呢?你的 C++编程朋友可能已经告诉你了。Python 很慢。你要确保你的程序不会比需要的速度慢。
当您学习 Python 时,您将学习进行线性搜索来检查元素是否在列表中。当你学习编码时,这没问题,但是当你在一个列表中有 60.000.000 个元素时会发生什么呢?
如果你在一个有 11 个元素的列表中进行线性搜索,你必须遍历所有的 11 个元素。如果你使用二分搜索法,根据你要找的东西,你可能最终只有 2 次迭代。见下图。
哪种方法更快应该是显而易见的。
当您开始学习 Python 时,您很可能已经处理过上百次列表了。检查值是否在列表中是一项正常的任务,您以前已经看到过这种情况:
my_list = [1,2,3,3,5,11,12]if 11 in my_list:
return Truereturn False
或者这个:
my_list = [1,2,3,3,5,11,12]for each in list:
if each==11:
return Truereturn False
让我们开始看看如何实现二分搜索法。
如何?
让我们想象一下二分搜索法是如何工作的。
首先,我们需要确保列表已经排序。你可以用.sort()
或者sorted()
来排序你的列表,我用.sort()
来改变列表的位置。如果你出于某种原因需要一个新的列表或者不想修改原来的列表,使用sorted()
这是我们的测试材料:
bin_list = [1,2,3,5,6,9,11,12,15,20,22]
search_value_a = 15
我们将寻找值 15 。
- 我们的起点。具有最小值和最大值的列表:
作者图片
- 当我们做二分搜索法时,我们从寻找列表中的中间元素开始:
作者图片
- 中间索引为
**5**
,值为**9**
。我们想知道的第一件事是**9**
是否是我们要找的号码。记住,我们找的是**15**
。如果不是,我们检查它是更低还是更高。在我们的例子中,**9**
小于 15,所以我们需要设置一个新的最小点。我们知道我们不再需要担心名单的下半部分。新的最小点将被设置为列表上部的第一个可能项目。
作者图片
- 有了新的中点,我们检查这是否是我们正在寻找的数字。在这种情况下,它是。
如果我们在寻找**2**
,并且我们的第一个中间值是**9**
,你认为算法会如何表现?你说得对。最大指数将会移动。
作者图片
作者图片
作者图片
作者图片
代码
好吧,我知道你是来编码的。让我们开始吧。
伪代码将如下所示:
# create function with list and target as parameters.# make sure the list is sorted.# get length of list minus 1 as max and 0 as start.# a loop will: # get the new middle value
# check if the middle value is higher or lower than the target.
# move the min or max to the middle after the check.
# if middle == target, return True
# if we reach the end of the list, the target is not in the list
代码的作用:
我们已经创建了一个带有两个参数的函数。一个list
和一个target value
。目标值是我们正在寻找的数字。列表是我们迭代的列表,寻找数字。
def binary_search(input_list , target_value):
如果我们找到目标值,我们将返回True
。如果没有,我们就返回False
。
我们做的第一件事是对列表进行排序,并定义列表的最小索引和最大索引。
input_list.sort()
min_index = 0
max_index = len(input_list) -1
我们使用len(list)-1
的原因是 Python 从0
开始索引。测试列表的长度为11
,但最后一个索引为[10]
。
现在,让我们来看看大脑的功能,这个循环:
while max_index >= min_index:
mid_index =(max_index+min_index)//2
if input_list[mid_index] == target_value:
return True
elif input_list[mid_index] < target_value:
min_index = mid_index+1
else:
max_index = mid_index-1
只要最大指数不高于最小指数,我们就继续前进。如果循环停止,这意味着我们已经折叠了列表,所以 max 小于 min。此时,搜索值没有意义,因为没有更多的列表。
mid
设置为max
和min
的平均值。注意我们如何使用基数除法/整数除法,例如7//2
将是3
而不是3.5
。这样我们总能为我们的索引得到一个干净的整数。- 如果带有 mid 索引的列表项的值等于我们的目标值,我们成功了!返回
True
回家。 - 如果该值小于目标值,我们知道必须将最小索引提高到该点。新的
min
因此是mid+1
- 如果该值不等于或小于目标值,则该值较大。这意味着我们可以删除列表的顶部,并降低 max 指数。
max
设置为mid-1
如果您发现很难理解,您可以在代码中添加一个print()
来获得索引跳转的可视化表示。
在 while 循环中的mid_index =(max_index+min_index)//2
之后添加:
print (f'min: {min_index} , mid: {mid_index} , max: {max_index}')
main()
我们的主过程包含测试数据和一些断言,以检查一切是否按预期工作。
请注意,如果您想查看搜索失败的示例,我添加了另一种方法来构建列表。
#bin_list = list(range(6,501))
但是这样更快吗?
这个函数的时间复杂度是 O(n ),其中 n 是列表的长度。为了检查哪种搜索更快,我们可以用线性搜索来计算二分搜索法的时间。
来自 Pexels 的 Andrea Piacquadio 的照片
首先,我们需要编写一个线性搜索函数:
def linear_search(input_list, target_value):
for each in input_list:
if each==target_value:
return True
return False
然后,我们需要编写性能测试,一个针对二进制,一个针对线性:
陷阱
如果你运行上面的代码(与原代码合并),你会看到线性搜索比快。这是什么魔法?
list = [6…500],target = 15,跑了 10.000 次。
有几个问题给二进制带来了困难。
- 整理
- 列表长度
- 低目标值
以上所有因素,给了线性一个良好的开端。现在,让我们继续在那里排序,首先改变列表长度:
bin_list = list(range(1,10000))
lin_list = list(range(1,10000))
列表= [1…10.000],目标= 15,运行了 10.000 次
线性搜索还是占了上风。让我们从函数中取出排序,在将列表传递给函数之前对其进行排序。(这对线性搜索不公平,因为线性搜索不依赖于排序列表)。我们所要做的就是注释掉它,因为我们的列表已经排序了。
list = [1…10.000],target = 15,运行了 10.000 次,正在排序
越来越近了。
如果我们把目标值移到 7.500 呢?现在,我们的偏见是闪亮的,因为我们真的希望二进制更快。
list = [1…10.000],target=7500,运行了 10.000 次,正在排序
这一次的差异是极端的。下面的最后一个例子将会使所有的事情变得公平。
让我们用随机目标创建一个随机长度的随机列表。然后我们将对这两个函数运行 100.000 次。
由于他们不能分享列表,我们将相信蒙特卡洛模拟(同时祈祷 Macbook 不会着火……)
test_list = list(range(1,random.randint(2,50000)))
test_number = random.randint(2,50000)
binary_search(test_list,test_number)
list = rand[1…50.000],target = rand,运行了 100.000 次,正在排序
list = rand[1…50.000],target = rand,运行 100.000 次,排序依据
现在让我们运行 6000 万次!开玩笑,我相信这些结果。MacBooks 很贵。
最后
二进制比线性快吗?是的,但是它看情况。
当有人告诉你二分搜索法更快,那是因为它通常是。
像往常一样,你必须看设置,不要每次都选择单一的解决方案,因为它“是最好的”。
我们从实验中看到,这取决于你在做什么。如果您有一个简短的列表,或者如果您正在查找列表下部的元素,那么执行线性搜索可能会更好。
这也是编程的妙处。你不应该在不知道为什么要做某事的情况下使用一种方式。如果你还不知道二分搜索法,现在你又有了一个搜索工具。当你发现它的用处时,就用它。
不过,我希望我们能在一件事上达成一致。二分搜索法非常酷!
二叉树和最低共同祖先
二叉树中两个节点的最低公共祖先以及寻找它的算法。
作者图片
什么是“最低共同祖先(LCA)”。
它是树中两个节点共享的最低级别的父节点。
让我们来看一个例子:
作者图片
在上面的二叉树中,节点 8 和 10 被突出显示。他们共有的父母是什么?
作者图片
共享的父母是 5,7,9 是相当明显的。
但最底层的共享父代是 9,称为最低共同祖先(LCA) 。
二叉查找树中最低的共同祖先
让我们用二叉查找树热身。
二叉查找树是二叉树的一个特例,左子树只包含较小的节点,而右子树只包含较大的节点。
我们上面的例子是一个二叉查找树。如您所见,在节点 3,左子树(0,1,2)中的所有节点都小于 3,而右子树中的所有节点都大于 4。
那么,我们如何着手寻找二叉查找树中的 LCA,例如节点 8 和 10?
实际上,二叉查找树让搜索变得非常简单。仔细看看节点 9,它和节点 7 有什么不同?我能想到一件事:
节点 9 介于 8 和 10 之间,而对于节点 7,节点 8 和 10 都更大。
这实际上是足够信息来确定节点 9 是二叉查找树中节点 8 和 10 的 LCA,这要归功于它的巨大属性。
下面是它的一个实现:
from collections import deque
class Node:
def __init__(self, value, left=None, right=None):
self.value = value
self.left = left
self.right = right
def create_bt(value_queue):
*"""create binary tree"""* if len(value_queue) <= 0:
return None, None
root = Node(value_queue.popleft())
current_queue = deque()
current_queue.append(root)
while len(current_queue) > 0 and len(value_queue) > 0:
current_node = current_queue.popleft()
left = value_queue.popleft()
if left is not None:
current_node.left = Node(left)
current_queue.append(current_node.left)
right = value_queue.popleft()
if right is not None:
current_node.right = Node(right)
current_queue.append(current_node.right)
return root
def create_bt_fls(value_list):
*"""create binary create from list"""* return create_bt(deque(value_list))
def lca_bst(bst, v1, v2):
*"""lowest common ancestor of two nodes
in a binary search tree"""* if v1 <= bst.value <= v2 or v1 >= bst.value >= v2:
return bst
elif bst.value > v1 and bst.value > v2:
return lca_bst(bst.left, v1, v2)
else:
return lca_bst(bst.right, v1, v2)
bt1 = create_bt_fls([5, 3, 7, 1, 4, 6, 9, 0, 2, None, None, None, None, 8, 10])
lca = lca_bst(bt1, 8, 10)
print(lca.value)
'''
output:
9
'''
在这个实现中,我们首先从一个值列表中创建一个二叉树数据结构,然后调用 LCA 算法来查找节点 8 和 10 的 LCA,它返回正确的值 9。
让我们暂停一下,稍微思考一下这个算法的复杂性。该算法从根节点开始,在每个节点将节点的值与两个输入节点进行比较,如果值在两个节点之间,则返回该节点作为答案,否则,如果值大于两个输入节点,则向左移动,如果值小于两个输入节点,则向右移动。
所以在每次递归迭代过程中,搜索空间被切成两半(树的一半),这意味着如果树中的节点总数为 n,则需要的迭代次数为 log(n)(以 2 为底),所以复杂度为 O(log(n))。不算太坏。
二叉树中的 LCA(非 BST)
这是一个很好的热身,现在让我们把事情变得稍微复杂一些。
现在假设这棵树不是二叉查找树,而是一棵没有特定结构的随机节点值的树。我们如何找到树中两个节点的 LCA?
二叉树,作者图片
如果你试图在上面的树上运行二叉查找树 LCA 算法,它将悲惨地失败:
ls1 = [0, 1, 2, 3, 4, 5, 6, 7, 8, None, None, None, None, 9, 10]
bt1, node_dict = create_bt_fls(ls1)
lca = lca_bst(bt1, 9, 10)
print(lca.value)
'''
output:
10 WRONG!!
'''
我们得想别的办法。
一种直观的方法是从下面的观察中得来的:
9 的路径:[0,2,6,9],10 的路径:[0,2,6,10],按作者排序的图像
从上图中可以看出,到节点 9 的路径是 0,2,6,9,到节点 10 的路径是 0,2,6,10。一旦我们有了到每个节点的路径,我们需要做的就是在两条路径中找到最后一个匹配的节点,这也是我们的 LCA!
下面是一个实现:
def find_path(tree, value):
*"""find path from root to value"""* if tree is None:
return None
elif tree.value == value:
return [tree]
res = find_path(tree.left, value)
if res is not None:
return [tree] + res
res = find_path(tree.right, value)
if res is not None:
return [tree] + res
return None
def lca_fp(tree, v1, v2):
*"""find lca of two nodes using find_path"""* path1 = find_path(tree, v1)
path2 = find_path(tree, v2)
cur_idx = 0
while len(path1) > cur_idx and \
len(path2) > cur_idx and \
path1[cur_idx].value == path2[cur_idx].value:
cur_idx = cur_idx + 1
return path1[cur_idx - 1] lca = lca_fp(bt1, 9, 10)
print(lca.value)
'''
output:
6
'''
作品。
上面的算法足够简单,只需要注意一点,Find Path 算法搜索整个树寻找到目标节点的路径,所以它的复杂度是 O(n),因此使用 find_path 的 LCA 算法也是 O(n)。因为没有将树构造为二叉查找树而牺牲了性能。
寻路算法的优化
查找路径算法的复杂性并不令人担心,但是如果您需要重复执行搜索该怎么办呢?请记住,在一个大小为 n 的树中,有 n 选择 2 个唯一的节点对,即:
作者图片
节点对的数量,如果我们使用查找路径算法得到所有节点对的 LCA,那么将是 O(n n)或 O(n)。也许我们应该试着加快一点。
加快查找路径计算速度的一种方法是在节点中存储一个父指针,这样当我们试图查找到一个节点的路径时,我们需要做的就是沿着父指针一直到根,而不是在整个树中搜索路径。
下面是对树的这种增强的实现:
class Node:
def __init__(self, value, **parent=None**, left=None, right=None):
self.value = value
**self.parent = parent**
self.left = left
self.right = right
**def add_to_dict(self, node_dict):
node_dict[self.value] = self**
def create_bt(value_queue):
*"""create binary tree"""* if len(value_queue) <= 0:
return None, None
**node_dict = {}**
root = Node(value_queue.popleft())
**root.add_to_dict(node_dict)**
current_queue = deque()
current_queue.append(root)
while len(current_queue) > 0 and len(value_queue) > 0:
current_node = current_queue.popleft()
left = value_queue.popleft()
if left is not None:
current_node.left = Node(left, **parent=current_node**)
**current_node.left.add_to_dict(node_dict)**
current_queue.append(current_node.left)
right = value_queue.popleft()
if right is not None:
current_node.right = Node(right, **parent=current_node**)
**current_node.right.add_to_dict(node_dict)**
current_queue.append(current_node.right)
return root, **node_dict**
def create_bt_fls(value_list):
*"""create binary create from list"""* return create_bt(deque(value_list))
在树的这个新实现中,我们在节点中存储了一个额外的字段 parent 来存储父节点。
我们还为节点创建了一个值字典,这样我们就可以很容易地找到对应于某个值的节点。
有了这两个增强,我们可以实现一个新 LCA 算法:
def lca(node1, node2):
parents = set()
cur_parent = node1
while cur_parent is not None:
parents.add(cur_parent)
cur_parent = cur_parent.parent
cur_parent = node2
while cur_parent is not None:
if cur_parent in parents:
return cur_parent
cur_parent = cur_parent.parent
return None ls1 = [0, 1, 2, 3, 4, 5, 6, 7, 8, None, None, None, None, 9, 10]
bt1, node_dict = create_bt_fls(ls1)
lca1 = lca(node_dict[9], node_dict[10])
print(lca1.value)
'''
output:
6
'''
又管用了。
通过这种增强,我们牺牲了多一点的内存空间 O(n)来存储父指针,但是由于从节点到根父节点的跟踪是 O(log(n)),所以算法的复杂度降低到 O(log(n))。在所有节点对上运行现在需要 O(n log(n))内存来提高速度。
未完待续……
这是我们能做的最好的吗?绝对不是,有很多方法可以优化算法。特别是 Tarjan 的离线最低共同祖先算法,可以将性能复杂度降低到常数!但是因为它涉及一个全新的数据结构(不相交集),我们将在另一个故事中讨论它。