如何在机器学习代码中加入Python分析工具

128 阅读7分钟

减少代码运行时间对开发者来说是很重要的。Python剖析器,如cProfile,帮助我们找到程序或代码的哪一部分需要更多的时间来运行。无论你使用的是Python GUI还是命令行,剖析都对追踪影响性能的代码瓶颈有很大帮助。

本文将指导你使用cProfile模块提取剖析数据和snakeviz模块进行可视化,并实施这些步骤来测试机器学习脚本的过程。

什么是代码剖析?

代码剖析是一种技术,可以弄清楚程序中的时间是如何度过的。更详细地说,剖析是一组统计数据,描述了程序各部分的执行频率和时间。

通过这些统计数据,我们可以找到程序的 "热点",并思考改进的方法。有时,在一个意想不到的地方出现的热点可能会给你提示程序中的错误。

一个程序运行缓慢一般可能是由于两个原因。一个部分运行缓慢,或者一个部分运行次数过多,加起来花费了太多时间。我们把这些 "性能大户 "称为热点。

我怎样才能得到cProfile库?

由于cProfile是一个内置的Python库,不需要进一步安装。

我怎样才能得到snakeviz库,将剖析结果可视化?

这里是你如何使用pip 获得snakeviz 的稳定版本。

pip install snakeviz

我如何在我的机器学习代码中实现Python剖析工具?

在代码内部使用剖析器

这种方法的优点是我们可以只关注剖析的一部分,而不是整个程序。例如,如果我们加载一个大的模块,它需要花费时间来引导,我们想把这个从剖析器中删除。在这种情况下,我们可以只对某些行调用剖析器。

下面是一个对普通最小平方(OLS)线性回归程序进行剖析的例子,只针对回归直到绘图的步骤。

# Import profiling tools
import cProfile as profile
import pstats
# Code source for Ordinary Linear Regression: Jaques Grobler
# License: BSD 3 clause
import matplotlib.pyplot as plt
import numpy as np
from sklearn import datasets, linear_model
from sklearn.metrics import mean_squared_error, r2_score

# Load the diabetes dataset
diabetes_X, diabetes_y = datasets.load_diabetes(return_X_y=True)

# Use only one feature
diabetes_X = diabetes_X[:, np.newaxis, 2]

# Split the data into training/testing sets
diabetes_X_train = diabetes_X[:-20]
diabetes_X_test = diabetes_X[-20:]

# Split the targets into training/testing sets
diabetes_y_train = diabetes_y[:-20]
diabetes_y_test = diabetes_y[-20:]

# Perform all the regression steps with profiling
prof = profile.Profile()
prof.enable()
# Create linear regression object
regr = linear_model.LinearRegression()

# Train the model using the training sets
regr.fit(diabetes_X_train, diabetes_y_train)

# Make predictions using the testing set
diabetes_y_pred = regr.predict(diabetes_X_test)

# The coefficients
print("Coefficients: n", regr.coef_)
# The mean squared error
print("Mean squared error: %.2f" % mean_squared_error(diabetes_y_test, diabetes_y_pred))
# The coefficient of determination: 1 is perfect prediction
print("Coefficient of determination: %.2f" % r2_score(diabetes_y_test, diabetes_y_pred))

# Plot outputs
plt.scatter(diabetes_X_test, diabetes_y_test, color="black")
plt.plot(diabetes_X_test, diabetes_y_pred, color="blue", linewidth=3)

plt.xticks(())
plt.yticks(())
prof.disable()

# Print profiling output
stats = pstats.Stats(prof).strip_dirs().sort_stats("cumtime")
stats.print_stats(10) # Print only top 10 rows

# Show plot
plt.show()

下面是PyScripterIDE上的输出。

output1_cprofile_ml-8073570

对于第二个例子,让我们考虑一个使用爬坡算法为感知器模型寻找超参数的程序。我们只想对爬坡算法的爬坡搜索部分进行剖析。

# Import profiling tools
import cProfile as profile
import pstats
# Manually search perceptron hyperparameters for binary classification
from numpy import mean
from numpy.random import randn
from numpy.random import rand
from sklearn.datasets import make_classification
from sklearn.model_selection import cross_val_score
from sklearn.model_selection import RepeatedStratifiedKFold
from sklearn.linear_model import Perceptron

# Objective function
def objective(X, y, cfg):
    # Unpack config
    eta, alpha = cfg
    # Define model
    model = Perceptron(penalty='elasticnet', alpha=alpha, eta0=eta)
    # Define evaluation procedure
    cv = RepeatedStratifiedKFold(n_splits=10, n_repeats=3, random_state=1)
    # Evaluate model
  scores = cross_val_score(model, X, y, scoring='accuracy', cv=cv, n_jobs=-1)
    # Calculate mean accuracy
    result = mean(scores)
    return result

# Take a step in the search space
def step(cfg, step_size):
    # Unpack the configuration
    eta, alpha = cfg
    # Step eta
    new_eta = eta + randn() * step_size
    # Check the bounds of eta
    if new_eta <= 0.0:
        new_eta = 1e-8
    if new_eta > 1.0:
         new_eta = 1.0
    # Step alpha
    new_alpha = alpha + randn() * step_size
    # Check the bounds of alpha
    if new_alpha < 0.0:
        new_alpha = 0.0
    # Return the new configuration
    return [new_eta, new_alpha]

# Hill climbing local search algorithm
def hillclimbing(X, y, objective, n_iter, step_size):
    # Starting point for the search
    solution = [rand(), rand()]
    # Evaluate the initial point
    solution_eval = objective(X, y, solution)
    # Run the hill climb
    for i in range(n_iter):
        # Take a step
         candidate = step(solution, step_size)
         # Evaluate candidate point
         candidate_eval = objective(X, y, candidate)
         # Check if we should keep the new point
         if candidate_eval >= solution_eval:
             # Store the new point
           solution, solution_eval = candidate, candidate_eval
           # Report progress
           print('>%d, cfg=%s %.5f' % (i, solution, solution_eval))
    return [solution, solution_eval]

# Define dataset
X, y = make_classification(n_samples=1000, n_features=5, n_informative=2, n_redundant=1, random_state=1)
# Define the total iterations
n_iter = 100
# Step size in the search space
step_size = 0.1
# Perform the hill climbing search with profiling
prof = profile.Profile()
prof.enable()
cfg, score = hillclimbing(X, y, objective, n_iter, step_size)
prof.disable()
# Print program output
print('Done!')
print('cfg=%s: Mean Accuracy: %f' % (cfg, score))
# Print profiling output
stats = pstats.Stats(prof).strip_dirs().sort_stats("cumtime")
stats.print_stats(10) # Print only top 10 rows

下面是PyScripter IDE上的输出。

output2_cprofile_ml-4194875

如何在运行时从命令提示符中使用Python代码剖析器

另一种对机器学习脚本进行剖析的方法是在运行时运行cProfile。这种方法的优点是你可以通过一行命令轻松地对整个代码进行剖析,而且你可以将剖析结果导出为文件,以便进一步分析。

下面是如何在命令提示符下进行机器学习代码剖析的方法。首先,删除剖析器部分,并将代码保存为ols.py。接下来,我们可以在命令行中运行剖析器,方法如下。

python -m cProfile ols.py

以下是剖析结果的摘录。

output3_cprofile_ml-9403605

对Hillclimb算法的脚本做同样的处理。删除剖析器部分,并将代码保存为 hillclimb.py。接下来,我们可以在命令行中运行剖析器,如下。

python -m cProfile hillclimb.py

下面是剖析结果的摘录。

output4_cprofile_ml-3687250

它提供了非常丰富和详细的代码剖析数据。

如何按调用次数对Python剖析结果进行排序?

前面几节介绍的剖析输出非常长,可能对我们没有用处,因为我们可能很难分辨哪个函数是热点。所以我们可以通过调用次数(ncalls)对上述输出进行排序,以找出运行次数过多的部分,使用以下命令。

python -m cProfile -s ncalls ols.py

下面是对ls.py的剖析结果的摘录,从调用次数最多的函数开始排序。

output11_cprofileols_orderedbycallcount-7800363

运行下面的命令对Hillclimb算法的剖析结果按调用次数排序。

python -m cProfile -s ncalls hillclimb.py

下面是对Hillclimb.py的分析结果的摘录,从调用次数最多的函数开始排序。

output12_cprofilehillclimb_orderedbycallcount-3656514

如何按照花费的总时间对Python剖析结果进行排序?

我们也可以通过给定函数花费的总时间(tottime)对cProfile输出进行排序,以找出运行缓慢的部分,使用下面的命令。

python -m cProfile -s tottime ols.py

而这里是命令提示符上的输出。

output13_cprofileols_orderedbytottime-1498133

运行下面的命令对Hillclimb算法的剖析结果按照在给定函数中花费的总时间进行排序。

python -m cProfile -s tottime hillclimb.py

这里是命令提示符上的输出。

output14_cprofileols_orderedbyhillclimb-5495736

如何保存机器学习代码剖析结果以便进一步分析?

与其在命令行上只打印剖析结果,我们可以通过将其导出到一个文件中,使其对进一步的结果更有用。

以下是如何做到这一点的。

python -m cProfile -o statsOls.dump ols.py

用下面的命令来保存Hillclimb算法的剖析结果。

python -m cProfile -o statsHillclimb.dump hillclimb.py

上述命令将把剖析结果导出到statsOls.dump和statsHillclimb.dump文件。

如何使用snakeviz将Python剖析结果可视化?

要可视化你的Python代码剖析结果,用snakeviz调用.dump文件,使用这个命令。

snakeviz statsOls.dump

它将启动snakeviz web服务器,并在你的默认浏览器上打开可视化结果。snakeviz web服务器默认在127.0.0.1:8080启动。

你可以设置可视化的风格、深度和截止点。

用Icicle风格进行可视化。

output5_snakevizols_icicle-4336785

用Sunburst风格进行可视化。

output6_snakevizols_sunburst-7178020

以表格的形式摘录所有的剖析结果。

output7_snakevizols_tabular-9263512

用这个命令对Hillclimb算法脚本做同样的处理。

snakeviz statsHillclimb.dump

用Icicle风格进行可视化。

output8_hillclimb_icicle-1371488

旭日风格的可视化。

output9_hillclimb_sunburst-9398092

以表格形式摘录所有剖析结果。

output10_hillclimb_tabular-5733430

下表是对每一列的解释。

ncalls呼叫的数量。
tottime在给定函数中花费的总时间(不包括调用子函数的时间)。
percalltottime除以ncalls的商数。
cumtime在这个函数和所有子函数中花费的累计时间(从调用到退出)。这个数字即使对于递归函数也是准确的。
percallcumtime的商数除以原始调用。
filename:lineno(function)提供每个函数的各自数据。

很神奇,不是吗?现在你可以用cProfile轻松地找出机器学习程序中的瓶颈,并用snakeviz对它们进行专业的可视化。 而且从现在开始,你可以把代码剖析作为机器学习工作流程中的一个可选但强大的步骤。

最后,Python的剖析器只给你提供了时间上的统计,但没有提供内存使用情况。你可能需要为这个目的寻找另一个库或工具。