Machine-Learning-Mastery-Python-教程-六-

167 阅读52分钟

Machine Learning Mastery Python 教程(六)

原文:Machine Learning Mastery

协议:CC BY-NC-SA 4.0

运行和传递信息给 Python 脚本

原文:machinelearningmastery.com/running-and-passing-information-to-a-python-script/

运行 Python 脚本是开发过程中的一个重要步骤,因为通过这种方式你可以确定你的代码是否按预期工作。同时,我们通常需要将信息传递给 Python 脚本,以使其正常工作。

在本教程中,你将发现多种运行和传递信息给 Python 脚本的方法。

完成本教程后,你将了解到:

  • 如何使用命令行界面、Jupyter Notebook 或集成开发环境(IDE)运行 Python 脚本

  • 如何使用 sys.argv 命令将信息传递给 Python 脚本,方法是通过硬编码输入变量到 Jupyter Notebook 中或通过交互式使用 input() 函数。

**用我的新书《Python 机器学习》**启动你的项目,Python for Machine Learning,包括逐步教程Python 源代码文件。

让我们开始吧。

运行和传递信息给 Python 脚本

图片来源 Andrea Leopardi,部分权利保留。

教程概述

本教程分为两部分;它们是:

  • 运行 Python 脚本

    • 使用命令行界面

    • 使用 Jupyter Notebook

    • 使用集成开发环境(IDE)

  • Python 输入

运行 Python 脚本:

使用命令行界面

命令行界面广泛用于运行 Python 代码。

首先,根据你使用的操作系统,打开一个命令提示符或终端窗口,测试一些命令。

在命令行界面中输入 python 命令将启动一个 Python 交互式会话。你将看到一条消息,告知你所使用的 Python 版本。

Python

Python 3.7.4 (default, Aug 13 2019, 15:17:50) 
[Clang 4.0.1 (tags/RELEASE_401/final)] :: Anaconda, Inc. on darwin
Type "help", "copyright", "credits" or "license" for more information.

在交互式会话中,你在命令行界面中输入的任何语句都会立即执行。例如,输入 2 + 3 会返回 5:

Python

2 + 3

Python

5

以这种方式进行交互式会话有其优势,因为你可以轻松快速地测试 Python 代码的各行。然而,如果我们更关注编写较长的程序(如开发机器学习算法),这不是理想的选择。代码还会在交互式会话结束后消失。

另一种选择是运行 Python 脚本。我们先从一个简单的例子开始。

在文本编辑器(例如Notepad++Visual Studio CodeSublime Text)中,键入语句 print("Hello World!") 并将文件保存为test_script.py或任何其他名称,只要包含*.py*扩展名。

现在,返回到您的命令行界面,键入python命令,后跟您的脚本文件名。在执行此操作之前,您可能需要更改路径,以指向包含脚本文件的目录。运行脚本文件应该会产生以下输出:

Python

python test_script.py

Python

Hello World!

现在让我们编写一个脚本文件,加载预训练的 Keras 模型,并输出对狗图像的预测。通常情况下,我们还需要通过命令行参数向 Python 脚本传递信息。为此,我们将使用sys.argv命令将图像路径和要返回的前几个猜测的数量传递给脚本。如果代码需要,我们可以有尽可能多的输入参数,此时我们将继续从参数列表中读取输入。

现在我们将要运行的脚本文件包含以下代码:

Python

import sys
import numpy as np
from tensorflow.keras.applications import vgg16
from tensorflow.keras.applications.vgg16 import preprocess_input, decode_predictions
from tensorflow.keras.preprocessing import image

# Load the VGG16 model pre-trained on the ImageNet dataset
vgg16_model = vgg16.VGG16(weights='imagenet')

# Read the command-line argument passed to the interpreter when invoking the script
image_path = sys.argv[1]
top_guesses = sys.argv[2]

# Load the image, resized according to the model target size
img_resized = image.load_img(image_path, target_size=(224, 224))

# Convert the image into an array
img = image.img_to_array(img_resized) 

# Add in a dimension
img = np.expand_dims(img, axis=0) 

# Scale the pixel intensity values
img = preprocess_input(img) 

# Generate a prediction for the test image
pred_vgg = vgg16_model.predict(img)

# Decode and print the top 3 predictions
print('Prediction:', decode_predictions(pred_vgg, top=int(top_guesses)))

在上述代码中,我们使用sys.argv[1]sys.argv[2]读取命令行参数的前两个参数。我们可以通过使用python命令后跟脚本文件名来运行脚本,并进一步传递图像路径(在图像保存到磁盘后)和我们想要预测的前几个猜测的数量:

Python

python pretrained_model.py dog.jpg 3

在这里,pretrained_model.py是脚本文件的名称,dog.jpg图像已保存在同一个目录中,该目录还包含 Python 脚本。

生成的前三个猜测如下:

Python

Prediction: [[('n02088364', 'beagle', 0.6751468), ('n02089867', 'Walker_hound', 0.1394801), ('n02089973', 'English_foxhound', 0.057901423)]]

但在命令行中可能还有更多内容。例如,以下命令行将以“优化”模式运行脚本,在此模式下,调试变量__debug__被设置为False,并且跳过assert语句。

Python

python -O test_script.py

以下是使用 Python 模块(例如调试器)启动脚本的方法:

Python

python -m pdb test_script.py

我们将在另一篇文章中讨论调试器和分析器的使用。

使用 Jupyter Notebook

从命令行界面运行 Python 脚本是一个直接的选择,如果您的代码生成字符串输出而不是其他内容。

然而,当我们处理图像时,通常希望生成可视化输出。我们可能会检查输入图像上应用的任何预处理的正确性,然后将其馈送到神经网络中,或者可视化神经网络产生的结果。Jupyter Notebook 提供了一个交互式计算环境,可以帮助我们实现这一目标。

通过 Jupyter Notebook 界面运行 Python 脚本的一种方法是简单地将代码添加到笔记本中的一个“单元格”中。但这意味着您的代码仅留在 Jupyter 笔记本中,无法像使用上述命令行那样在其他地方访问。另一种方法是使用以%字符为前缀的运行魔术命令。尝试在 Jupyter Notebook 的一个单元格中输入以下代码:

Python

%run pretrained_model.py dog.jpg 3

在这里,我们再次指定 Python 脚本文件名为pretrained_model.py,接着是图像路径和顶部猜测的数量作为输入参数。您将看到前三个预测结果打印在生成此结果的单元格下方。

现在,假设我们想要显示输入图像,以检查它是否已按照模型目标大小加载。为此,我们将稍微修改代码如下,并保存到一个新的 Python 脚本pretrained_model_image.py中:

Python

import sys
import numpy as np
import matplotlib.pyplot as plt
from tensorflow.keras.applications import vgg16
from tensorflow.keras.applications.vgg16 import preprocess_input, decode_predictions
from tensorflow.keras.preprocessing import image

# Load the VGG16 model pre-trained on the ImageNet dataset
vgg16_model = vgg16.VGG16(weights='imagenet')

# Read the arguments passed to the interpreter when invoking the script
image_path = sys.argv[1]
top_guesses = sys.argv[2]

# Load the image, resized according to the model target size
img_resized = image.load_img(image_path, target_size=(224, 224))

# Convert the image into an array
img = image.img_to_array(img_resized)

# Display the image to check that it has been correctly resized
plt.imshow(img.astype(np.uint8))

# Add in a dimension
img = np.expand_dims(img, axis=0) 

# Scale the pixel intensity values
img = preprocess_input(img) 

# Generate a prediction for the test image
pred_vgg = vgg16_model.predict(img)

# Decode and print the top 3 predictions
print('Prediction:', decode_predictions(pred_vgg, top=int(top_guesses)))

通过 Jupyter Notebook 界面运行新保存的 Python 脚本现在显示了调整大小为224×224224 \times 224像素的图像,并打印了前三个预测结果:

Python

%run pretrained_model_image.py dog.jpg 3

在 Jupyter Notebook 中运行 Python 脚本

或者,我们可以将代码简化为以下内容(并将其保存到另一个 Python 脚本pretrained_model_inputs.py中):

Python

# Load the VGG16 model pre-trained on the ImageNet dataset
vgg16_model = vgg16.VGG16(weights='imagenet')

# Load the image, resized according to the model target size
img_resized = image.load_img(image_path, target_size=(224, 224))

# Convert the image into an array
img = image.img_to_array(img_resized) 

# Display the image to check that it has been correctly resized
plt.imshow(img.astype(np.uint8))

# Add in a dimension
img = np.expand_dims(img, axis=0) 

# Scale the pixel intensity values
img = preprocess_input(img) 

# Generate a prediction for the test image
pred_vgg = vgg16_model.predict(img)

# Decode and print the top 3 predictions
print('Prediction:', decode_predictions(pred_vgg, top=top_guesses))

并在 Jupyter Notebook 的一个单元格中定义输入变量。以这种方式运行 Python 脚本需要在%run 魔术之后使用-i 选项:

Python

%run -i pretrained_model_inputs.py

在 Jupyter Notebook 中运行 Python 脚本

这样做的优点是更轻松地访问可以交互定义的 Python 脚本内的变量。

随着您的代码增长,结合文本编辑器与 Jupyter Notebook 可能会提供一种便捷的方法:文本编辑器可用于创建 Python 脚本,存储可重用的代码,而 Jupyter Notebook 则提供了交互式计算环境,便于数据探索。

想要开始使用 Python 进行机器学习吗?

现在立即获取我的免费 7 天电子邮件快速课程(附有示例代码)。

点击注册并获得课程的免费 PDF 电子书版本。

使用集成开发环境(IDE)

另一种选择是从集成开发环境(IDE)运行 Python 脚本。这需要首先创建一个项目,并将带有*.py*扩展名的 Python 脚本添加到其中。

如果我们选择 PyCharm 或 Visual Studio Code 作为 IDE,这将要求我们创建一个新项目,然后选择我们想使用的 Python 解释器版本。在将 Python 脚本添加到新创建的项目后,可以运行它以生成输出。以下是 macOS 上运行 Visual Studio Code 的屏幕截图。根据 IDE 的不同,应该有一个选项来选择是否使用调试器运行代码。

Python 输入

到目前为止,我们考虑了使用 sys.argv 命令或在运行脚本前在 Jupyter Notebook 中硬编码输入变量的选项,以将信息传递给 Python 脚本。

另一种选择是通过 input() 函数从用户那里获取输入。

考虑以下代码:

Python

import numpy as np
import matplotlib.pyplot as plt
from tensorflow.keras.applications import vgg16
from tensorflow.keras.applications.vgg16 import preprocess_input, decode_predictions
from tensorflow.keras.preprocessing import image

# Load the VGG16 model pre-trained on the ImageNet dataset
vgg16_model = vgg16.VGG16(weights='imagenet')

# Ask the user for manual inputs
image_path = input("Enter image path: ")
top_guesses = input("Enter number of top guesses: ")

# Load the image, resized according to the model target size
img_resized = image.load_img(image_path, target_size=(224, 224))

# Convert the image into an array
img = image.img_to_array(img_resized)

# Add in a dimension
img = np.expand_dims(img, axis=0) 

# Scale the pixel intensity values
img = preprocess_input(img) 

# Generate a prediction for the test image
pred_vgg = vgg16_model.predict(img)

# Decode and print the top 3 predictions
print('Prediction:', decode_predictions(pred_vgg, top=int(top_guesses)))

在这里,系统会提示用户手动输入图像路径(图像已保存到包含 Python 脚本的同一目录中,因此只需指定图像名称即可)和要生成的 top guesses 数量。两个输入值都是字符串类型的;然而,top guesses 的数量在使用时会被转换为整数。

无论代码是在命令行界面、Jupyter Notebook 还是 Python IDE 中运行,它都会提示用户输入所需的输入值,并随后生成用户要求的预测数量。

进一步阅读

本节提供了更多相关资源,如果你希望深入了解这个话题。

书籍

总结

在本教程中,你了解了运行 Python 脚本和传递信息的各种方式。

具体来说,你学到了:

  • 如何使用命令行界面、Jupyter Notebook 或集成开发环境(IDE)运行 Python 脚本

  • 如何通过在 Jupyter Notebook 中硬编码输入变量或使用 input() 函数的交互方式,将信息传递给 Python 脚本,使用 sys.argv 命令

你有任何问题吗?

在下方评论中提出你的问题,我会尽力回答。

NumPy 和 SciPy 中的科学函数

原文:machinelearningmastery.com/scientific-functions-in-numpy-and-scipy/

Python 是一个通用计算语言,但在科学计算中非常受欢迎。由于 Python 生态系统中的一些库,它在许多情况下可以替代 R 和 Matlab。在机器学习中,我们广泛使用一些数学或统计函数,并且我们经常发现 NumPy 和 SciPy 非常有用。接下来,我们将简要概述 NumPy 和 SciPy 提供了什么以及一些使用技巧。

完成本教程后,你将了解到:

  • NumPy 和 SciPy 为你的项目提供了什么

  • 如何使用 numba 快速加速 NumPy 代码

通过我的新书《机器学习的 Python》快速启动你的项目,包括一步步的教程和所有示例的Python 源代码文件。

让我们开始吧!!

NumPy 和 SciPy 中的科学函数

照片由Nothing Ahead提供。保留所有权利。

概述

本教程分为三个部分:

  • NumPy 作为张量库

  • SciPy 的函数

  • 使用 numba 加速

NumPy 作为张量库

虽然 Python 中的列表和元组是我们本地管理数组的方式,但 NumPy 提供了更接近 C 或 Java 的数组功能,意味着我们可以强制所有元素为相同的数据类型,并且在高维数组的情况下,每个维度中的形状都是规则的。此外,在 NumPy 数组中执行相同的操作通常比在 Python 本地中更快,因为 NumPy 中的代码经过高度优化。

NumPy 提供了上千个函数,你应该查阅 NumPy 的文档以获取详细信息。以下备忘单中可以找到一些常见用法:

NumPy 备忘单。版权所有 2022 MachineLearningMastery.com

NumPy 中有一些很酷的功能值得一提,因为它们对机器学习项目很有帮助。

例如,如果我们想绘制一个 3D 曲线,我们会计算xxyy范围内的z=f(x,y)z=f(x,y),然后在xyzxyz空间中绘制结果。我们可以用以下方法生成范围:

import numpy as np
x = np.linspace(-1, 1, 100)
y = np.linspace(-2, 2, 100)

对于z=f(x,y)=1x2(y/2)2z=f(x,y)=\sqrt{1-x²-(y/2)²},我们可能需要一个嵌套的 for 循环来扫描数组xy中的每个值并进行计算。但在 NumPy 中,我们可以使用meshgrid将两个 1D 数组扩展为两个 2D 数组,通过匹配索引,我们得到所有组合,如下所示:

import matplotlib.pyplot as plt 
import numpy as np

x = np.linspace(-1, 1, 100)
y = np.linspace(-2, 2, 100)

# convert vector into 2D arrays
xx, yy = np.meshgrid(x,y)
# computation on matching
z = np.sqrt(1 - xx**2 - (yy/2)**2)

fig = plt.figure(figsize=(8,8))
ax = plt.axes(projection='3d')
ax.set_xlim([-2,2])
ax.set_ylim([-2,2])
ax.set_zlim([0,2])
ax.plot_surface(xx, yy, z, cmap="cividis")
ax.view_init(45, 35)
plt.show()

上述中,由 meshgrid() 生成的 2D 数组 xx 在同一列上具有相同的值,yy 在同一行上具有相同的值。因此,xxyy 上的逐元素操作实际上是对 xyxy 平面的操作。这就是为什么它有效,以及我们如何绘制上面的椭球体。

NumPy 另一个很好的功能是扩展维度的函数。神经网络中的卷积层通常期望 3D 图像,即 2D 像素和作为第三维度的不同颜色通道。它适用于使用 RGB 通道的彩色图像,但在灰度图像中我们只有一个通道。例如,scikit-learn 中的数字数据集:

from sklearn.datasets import load_digits
images = load_digits()["images"]
print(images.shape)
(1797, 8, 8)

这表明这个数据集有 1797 张图像,每张图像为 8×8 像素。这是一个灰度数据集,显示每个像素的黑暗值。我们将第四轴添加到这个数组中(即,将一个 3D 数组转换为 4D 数组),以便每张图像为 8x8x1 像素:

...

# image has axes 0, 1, and 2, adding axis 3
images = np.expand_dims(images, 3)
print(images.shape)
(1797, 8, 8, 1)

在处理 NumPy 数组时,一个方便的特性是布尔索引和花式索引。例如,如果我们有一个 2D 数组:

import numpy as np

X = np.array([
    [ 1.299,  0.332,  0.594, -0.047,  0.834],
    [ 0.842,  0.441, -0.705, -1.086, -0.252],
    [ 0.785,  0.478, -0.665, -0.532, -0.673],
    [ 0.062,  1.228, -0.333,  0.867,  0.371]
])

我们可以检查列中的所有值是否都是正值:

...
y = (X > 0).all(axis=0)
print(y)
array([ True,  True, False, False, False])

这仅显示前两列都是正值。注意这是一个长度为 5 的一维数组,其大小与数组 X 的轴 1 相同。如果我们在轴 1 上使用这个布尔数组作为索引,我们只选择索引为正的子数组:

...
y = X[:, (X > 0).all(axis=0)
print(y)
array([[1.299, 0.332],
       [0.842, 0.441],
       [0.785, 0.478],
       [0.062, 1.228]])

如果用整数列表代替上述布尔数组,我们根据与列表匹配的索引从 X 中选择。NumPy 称之为花式索引。如下所示,我们可以选择前两列两次并形成一个新数组:

...
y = X[:, [0,1,1,0]]
print(y)
array([[1.299, 0.332, 0.332, 1.299],
       [0.842, 0.441, 0.441, 0.842],
       [0.785, 0.478, 0.478, 0.785],
       [0.062, 1.228, 1.228, 0.062]])

SciPy 的函数

SciPy 是 NumPy 的姊妹项目。因此,你通常会看到 SciPy 函数期望 NumPy 数组作为参数或返回一个。SciPy 提供了许多不常用或更高级的函数。

SciPy 函数被组织在子模块下。一些常见的子模块包括:

  • scipy.cluster.hierarchy: 层次聚类

  • scipy.fft: 快速傅里叶变换

  • scipy.integrate: 数值积分

  • scipy.interpolate: 插值和样条函数

  • scipy.linalg: 线性代数

  • scipy.optimize: 数值优化

  • scipy.signal: 信号处理

  • scipy.sparse: 稀疏矩阵表示

  • scipy.special: 一些特殊的数学函数

  • scipy.stats: 统计,包括概率分布

但不要假设 SciPy 可以覆盖所有内容。例如,对于时间序列分析,最好依赖于 statsmodels 模块。

我们在其他文章中已经讨论了许多使用 scipy.optimize 的例子。它是一个很棒的工具,可以使用例如牛顿法来找到函数的最小值。NumPy 和 SciPy 都有 linalg 子模块用于线性代数,但 SciPy 中的函数更高级,如进行 QR 分解或矩阵指数的函数。

也许 SciPy 最常用的功能是 stats 模块。在 NumPy 和 SciPy 中,我们可以生成具有非零相关性的多变量高斯随机数。

import numpy as np
from scipy.stats import multivariate_normal
import matplotlib.pyplot as plt

mean = [0, 0]             # zero mean
cov = [[1, 0.8],[0.8, 1]] # covariance matrix
X1 = np.random.default_rng().multivariate_normal(mean, cov, 5000)
X2 = multivariate_normal.rvs(mean, cov, 5000)

fig = plt.figure(figsize=(12,6))
ax = plt.subplot(121)
ax.scatter(X1[:,0], X1[:,1], s=1)
ax.set_xlim([-4,4])
ax.set_ylim([-4,4])
ax.set_title("NumPy")

ax = plt.subplot(122)
ax.scatter(X2[:,0], X2[:,1], s=1)
ax.set_xlim([-4,4])
ax.set_ylim([-4,4])
ax.set_title("SciPy")

plt.show()

但如果我们想要引用分布函数本身,最好依赖于 SciPy。例如,著名的 68-95-99.7 规则是指标准正态分布,我们可以通过 SciPy 的累积分布函数获取确切的百分比:

from scipy.stats import norm
n = norm.cdf([1,2,3,-1,-2,-3])
print(n)
print(n[:3] - n[-3:])
[0.84134475 0.97724987 0.9986501  0.15865525 0.02275013 0.0013499 ]
[0.68268949 0.95449974 0.9973002 ]

因此我们看到,在正态分布中,我们期望 68.269%的概率值会落在均值的一个标准差范围内。相反,我们有百分位点函数作为累积分布函数的逆函数:

...
print(norm.ppf(0.99))
2.3263478740408408

这意味着,如果值服从正态分布,我们期望有 99%的概率(单尾概率)值不会超过均值的 2.32 倍标准差。

这些都是 SciPy 如何超越 NumPy 的例子。

想要开始使用 Python 进行机器学习吗?

立即参加我的免费 7 天邮件速成课程(包括示例代码)。

点击注册,还可以获得课程的免费 PDF 电子书版本。

使用 numba 提速

NumPy 比原生 Python 更快,因为许多操作是用 C 实现的并使用了优化的算法。但有时我们希望做一些事情时,NumPy 仍然太慢。

如果你有 GPU,你可以要求 numba 进一步优化,通过并行化或将操作移到 GPU 上来加速。你需要先安装 numba 模块:

pip install numba

如果你需要将 numba 编译成 Python 模块,可能需要一段时间。之后,如果你有一个纯 NumPy 操作的函数,你可以添加 numba 装饰器来加速它:

import numba

@numba.jit(nopython=True)
def numpy_only_function(...)
    ...

它的作用是使用即时编译器来向量化操作,以便它可以更快地运行。如果你的函数在程序中运行很多次(例如,梯度下降中的更新函数),你可以看到最佳的性能提升,因为编译器的开销可以得到摊销。

例如,下面是一个将 784 维数据转换为 2 维的 t-SNE 算法实现。我们不会详细解释 t-SNE 算法,但它需要很多次迭代才能收敛。以下代码展示了我们如何使用 numba 来优化内部循环函数(同时也演示了一些 NumPy 的用法)。完成需要几分钟时间。你可以尝试在之后移除 @numba.jit 装饰器。这将需要相当长的时间。

import datetime

import tensorflow as tf
import matplotlib.pyplot as plt
import numpy as np
import numba

def tSNE(X, ndims=2, perplexity=30, seed=0, max_iter=500, stop_lying_iter=100, mom_switch_iter=400):
    """The t-SNE algorithm

	Args:
		X: the high-dimensional coordinates
		ndims: number of dimensions in output domain
    Returns:
        Points of X in low dimension
    """
    momentum = 0.5
    final_momentum = 0.8
    eta = 200.0
    N, _D = X.shape
    np.random.seed(seed)

    # normalize input
    X -= X.mean(axis=0) # zero mean
    X /= np.abs(X).max() # min-max scaled

    # compute input similarity for exact t-SNE
    P = computeGaussianPerplexity(X, perplexity)
    # symmetrize and normalize input similarities
    P = P + P.T
    P /= P.sum()
    # lie about the P-values
    P *= 12.0
    # initialize solution
    Y = np.random.randn(N, ndims) * 0.0001
    # perform main training loop
    gains = np.ones_like(Y)
    uY = np.zeros_like(Y)
    for i in range(max_iter):
        # compute gradient, update gains
        dY = computeExactGradient(P, Y)
        gains = np.where(np.sign(dY) != np.sign(uY), gains+0.2, gains*0.8).clip(0.1)
        # gradient update with momentum and gains
        uY = momentum * uY - eta * gains * dY
        Y = Y + uY
        # make the solution zero-mean
        Y -= Y.mean(axis=0)
        # Stop lying about the P-values after a while, and switch momentum
        if i == stop_lying_iter:
            P /= 12.0
        if i == mom_switch_iter:
            momentum = final_momentum
        # print progress
        if (i % 50) == 0:
            C = evaluateError(P, Y)
            now = datetime.datetime.now()
            print(f"{now} - Iteration {i}: Error = {C}")
    return Y

@numba.jit(nopython=True)
def computeExactGradient(P, Y):
    """Gradient of t-SNE cost function

	Args:
        P: similarity matrix
        Y: low-dimensional coordinates
    Returns:
        dY, a numpy array of shape (N,D)
	"""
    N, _D = Y.shape
    # compute squared Euclidean distance matrix of Y, the Q matrix, and the normalization sum
    DD = computeSquaredEuclideanDistance(Y)
    Q = 1/(1+DD)
    sum_Q = Q.sum()
    # compute gradient
    mult = (P - (Q/sum_Q)) * Q
    dY = np.zeros_like(Y)
    for n in range(N):
        for m in range(N):
            if n==m: continue
            dY[n] += (Y[n] - Y[m]) * mult[n,m]
    return dY

@numba.jit(nopython=True)
def evaluateError(P, Y):
    """Evaluate t-SNE cost function

    Args:
        P: similarity matrix
        Y: low-dimensional coordinates
    Returns:
        Total t-SNE error C
    """
    DD = computeSquaredEuclideanDistance(Y)
    # Compute Q-matrix and normalization sum
    Q = 1/(1+DD)
    np.fill_diagonal(Q, np.finfo(np.float32).eps)
    Q /= Q.sum()
    # Sum t-SNE error: sum P log(P/Q)
    error = P * np.log( (P + np.finfo(np.float32).eps) / (Q + np.finfo(np.float32).eps) )
    return error.sum()

@numba.jit(nopython=True)
def computeGaussianPerplexity(X, perplexity):
    """Compute Gaussian Perplexity

    Args:
        X: numpy array of shape (N,D)
        perplexity: double
    Returns:
        Similarity matrix P
    """
    # Compute the squared Euclidean distance matrix
    N, _D = X.shape
    DD = computeSquaredEuclideanDistance(X)
    # Compute the Gaussian kernel row by row
    P = np.zeros_like(DD)
    for n in range(N):
        found = False
        beta = 1.0
        min_beta = -np.inf
        max_beta = np.inf
        tol = 1e-5

        # iterate until we get a good perplexity
        n_iter = 0
        while not found and n_iter < 200:
            # compute Gaussian kernel row
            P[n] = np.exp(-beta * DD[n])
            P[n,n] = np.finfo(np.float32).eps
            # compute entropy of current row
            # Gaussians to be row-normalized to make it a probability
            # then H = sum_i -P[i] log(P[i])
            #        = sum_i -P[i] (-beta * DD[n] - log(sum_P))
            #        = sum_i P[i] * beta * DD[n] + log(sum_P)
            sum_P = P[n].sum()
            H = beta * (DD[n] @ P[n]) / sum_P + np.log(sum_P)
            # Evaluate if entropy within tolerance level
            Hdiff = H - np.log2(perplexity)
            if -tol < Hdiff < tol:
                found = True
                break
            if Hdiff > 0:
                min_beta = beta
                if max_beta in (np.inf, -np.inf):
                    beta *= 2
                else:
                    beta = (beta + max_beta) / 2
            else:
                max_beta = beta
                if min_beta in (np.inf, -np.inf):
                    beta /= 2
                else:
                    beta = (beta + min_beta) / 2
            n_iter += 1
        # normalize this row
        P[n] /= P[n].sum()
    assert not np.isnan(P).any()
    return P

@numba.jit(nopython=True)
def computeSquaredEuclideanDistance(X):
    """Compute squared distance
    Args:
        X: numpy array of shape (N,D)
    Returns:
        numpy array of shape (N,N) of squared distances
    """
    N, _D = X.shape
    DD = np.zeros((N,N))
    for i in range(N-1):
        for j in range(i+1, N):
            diff = X[i] - X[j]
            DD[j][i] = DD[i][j] = diff @ diff
    return DD

(X_train, y_train), (X_test, y_test) = tf.keras.datasets.mnist.load_data()
# pick 1000 samples from the dataset
rows = np.random.choice(X_test.shape[0], 1000, replace=False)
X_data = X_train[rows].reshape(1000, -1).astype("float")
X_label = y_train[rows]
# run t-SNE to transform into 2D and visualize in scatter plot
Y = tSNE(X_data, 2, 30, 0, 500, 100, 400)
plt.figure(figsize=(8,8))
plt.scatter(Y[:,0], Y[:,1], c=X_label)
plt.show()

进一步阅读

本节提供了更多关于该主题的资源,如果你希望更深入地了解。

API 文档

总结

在本教程中,你简要了解了 NumPy 和 SciPy 提供的函数。

具体来说,你学到了:

  • 如何使用 NumPy 数组

  • SciPy 提供的一些帮助函数

  • 如何通过使用来自 numba 的 JIT 编译器加快 NumPy 代码的运行速度

在 Python 中设置断点和异常钩子

原文:machinelearningmastery.com/setting-breakpoints-and-exception-hooks-in-python/

在 Python 中调试代码有多种方法,其中之一是在代码中引入断点,以便在希望调用 Python 调试器的地方设置断点。不同调用点使用的语句取决于你所使用的 Python 解释器版本,正如我们在本教程中将看到的那样。

在本教程中,你将发现设置断点的各种方法,适用于不同版本的 Python。

完成本教程后,你将了解:

  • 如何在早期版本的 Python 中调用 pdb 调试器

  • 如何使用 Python 3.7 中引入的新内置 breakpoint() 函数

  • 如何编写自己的 breakpoint() 函数,以简化早期版本 Python 中的调试过程

  • 如何使用事后调试器

通过我的新书 《Python 机器学习》 启动你的项目,其中包括 逐步教程所有示例的 Python 源代码 文件。

开始吧。

在不同版本的 Python 中设置断点

照片由 Josh Withers 提供,部分权利保留。

教程概述

本教程分为三个部分,它们是:

  • 在 Python 代码中设置断点

    • 在早期版本的 Python 中调用 pdb 调试器

    • 在 Python 3.7 中使用 breakpoint() 函数

  • 为早期版本的 Python 编写自己的 breakpoint() 函数

  • breakpoint() 函数的限制

在 Python 代码中设置断点

我们之前已经看到 调试 Python 脚本的一种方法是使用 Python 调试器在命令行中运行它。

为此,我们需要使用 -m pdb 命令,该命令在执行 Python 脚本之前加载 pdb 模块。在相同的命令行界面中,我们可以跟随一个特定的调试器命令,例如 n 以移动到下一行,或 s 如果我们打算进入一个函数。

随着代码长度的增加,这种方法可能会变得繁琐。解决这个问题并更好地控制代码断点的一种方法是直接在代码中插入断点。

在早期版本的 Python 中调用 pdb 调试器

在 Python 3.7 之前调用 pdb 调试器,需要导入 pdb 并在代码中希望进入交互调试会话的地方调用 pdb.set_trace()

如果我们重新考虑,比如说,代码用于 实现通用注意力机制,我们可以按如下方式进入代码:

Python

from numpy import array
from numpy import random
from numpy import dot
from scipy.special import softmax

# importing the Python debugger module
import pdb

# encoder representations of four different words
word_1 = array([1, 0, 0])
word_2 = array([0, 1, 0])
word_3 = array([1, 1, 0])
word_4 = array([0, 0, 1])

# stacking the word embeddings into a single array
words = array([word_1, word_2, word_3, word_4])

# generating the weight matrices
random.seed(42)
W_Q = random.randint(3, size=(3, 3))
W_K = random.randint(3, size=(3, 3))
W_V = random.randint(3, size=(3, 3))

# generating the queries, keys and values
Q = dot(words, W_Q)
K = dot(words, W_K)
V = dot(words, W_V)

# inserting a breakpoint
pdb.set_trace()

# scoring the query vectors against all key vectors
scores = dot(Q, K.transpose())

# computing the weights by a softmax operation
weights = softmax(scores / K.shape[1] ** 0.5, axis=1)

# computing the attention by a weighted sum of the value vectors
attention = dot(weights, V)

print(attention)

现在执行脚本会在计算变量 scores 之前打开 pdb 调试器,我们可以继续发出任何调试器命令,例如 n 以移动到下一行或 c 以继续执行:

Python

/Users/mlm/main.py(33)<module>()
-> scores = dot(Q, K.transpose())
(Pdb) n
> /Users/mlm/main.py(36)<module>()
-> weights = softmax(scores / K.shape[1] ** 0.5, axis=1)
(Pdb) c
[[0.98522025 1.74174051 0.75652026]
 [0.90965265 1.40965265 0.5       ]
 [0.99851226 1.75849334 0.75998108]
 [0.99560386 1.90407309 0.90846923]]

尽管功能正常,但这不是将断点插入代码的最优雅和直观的方法。Python 3.7 实现了一种更直接的方法,接下来我们将看到。

在 Python 3.7 中使用 breakpoint() 函数

Python 3.7 附带了一个内置的 breakpoint() 函数,该函数在调用站点(即 breakpoint() 语句所在的代码点)进入 Python 调试器。

当调用时,breakpoint() 函数的默认实现会调用 sys.breakpointhook(),而 sys.breakpointhook() 进而调用 pdb.set_trace() 函数。这很方便,因为我们不需要自己显式地导入 pdb 并调用 pdb.set_trace()

让我们重新考虑实现通用注意力机制的代码,并通过 breakpoint() 语句引入一个断点:

Python

from numpy import array
from numpy import random
from scipy.special import softmax

# encoder representations of four different words
word_1 = array([1, 0, 0])
word_2 = array([0, 1, 0])
word_3 = array([1, 1, 0])
word_4 = array([0, 0, 1])

# stacking the word embeddings into a single array
words = array([word_1, word_2, word_3, word_4])

# generating the weight matrices
random.seed(42)
W_Q = random.randint(3, size=(3, 3))
W_K = random.randint(3, size=(3, 3))
W_V = random.randint(3, size=(3, 3))

# generating the queries, keys and values
Q = words @ W_Q
K = words @ W_K
V = words @ W_V

# inserting a breakpoint
breakpoint()

# scoring the query vectors against all key vectors
scores = Q @ K.transpose()

# computing the weights by a softmax operation
weights = softmax(scores / K.shape[1] ** 0.5, axis=1)

# computing the attention by a weighted sum of the value vectors
attention = weights @ V

print(attention)

使用 breakpoint() 函数的一个优点是,调用 sys.breakpointhook() 的默认实现时,会查阅一个新的环境变量 PYTHONBREAKPOINT 的值。这个环境变量可以取不同的值,根据这些值可以执行不同的操作。

例如,将 PYTHONBREAKPOINT 的值设置为 0 会禁用所有断点。因此,您的代码可以包含尽可能多的断点,但这些断点可以很容易地被停止,而无需实际删除它们。如果(例如)包含代码的脚本名称为 main.py,我们可以通过在命令行界面中如下调用来禁用所有断点:

Python

PYTHONBREAKPOINT=0 python main.py

否则,我们可以通过在代码中设置环境变量来实现相同的结果:

Python

from numpy import array
from numpy import random
from scipy.special import softmax

# setting the value of the PYTHONBREAKPOINT environment variable
import os
os.environ['PYTHONBREAKPOINT'] = '0'

# encoder representations of four different words
word_1 = array([1, 0, 0])
word_2 = array([0, 1, 0])
word_3 = array([1, 1, 0])
word_4 = array([0, 0, 1])

# stacking the word embeddings into a single array
words = array([word_1, word_2, word_3, word_4])

# generating the weight matrices
random.seed(42)
W_Q = random.randint(3, size=(3, 3))
W_K = random.randint(3, size=(3, 3))
W_V = random.randint(3, size=(3, 3))

# generating the queries, keys and values
Q = words @ W_Q
K = words @ W_K
V = words @ W_V

# inserting a breakpoint
breakpoint()

# scoring the query vectors against all key vectors
scores = Q @ K.transpose()

# computing the weights by a softmax operation
weights = softmax(scores / K.shape[1] ** 0.5, axis=1)

# computing the attention by a weighted sum of the value vectors
attention = weights @ V

print(attention)

每次调用 sys.breakpointhook() 时,都会查阅 PYTHONBREAKPOINT 的值。这意味着该环境变量的值在代码执行期间可以更改,而 breakpoint() 函数会相应地作出反应。

PYTHONBREAKPOINT 环境变量也可以设置为其他值,例如可调用对象的名称。例如,如果我们想使用除了 pdb 之外的其他 Python 调试器,如 ipdb(如果调试器尚未安装,请先运行 pip install ipdb)。在这种情况下,我们可以在命令行界面中调用 main.py 脚本并挂钩调试器,而无需对代码本身进行任何更改:

Python

PYTHONBREAKPOINT=ipdb.set_trace python main.py

这样,breakpoint() 函数会在下一个调用站点进入 ipdb 调试器:

Python

> /Users/Stefania/Documents/PycharmProjects/BreakpointPy37/main.py(33)<module>()
     32 # scoring the query vectors against all key vectors
---> 33 scores = Q @ K.transpose()
     34 

ipdb> n
> /Users/Stefania/Documents/PycharmProjects/BreakpointPy37/main.py(36)<module>()
     35 # computing the weights by a softmax operation
---> 36 weights = softmax(scores / K.shape[1] ** 0.5, axis=1)
     37 

ipdb> c
[[0.98522025 1.74174051 0.75652026]
 [0.90965265 1.40965265 0.5       ]
 [0.99851226 1.75849334 0.75998108]
 [0.99560386 1.90407309 0.90846923]]

该函数还可以接受输入参数,如 breakpoint(*args, **kws),这些参数会传递给 sys.breakpointhook()。这是因为任何可调用对象(如第三方调试模块)可能接受可选参数,这些参数可以通过 breakpoint() 函数传递。

想要开始使用 Python 进行机器学习?

现在获取我的免费 7 天电子邮件速成课程(附示例代码)。

点击注册并免费获得课程的 PDF 电子书版本。

在早期版本的 Python 中编写自己的 breakpoint() 函数

让我们回到 Python 3.7 之前的版本不自带 breakpoint() 函数的事实。我们可以编写自己的函数。

与从 Python 3.7 开始实现的 breakpoint() 函数类似,我们可以实现一个检查环境变量值的函数,并:

  • 如果环境变量的值设置为 0,则会跳过代码中的所有断点。

  • 如果环境变量为空字符串,则进入默认的 Python pdb 调试器。

  • 根据环境变量的值进入另一个调试器。

Python

...

# defining our breakpoint() function
def breakpoint(*args, **kwargs):
    import importlib
    # reading the value of the environment variable
    val = os.environ.get('PYTHONBREAKPOINT')
    # if the value has been set to 0, skip all breakpoints
    if val == '0':
        return None
    # else if the value is an empty string, invoke the default pdb debugger
    elif len(val) == 0:
        hook_name = 'pdb.set_trace'
    # else, assign the value of the environment variable
    else:
        hook_name = val
    # split the string into the module name and the function name
    mod, dot, func = hook_name.rpartition('.')
    # get the function from the module
    module = importlib.import_module(mod)
    hook = getattr(module, func)

    return hook(*args, **kwargs)

...

我们可以将这个函数包含到代码中并运行(在此例中使用 Python 2.7 解释器)。如果我们将环境变量的值设置为空字符串,我们会发现 pdb 调试器会停在我们放置了 breakpoint() 函数的代码点。然后,我们可以从那里开始在命令行中输入调试器命令:

Python

from numpy import array
from numpy import random
from numpy import dot
from scipy.special import softmax

# setting the value of the environment variable
import os
os.environ['PYTHONBREAKPOINT'] = ''

# defining our breakpoint() function
def breakpoint(*args, **kwargs):
    import importlib
    # reading the value of the environment variable
    val = os.environ.get('PYTHONBREAKPOINT')
    # if the value has been set to 0, skip all breakpoints
    if val == '0':
        return None
    # else if the value is an empty string, invoke the default pdb debugger
    elif len(val) == 0:
        hook_name = 'pdb.set_trace'
    # else, assign the value of the environment variable
    else:
        hook_name = val
    # split the string into the module name and the function name
    mod, dot, func = hook_name.rpartition('.')
    # get the function from the module
    module = importlib.import_module(mod)
    hook = getattr(module, func)

    return hook(*args, **kwargs)

# encoder representations of four different words
word_1 = array([1, 0, 0])
word_2 = array([0, 1, 0])
word_3 = array([1, 1, 0])
word_4 = array([0, 0, 1])

# stacking the word embeddings into a single array
words = array([word_1, word_2, word_3, word_4])

# generating the weight matrices
random.seed(42)
W_Q = random.randint(3, size=(3, 3))
W_K = random.randint(3, size=(3, 3))
W_V = random.randint(3, size=(3, 3))

# generating the queries, keys and values
Q = dot(words, W_Q)
K = dot(words, W_K)
V = dot(words, W_V)

# inserting a breakpoint
breakpoint()

# scoring the query vectors against all key vectors
scores = dot(Q, K.transpose())

# computing the weights by a softmax operation
weights = softmax(scores / K.shape[1] ** 0.5, axis=1)

# computing the attention by a weighted sum of the value vectors
attention = dot(weights, V)

print(attention)

Python

> /Users/Stefania/Documents/PycharmProjects/BreakpointPy27/main.py(32)breakpoint()->None
-> return hook(*args, **kwargs)
(Pdb) n
> /Users/Stefania/Documents/PycharmProjects/BreakpointPy27/main.py(59)<module>()
-> scores = dot(Q, K.transpose())
(Pdb) n
> /Users/Stefania/Documents/PycharmProjects/BreakpointPy27/main.py(62)<module>()
-> weights = softmax(scores / K.shape[1] ** 0.5, axis=1)
(Pdb) c
[[0.98522025 1.74174051 0.75652026]
 [0.90965265 1.40965265 0.5       ]
 [0.99851226 1.75849334 0.75998108]
 [0.99560386 1.90407309 0.90846923]]

同样地,如果我们将环境变量设置为:

Python

os.environ['PYTHONBREAKPOINT'] = 'ipdb.set_trace'

我们现在实现的 breakpoint() 函数会进入 ipdb 调试器并停在调用点:

Python

> /Users/Stefania/Documents/PycharmProjects/BreakpointPy27/main.py(31)breakpoint()
     30 
---> 31     return hook(*args, **kwargs)
     32 

ipdb> n
> /Users/Stefania/Documents/PycharmProjects/BreakpointPy27/main.py(58)<module>()
     57 # scoring the query vectors against all key vectors
---> 58 scores = dot(Q, K.transpose())
     59 

ipdb> n
> /Users/Stefania/Documents/PycharmProjects/BreakpointPy27/main.py(61)<module>()
     60 # computing the weights by a softmax operation
---> 61 weights = softmax(scores / K.shape[1] ** 0.5, axis=1)
     62 

ipdb> c
[[0.98522025 1.74174051 0.75652026]
 [0.90965265 1.40965265 0.5       ]
 [0.99851226 1.75849334 0.75998108]
 [0.99560386 1.90407309 0.90846923]]

将环境变量设置为 0 会跳过所有断点,计算得到的注意力输出会按预期返回到命令行:

Python

os.environ['PYTHONBREAKPOINT'] = '0'

Python

[[0.98522025 1.74174051 0.75652026]
 [0.90965265 1.40965265 0.5       ]
 [0.99851226 1.75849334 0.75998108]
 [0.99560386 1.90407309 0.90846923]]

这简化了 Python 3.7 之前版本的代码调试过程,因为现在只需设置环境变量的值,而不必手动在代码中的不同调用点引入(或移除)import pdb; pdb.set_trace() 语句。

breakpoint() 函数的限制

breakpoint() 函数允许你在程序的某个点引入调试器。你需要找到需要调试器放置断点的确切位置。如果你考虑以下代码:

try:
    func()
except:
    breakpoint()
    print("exception!")

当函数 func() 引发异常时,这将带来调试器。它可以由函数自身或它调用的其他函数中的深处触发。但调试器会在上述 print("exception!") 这一行开始,这可能不是很有用。

当事后调试器启动时,我们可以在异常点调试器处打印回溯和异常。这种方式被称为事后调试器。当未捕获异常被引发时,它会请求 Python 将调试器pdb.pm()注册为异常处理程序。当调用它时,它将查找最后引发的异常并从那一点开始启动调试器。要使用事后调试器,我们只需在运行程序之前添加以下代码:

import sys
import pdb

def debughook(etype, value, tb):
    pdb.pm() # post-mortem debugger
sys.excepthook = debughook

这很方便,因为程序中不需要进行任何其他更改。例如,假设我们想要使用以下程序评估1/x1/x的平均值。很容易忽视一些边界情况,但是当引发异常时,我们可以捕获问题:

import sys
import pdb
import random

def debughook(etype, value, tb):
    pdb.pm() # post-mortem debugger
sys.excepthook = debughook

# Experimentally find the average of 1/x where x is a random integer in 0 to 9999
N = 1000
randomsum = 0
for i in range(N):
    x = random.randint(0,10000)
    randomsum += 1/x

print("Average is", randomsum/N)

当我们运行上述程序时,程序可能会终止,或者在循环中的随机数生成器生成零时可能会引发除零异常。在这种情况下,我们可能会看到以下内容:

> /Users/mlm/py_pmhook.py(17)<module>()
-> randomsum += 1/x
(Pdb) p i
16
(Pdb) p x
0

我们找到了异常引发的位置以及我们可以像通常在pdb中做的那样检查变量的值。

实际上,在启动事后调试器时,打印回溯和异常更加方便:

import sys
import pdb
import traceback

def debughook(etype, value, tb):
    traceback.print_exception(etype, value, tb)
    print() # make a new line before launching post-mortem
    pdb.pm() # post-mortem debugger
sys.excepthook = debughook

调试器会话将如下启动:

Traceback (most recent call last):
  File "/Users/mlm/py_pmhook.py", line 17, in <module>
    randomsum += 1/x
ZeroDivisionError: division by zero

> /Users/mlm/py_pmhook.py(17)<module>()
-> randomsum += 1/x
(Pdb)

进一步阅读

如果你希望深入了解,本节提供了更多关于这个主题的资源。

网站

总结

在本教程中,你了解了在不同版本的 Python 中设置断点的各种方法。

具体来说,你学到了:

  • 如何在早期版本的 Python 中调用 pdb 调试器。

  • 如何使用 Python 3.7 中引入的新内置断点函数breakpoint()

  • 如何编写自己的断点函数以简化早期版本 Python 中的调试过程

你有任何问题吗?

在下面的评论中提出你的问题,我会尽力回答。

Python 中的一些语言特性

原文:machinelearningmastery.com/some-language-features-in-python/

Python 语言的语法非常强大且富有表现力。因此,用 Python 表达一个算法简洁明了。也许这就是它在机器学习中受欢迎的原因,因为在开发机器学习模型时,我们需要进行大量实验。

如果你对 Python 不熟悉但有其他编程语言的经验,你会发现 Python 的语法有时容易理解但又奇怪。如果你习惯于用 C++或 Java 编写代码,然后转到 Python,可能你的程序就不是Pythonic的。

在本教程中,我们将涵盖 Python 中的几种常见语言特性,这些特性使其与其他编程语言有所区别。

用我的新书Python for Machine Learning启动你的项目,包括逐步教程和所有示例的Python 源代码文件。

让我们开始吧。

Python 中的一些语言特性

图片由David Clode提供,部分权利保留。

教程概述

本教程分为两部分;它们是:

  1. 操作符

  2. 内置数据结构

  3. 特殊变量

  4. 内置函数

操作符

Python 中使用的大多数操作符与其他语言相同。优先级表如下,采用自 Python 语言参考第六章(docs.python.org/3/reference/expressions.html):

Operator描述
(expressions…), [expressions…], {key: value…}, {expressions…}绑定或括号表达式、列表显示、字典显示、集合显示
x[index], x[index:index], x(arguments…), x.attribute订阅、切片、调用、属性引用
await x等待表达式
**幂运算
+x, -x, ~x正数、负数、按位非
*, @, /, //, %乘法、矩阵乘法、除法、地板除法、余数
+, –加法和减法
<<, >>位移
&按位与
按位异或
|按位或
in, not in, is, is not, <, <=, >, >=, !=, ==比较,包括成员测试和身份测试
not x布尔非
and布尔与
or布尔或
if – else条件表达式
lambdaLambda 表达式
:=赋值表达式

与其他语言的一些关键区别:

  • 布尔运算符是完整拼写的,而位运算符是字符&^|

  • 幂运算使用2**3

  • 整数除法使用//,而除法/总是返回浮点值

  • 三元运算符:如果你熟悉 C 语言中的表达式(x)?a:b,我们在 Python 中写作a if x else b

  • 比较两个东西是否相等可以使用 ==is== 运算符对于相等性与其他语言相同,但 is 更严格,保留用于检查两个变量是否指向同一个对象。

在 Python 中,我们允许在比较操作符中进行连接。例如,要测试一个值是否在 -1 到 +1 之间,我们可以这样做:

Python

if value > -1 and value < 1:
    ...

但我们也可以这样做:

if -1 < value < 1:
    ...

内置数据结构

和许多其他语言一样,Python 中有整数和浮点数数据类型。但也有复数(例如 3+1j),布尔常量(TrueFalse),字符串,以及一个虚拟类型 None

但 Python 作为一种语言的强大之处在于它内置了容器类型:Python 数组称为“列表”,它会自动扩展。关联数组(或哈希表)称为“字典”。我们还有“元组”作为只读列表和“集合”作为存储唯一项的容器。例如,在 C++ 中,您需要 STL 来提供这些功能。

"dict" 数据结构可能是 Python 中最强大的一个,让我们在编写代码时更加方便。例如,在狗和猫的图像分类问题中,我们的机器学习模型可能只会给出 0 或 1 的值,如果想要打印名称,我们可以这样做:

value = 0 # This is obtained from a model

value_to_name = {0: "cat", 1: "dog"}
print("Result is %s" % value_to_name[value])
Result is cat

在这种情况下,我们使用字典 value_to_name 作为查找表。类似地,我们还可以利用字典来构建计数器:

sentence = "Portez ce vieux whisky au juge blond qui fume"
counter = {}
for char in sentence:
    if char not in counter:
        counter[char] = 0
    counter[char] += 1

print(counter)
{'P': 1, 'o': 2, 'r': 1, 't': 1, 'e': 5, 'z': 1, ' ': 8, 'c': 1, 'v': 1, 'i': 3, 'u': 5, 'x': 1, 'w': 1, 'h': 1, 's': 1, 'k': 1, 'y': 1, 'a': 1, 'j': 1, 'g': 1, 'b': 1, 'l': 1, 'n': 1, 'd': 1, 'q': 1, 'f': 1, 'm': 1}

这将构建一个名为 counter 的字典,将每个字符映射到句子中出现的次数。

Python 列表还具有强大的语法。与某些其他语言不同,我们可以将任何东西放入列表中:

A = [1, 2, "fizz", 4, "buzz", "fizz", 7]
A += [8, "fizz", "buzz", 11, "fizz", 13, 14, "fizzbuzz"]
print(A)
[1, 2, 'fizz', 4, 'buzz', 'fizz', 7, 8, 'fizz', 'buzz', 11, 'fizz', 13, 14, 'fizzbuzz']

我们可以使用 + 连接列表。在上例中,我们使用 += 来扩展列表 A

Python 列表具有切片语法。例如,在上述 A 中,我们可以使用 A[1:3] 表示第 1 和第 2 个元素,即 [2, "fizz"],而 A[1:1] 则是一个空列表。事实上,我们可以将某些内容分配给一个切片,以插入或删除一些元素。例如:

...
A[2:2] = [2.1, 2.2]
print(A)
[1, 2, 2.1, 2.2, 'fizz', 4, 'buzz', 'fizz', 7, 8, 'fizz', 'buzz', 11, 'fizz', 13, 14, 'fizzbuzz']

然后,

...
A[0:2] = []
print(A)
[2.1, 2.2, 'fizz', 4, 'buzz', 'fizz', 7, 8, 'fizz', 'buzz', 11, 'fizz', 13, 14, 'fizzbuzz']

元组与列表具有类似的语法,只是使用圆括号来定义:

A = ("foo", "bar")

元组是不可变的。这意味着一旦定义,就无法修改它。在 Python 中,如果用逗号分隔几个东西放在一起,它被认为是一个元组。这样做的意义在于,我们可以以非常清晰的语法交换两个变量:

a = 42
b = "foo"
print("a is %s; b is %s" % (a,b))
a, b = b, a # swap
print("After swap, a is %s; b is %s" % (a,b))
a is 42; b is foo
After swap, a is foo; b is 42

最后,正如您在上面的示例中看到的那样,Python 字符串支持即时替换。与 C 中的 printf() 函数类似的模板语法,我们可以使用 %s 替换字符串或 %d 替换整数。我们还可以使用 %.3f 替换带有三位小数的浮点数。以下是一个示例:

template = "Square root of %d is %.3f"
n = 10
answer = template % (n, n**0.5)
print(answer)
Square root of 10 is 3.162

但这只是其中的一种方法。上述内容也可以通过 f-string 和 format() 方法来实现。

特殊变量

Python 有几个“特殊变量”预定义。__name__ 告诉当前命名空间,而 __file__ 告诉脚本的文件名。更多的特殊变量存在于对象内部,但几乎所有的通常不应该被直接使用。作为一种惯例(即,仅仅是一种习惯,没有人阻止你这样做),我们以单下划线或双下划线作为前缀来命名内部变量(顺便提一下,双下划线有些人称之为“dunder”)。如果你来自 C++ 或 Java,这些相当于类的私有成员,尽管它们在技术上并不是私有的。

一个值得注意的“特殊”变量是 _,仅一个下划线字符。按照惯例,它表示我们不关心的变量。为什么需要一个不关心的变量?因为有时你会保存一个函数的返回值。例如,在 pandas 中,我们可以扫描数据框的每一行:

import pandas as pd
A = pd.DataFrame([[1,2,3],[2,3,4],[3,4,5],[5,6,7]], columns=["x","y","z"])
print(A)

for _, row in A.iterrows():
    print(row["z"])
x y z
0 1 2 3
1 2 3 4
2 3 4 5
3 5 6 7

3
4
5
7

在上述内容中,我们可以看到数据框有三列,“x”、“y”和“z”,行由 0 到 3 进行索引。如果我们调用 A.iterrows(),它会逐行返回索引和行,但我们不关心索引。我们可以创建一个新变量来保存它但不使用它。为了明确我们不会使用它,我们使用 _ 作为保存索引的变量,而行则存储到变量 row 中。

想要开始学习用于机器学习的 Python 吗?

现在就领取我的免费 7 天电子邮件速成课程(附示例代码)。

点击注册,还可以获得课程的免费 PDF 电子书版本。

内置函数

在 Python 中,一些函数被定义为内置函数,而其他功能则通过其他包提供。所有内置函数的列表可以在 Python 标准库文档中找到(docs.python.org/3/library/functions.html)。以下是 Python 3.10 中定义的函数:

abs()
aiter()
all()
any()
anext()
ascii()
bin()
bool()
breakpoint()
bytearray()
bytes()
callable()
chr()
classmethod()
compile()
complex()
delattr()
dict()
dir()
divmod()
enumerate()
eval()
exec()
filter()
float()
format()
frozenset()
getattr()
globals()
hasattr()
hash()
help()
hex()
id()
input()
int()
isinstance()
issubclass()
iter()
len()
list()
locals()
map()
max()
memoryview()
min()
next()
object()
oct()
open()
ord()
pow()
print()
property()
range()
repr()
reversed()
round()
set()
setattr()
slice()
sorted()
staticmethod()
str()
sum()
super()
tuple()
type()
vars()
zip()
__import__()

并非所有的函数每天都会用到,但有些特别值得注意:

zip() 允许你将多个列表组合在一起。例如,

a = ["x", "y", "z"]
b = [3, 5, 7, 9]
c = [2.1, 2.5, 2.9]
for x in zip(a, b, c):
    print(x)
('x', 3, 2.1)
('y', 5, 2.5)
('z', 7, 2.9)

如果你想“旋转”一个列表的列表,这很方便,例如,

a = [['x', 3, 2.1], ['y', 5, 2.5], ['z', 7, 2.9]]
p,q,r = zip(*a)
print(p)
print(q)
print(r)
('x', 'y', 'z')
(3, 5, 7)
(2.1, 2.5, 2.9)

enumerate() 非常方便,可以让你对列表项进行编号,例如:

a = ["quick", "brown", "fox", "jumps", "over"]
for num, item in enumerate(a):
    print("item %d is %s" % (num, item))
item 0 is quick
item 1 is brown
item 2 is fox
item 3 is jumps
item 4 is over

如果你不使用 enumerate,这等同于以下操作:

a = ["quick", "brown", "fox", "jumps", "over"]
for num in range(len(a)):
    print("item %d is %s" % (num, a[num]))

与其他语言相比,Python 中的 for 循环是迭代一个预定义的范围,而不是在每次迭代中计算值。换句话说,它没有直接等同于以下的 C for 循环:

C

for (i=0; i<100; ++i) {
...
}

在 Python 中,我们必须使用 range() 来完成相同的操作:

for i in range(100):
    ...

类似地,有一些函数用于操作列表(或类似列表的数据结构,Python 称之为“可迭代对象”):

  • max(a):查找列表 a 中的最大值

  • min(a):查找列表 a 中的最小值

  • sum(a):查找列表 a 中值的总和

  • reverse(a):从列表 a 的末尾开始迭代

  • sorted(a):返回一个按排序顺序排列的列表 a 的副本

我们将在下一篇文章中进一步讨论这些内容。

进一步阅读

上述内容仅突出了 Python 中的一些关键特性。毫无疑问,没有比 Python.org 的官方文档更权威的资料了;所有初学者都应从 Python 教程开始,并查看语言参考以获取语法细节,标准库则提供了 Python 安装附带的额外库:

对于书籍,Lutz 的 Learning Python 是一个老而好的入门书籍。之后,流畅的 Python 可以帮助您更好地理解语言的内部结构。然而,如果您想快速入门,Al Sweigart 的书籍可以通过示例帮助您快速掌握语言。一旦熟悉 Python,您可能希望从 Python Cookbook 中获取某个特定任务的快速技巧。

总结

在本教程中,您发现了 Python 的一些独特特性。具体来说,您学到了:

  • Python 提供的运算符

  • 一些内置数据结构的使用

  • 一些经常使用的内置函数及其实用性

Python 中的静态分析器

原文:machinelearningmastery.com/static-analyzers-in-python/

静态分析器是帮助你检查代码而不实际运行代码的工具。最基本的静态分析器形式是你最喜欢的编辑器中的语法高亮器。如果你需要编译代码(比如在 C++中),你的编译器,如 LLVM,可能还会提供一些静态分析器功能,以警告你潜在的问题(例如,C++中的误用赋值“=”代替等于“==”)。在 Python 中,我们有一些工具来识别潜在错误或指出代码标准的违反。

完成本教程后,你将学习一些这些工具。具体来说,

  • 工具 Pylint、Flake8 和 mypy 能做什么?

  • 什么是编码风格违规?

  • 我们如何使用类型提示来帮助分析器识别潜在的错误?

通过我的新书Python for Machine Learning启动你的项目,包括逐步教程和所有示例的Python 源代码文件。

让我们开始吧!

Python 中的静态分析器

图片由Skylar Kang提供。一些权利保留

概述

本教程分为三个部分;它们是:

  • Pylint 简介

  • Flake8 简介

  • mypy 简介

Pylint

Lint 是很久以前为 C 创建的静态分析器的名称。Pylint 借用了这个名字,并且是最广泛使用的静态分析器之一。它作为一个 Python 包提供,我们可以通过pip安装:

Shell

$ pip install pylint

然后我们在系统中有命令pylint可用。

Pylint 可以检查一个脚本或整个目录。例如,如果我们将以下脚本保存为lenet5-notworking.py

import numpy as np
import h5py
import tensorflow as tf
from tensorflow.keras.datasets import mnist
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Conv2D, Dense, AveragePooling2D, Dropout, Flatten
from tensorflow.keras.utils import to_categorical
from tensorflow.keras.callbacks import EarlyStopping

# Load MNIST digits
(X_train, Y_train), (X_test, Y_test) = mnist.load_data()

# Reshape data to (n_samples, height, wiedth, n_channel)
X_train = np.expand_dims(X_train, axis=3).astype("float32")
X_test = np.expand_dims(X_test, axis=3).astype("float32")

# One-hot encode the output
y_train = to_categorical(y_train)
y_test = to_categorical(y_test)

# LeNet5 model
def createmodel(activation):
    model = Sequential([
        Conv2D(6, (5,5), input_shape=(28,28,1), padding="same", activation=activation),
        AveragePooling2D((2,2), strides=2),
        Conv2D(16, (5,5), activation=activation),
        AveragePooling2D((2,2), strides=2),
        Conv2D(120, (5,5), activation=activation),
        Flatten(),
        Dense(84, activation=activation),
        Dense(10, activation="softmax")
    ])
    return model

# Train the model
model = createmodel(tanh)
model.compile(loss="categorical_crossentropy", optimizer="adam", metrics=["accuracy"])
earlystopping = EarlyStopping(monitor="val_loss", patience=4, restore_best_weights=True)
model.fit(X_train, y_train, validation_data=(X_test, y_test), epochs=100, batch_size=32, callbacks=[earlystopping])

# Evaluate the model
print(model.evaluate(X_test, y_test, verbose=0))
model.save("lenet5.h5")

我们可以在运行代码之前请 Pylint 告诉我们代码的质量如何:

Shell

$ pylint lenet5-notworking.py

输出如下:

************* Module lenet5-notworking
lenet5-notworking.py:39:0: C0301: Line too long (115/100) (line-too-long)
lenet5-notworking.py:1:0: C0103: Module name "lenet5-notworking" doesn't conform to snake_case naming style (invalid-name)
lenet5-notworking.py:1:0: C0114: Missing module docstring (missing-module-docstring)
lenet5-notworking.py:4:0: E0611: No name 'datasets' in module 'LazyLoader' (no-name-in-module)
lenet5-notworking.py:5:0: E0611: No name 'models' in module 'LazyLoader' (no-name-in-module)
lenet5-notworking.py:6:0: E0611: No name 'layers' in module 'LazyLoader' (no-name-in-module)
lenet5-notworking.py:7:0: E0611: No name 'utils' in module 'LazyLoader' (no-name-in-module)
lenet5-notworking.py:8:0: E0611: No name 'callbacks' in module 'LazyLoader' (no-name-in-module)
lenet5-notworking.py:18:25: E0601: Using variable 'y_train' before assignment (used-before-assignment)
lenet5-notworking.py:19:24: E0601: Using variable 'y_test' before assignment (used-before-assignment)
lenet5-notworking.py:23:4: W0621: Redefining name 'model' from outer scope (line 36) (redefined-outer-name)
lenet5-notworking.py:22:0: C0116: Missing function or method docstring (missing-function-docstring)
lenet5-notworking.py:36:20: E0602: Undefined variable 'tanh' (undefined-variable)
lenet5-notworking.py:2:0: W0611: Unused import h5py (unused-import)
lenet5-notworking.py:3:0: W0611: Unused tensorflow imported as tf (unused-import)
lenet5-notworking.py:6:0: W0611: Unused Dropout imported from tensorflow.keras.layers (unused-import)

-------------------------------------
Your code has been rated at -11.82/10

如果你将模块的根目录提供给 Pylint,Pylint 将检查该模块的所有组件。在这种情况下,你会看到每行开头的不同文件路径。

这里有几点需要注意。首先,Pylint 的抱怨分为不同的类别。最常见的是我们会看到关于规范(即风格问题)、警告(即代码可能以与预期不同的方式运行)和错误(即代码可能无法运行并抛出异常)的问题。它们通过像 E0601 这样的代码来标识,其中第一个字母是类别。

Pylint 可能会出现误报。在上面的例子中,我们看到 Pylint 将从 tensorflow.keras.datasets 的导入标记为错误。这是由于 Tensorflow 包中的优化,导致在导入 Tensorflow 时,Python 并不会扫描和加载所有内容,而是创建了一个 LazyLoader 以仅加载大型包的必要部分。这可以显著节省程序启动时间,但也会使 Pylint 误以为我们导入了不存在的东西。

此外,Pylint 的一个关键特性是帮助我们使代码符合 PEP8 编码风格。例如,当我们定义一个没有文档字符串的函数时,即使代码没有任何错误,Pylint 也会抱怨我们没有遵循编码规范。

但 Pylint 最重要的用途是帮助我们识别潜在的问题。例如,我们将 y_train 拼写为大写的 Y_train。Pylint 会告诉我们我们在使用一个未赋值的变量。它不会直接告诉我们出了什么问题,但肯定会指向我们审校代码的正确位置。类似地,当我们在第 23 行定义变量 model 时,Pylint 告诉我们在外部范围内有一个同名变量。因此,稍后的 model 引用可能不是我们想的那样。类似地,未使用的导入可能只是因为我们拼错了模块名称。

这些都是 Pylint 提供的 提示。我们仍然需要运用判断来修正代码(或忽略 Pylint 的抱怨)。

但如果你知道 Pylint 应该停止抱怨的内容,你可以要求忽略这些。例如,我们知道 import 语句是可以的,所以我们可以用以下命令调用 Pylint:

Shell

$ pylint -d E0611 lenet5-notworking.py

现在,所有代码 E0611 的错误将被 Pylint 忽略。你可以通过逗号分隔的列表禁用多个代码,例如:

Shell

$ pylint -d E0611,C0301 lenet5-notworking.py

如果你想在特定的行或代码的特定部分禁用某些问题,可以在代码中添加特殊注释,如下所示:

...
from tensorflow.keras.datasets import mnist  # pylint: disable=no-name-in-module
from tensorflow.keras.models import Sequential # pylint: disable=E0611
from tensorflow.keras.layers import Conv2D, Dense, AveragePooling2D, Dropout, Flatten
from tensorflow.keras.utils import to_categorical

魔法关键字 pylint: 将引入 Pylint 特定的指令。代码 E0611 和名称 no-name-in-module 是相同的。在上面的例子中,由于这些特殊注释,Pylint 会对最后两个导入语句提出抱怨,但不会对前两个提出抱怨。

Flake8

工具 Flake8 实际上是 PyFlakes、McCabe 和 pycodestyle 的封装器。当你使用以下命令安装 flake8 时:

Shell

$ pip install flake8

你将安装所有这些依赖项。

与 Pylint 类似,安装此软件包后,我们可以使用 flake8 命令,并可以传递一个脚本或目录进行分析。但 Flake8 的重点倾向于编码风格。因此,对于上述相同的代码,我们会看到以下输出:

Shell

$ flake8 lenet5-notworking.py
lenet5-notworking.py:2:1: F401 'h5py' imported but unused
lenet5-notworking.py:3:1: F401 'tensorflow as tf' imported but unused
lenet5-notworking.py:6:1: F401 'tensorflow.keras.layers.Dropout' imported but unused
lenet5-notworking.py:6:80: E501 line too long (85 > 79 characters)
lenet5-notworking.py:18:26: F821 undefined name 'y_train'
lenet5-notworking.py:19:25: F821 undefined name 'y_test'
lenet5-notworking.py:22:1: E302 expected 2 blank lines, found 1
lenet5-notworking.py:24:21: E231 missing whitespace after ','
lenet5-notworking.py:24:41: E231 missing whitespace after ','
lenet5-notworking.py:24:44: E231 missing whitespace after ','
lenet5-notworking.py:24:80: E501 line too long (87 > 79 characters)
lenet5-notworking.py:25:28: E231 missing whitespace after ','
lenet5-notworking.py:26:22: E231 missing whitespace after ','
lenet5-notworking.py:27:28: E231 missing whitespace after ','
lenet5-notworking.py:28:23: E231 missing whitespace after ','
lenet5-notworking.py:36:1: E305 expected 2 blank lines after class or function definition, found 1
lenet5-notworking.py:36:21: F821 undefined name 'tanh'
lenet5-notworking.py:37:80: E501 line too long (86 > 79 characters)
lenet5-notworking.py:38:80: E501 line too long (88 > 79 characters)
lenet5-notworking.py:39:80: E501 line too long (115 > 79 characters)

以字母 E 开头的错误代码来自 pycodestyle,以字母 F 开头的错误代码来自 PyFlakes。我们可以看到它抱怨代码风格问题,例如使用 (5,5) 而逗号后没有空格。我们还可以看到它可以识别变量在赋值之前的使用。但它没有捕捉到一些代码异味,例如函数 createmodel() 重新使用了在外部作用域中已经定义的变量 model

想要开始使用 Python 进行机器学习吗?

立即获取我的免费 7 天电子邮件速成课程(包含示例代码)。

点击注册,还可以获得课程的免费 PDF 电子书版本。

与 Pylint 类似,我们也可以要求 Flake8 忽略一些警告。例如,

Shell

flake8 --ignore E501,E231 lenet5-notworking.py

这些行不会被打印在输出中:

lenet5-notworking.py:2:1: F401 'h5py' imported but unused
lenet5-notworking.py:3:1: F401 'tensorflow as tf' imported but unused
lenet5-notworking.py:6:1: F401 'tensorflow.keras.layers.Dropout' imported but unused
lenet5-notworking.py:18:26: F821 undefined name 'y_train'
lenet5-notworking.py:19:25: F821 undefined name 'y_test'
lenet5-notworking.py:22:1: E302 expected 2 blank lines, found 1
lenet5-notworking.py:36:1: E305 expected 2 blank lines after class or function definition, found 1
lenet5-notworking.py:36:21: F821 undefined name 'tanh'

我们还可以使用魔法注释来禁用一些警告,例如,

...
import tensorflow as tf  # noqa: F401
from tensorflow.keras.datasets import mnist
from tensorflow.keras.models import Sequential

Flake8 将查找注释 # noqa: 来跳过这些特定行上的一些警告。

Mypy

Python 不是一种强类型语言,因此,与 C 或 Java 不同,你不需要在使用之前声明一些函数或变量的类型。但最近,Python 引入了类型提示符号,因此我们可以指定一个函数或变量意图是什么类型,而不强制遵守像强类型语言那样。

想要开始使用 Python 进行机器学习吗?

立即获取我的免费 7 天电子邮件速成课程(包含示例代码)。

点击注册,还可以获得课程的免费 PDF 电子书版本。

在 Python 中使用类型提示的最大好处之一是为静态分析工具提供额外的信息进行检查。 Mypy 是能够理解类型提示的工具。 即使没有类型提示,Mypy 仍然可以提供类似于 Pylint 和 Flake8 的警告。

我们可以从 PyPI 安装 Mypy:

Shell

$ pip install mypy

然后可以将上述示例提供给 mypy 命令:

$ mypy lenet5-notworking.py
lenet5-notworking.py:2: error: Skipping analyzing "h5py": module is installed, but missing library stubs or py.typed marker
lenet5-notworking.py:2: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports
lenet5-notworking.py:3: error: Skipping analyzing "tensorflow": module is installed, but missing library stubs or py.typed marker
lenet5-notworking.py:4: error: Skipping analyzing "tensorflow.keras.datasets": module is installed, but missing library stubs or py.typed marker
lenet5-notworking.py:5: error: Skipping analyzing "tensorflow.keras.models": module is installed, but missing library stubs or py.typed marker
lenet5-notworking.py:6: error: Skipping analyzing "tensorflow.keras.layers": module is installed, but missing library stubs or py.typed marker
lenet5-notworking.py:7: error: Skipping analyzing "tensorflow.keras.utils": module is installed, but missing library stubs or py.typed marker
lenet5-notworking.py:8: error: Skipping analyzing "tensorflow.keras.callbacks": module is installed, but missing library stubs or py.typed marker
lenet5-notworking.py:18: error: Cannot determine type of "y_train"
lenet5-notworking.py:19: error: Cannot determine type of "y_test"
lenet5-notworking.py:36: error: Name "tanh" is not defined
Found 10 errors in 1 file (checked 1 source file)

我们看到与上面的 Pylint 相似的错误,尽管有时不如 Pylint 精确(例如,变量 y_train 的问题)。然而,我们在上面看到的一个 mypy 特点是:它期望我们使用的所有库都附带一个存根,以便进行类型检查。这是因为类型提示是可选的。如果库中的代码未提供类型提示,代码仍然可以正常工作,但 mypy 无法验证。一些库提供了类型存根,使 mypy 可以更好地检查它们。

让我们考虑另一个例子:

import h5py

def dumphdf5(filename: str) -> int:
    """Open a HDF5 file and print all the dataset and attributes stored

    Args:
        filename: The HDF5 filename

    Returns:
        Number of dataset found in the HDF5 file
    """
    count: int = 0

    def recur_dump(obj) -> None:
        print(f"{obj.name} ({type(obj).__name__})")
        if obj.attrs.keys():
            print("\tAttribs:")
            for key in obj.attrs.keys():
                print(f"\t\t{key}: {obj.attrs[key]}")
        if isinstance(obj, h5py.Group):
            # Group has key-value pairs
            for key, value in obj.items():
                recur_dump(value)
        elif isinstance(obj, h5py.Dataset):
            count += 1
            print(obj[()])

    with h5py.File(filename) as obj:
        recur_dump(obj)
        print(f"{count} dataset found")

with open("my_model.h5") as fp:
    dumphdf5(fp)

这个程序应该加载一个 HDF5 文件(例如一个 Keras 模型),并打印其中存储的每个属性和数据。我们使用了 h5py 模块(它没有类型存根,因此 mypy 无法识别它使用的类型),但我们为我们定义的函数 dumphdf5() 添加了类型提示。这个函数期望一个 HDF5 文件的文件名并打印其中存储的所有内容。最后,将返回存储的数据集数量。

当我们将此脚本保存为 dumphdf5.py 并传递给 mypy 时,我们将看到如下内容:

Shell

$ mypy dumphdf5.py
dumphdf5.py:1: error: Skipping analyzing "h5py": module is installed, but missing library stubs or py.typed marker
dumphdf5.py:1: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports
dumphdf5.py:3: error: Missing return statement
dumphdf5.py:33: error: Argument 1 to "dumphdf5" has incompatible type "TextIO"; expected "str"
Found 3 errors in 1 file (checked 1 source file)

我们误用了函数,导致一个打开的文件对象被传递给dumphdf5(),而不是仅仅传递文件名(作为字符串)。Mypy 可以识别这个错误。我们还声明了该函数应该返回一个整数,但函数中没有返回语句。

然而,还有一个错误是 mypy 没有识别出来的。也就是说,内函数recur_dump()中使用的变量count应该声明为nonlocal,因为它是在作用域之外定义的。这个错误可以被 Pylint 和 Flake8 捕获,但 mypy 漏掉了它。

以下是完整的、修正过的代码,没有更多错误。注意,我们在第一行添加了魔法注释“# type: ignore”以抑制 mypy 的类型提示警告:

import h5py # type: ignore

def dumphdf5(filename: str) -> int:
    """Open a HDF5 file and print all the dataset and attributes stored

    Args:
        filename: The HDF5 filename

    Returns:
        Number of dataset found in the HDF5 file
    """
    count: int = 0

    def recur_dump(obj) -> None:
        nonlocal count
        print(f"{obj.name} ({type(obj).__name__})")
        if obj.attrs.keys():
            print("\tAttribs:")
            for key in obj.attrs.keys():
                print(f"\t\t{key}: {obj.attrs[key]}")
        if isinstance(obj, h5py.Group):
            # Group has key-value pairs
            for key, value in obj.items():
                recur_dump(value)
        elif isinstance(obj, h5py.Dataset):
            count += 1
            print(obj[()])

    with h5py.File(filename) as obj:
        recur_dump(obj)
        print(f"{count} dataset found")
    return count

dumphdf5("my_model.h5")

总之,我们上面介绍的三种工具可以互补。你可以考虑运行所有这些工具,以查找代码中的任何潜在错误或改善编码风格。每个工具都允许一些配置,无论是通过命令行还是配置文件,以适应你的需求(例如,什么样的行长度应该引发警告?)。使用静态分析器也是帮助自己提高编程技能的一种方式。

进一步阅读

本节提供了更多关于这个主题的资源,如果你想深入了解。

文章

软件包

总结

在本教程中,你已经看到一些常见的静态分析器如何帮助你编写更好的 Python 代码。具体来说,你学习了:

  • 三个工具(Pylint、Flake8 和 mypy)的优缺点

  • 如何自定义这些工具的行为

  • 如何理解这些分析器提出的投诉

编写更好 Python 代码的技术

原文:machinelearningmastery.com/techniques-to-write-better-python-code/

我们编写程序是为了问题解决或者制作一个可以重复解决类似问题的工具。对于后者,我们难免会再次回到之前编写的程序中,或者其他人会重用我们编写的程序。也有可能会遇到我们在编写程序时没有预见的数据。毕竟,我们仍然希望我们的程序能够正常运行。有一些技术和心态可以帮助我们编写更健壮的代码。

完成本教程后,你将学到

  • 如何为意外情况准备代码

  • 如何为代码无法处理的情况提供适当的信号

  • 编写更健壮程序的最佳实践是什么

通过我的新书《机器学习 Python 编程》快速启动你的项目,书中包括逐步教程和所有示例的Python 源代码文件。

让我们开始吧!!

编写更好 Python 代码的技术

图片由Anna Shvets提供。保留所有权利。

概述

本教程分为三个部分,分别是:

  • 数据清理和自我检测编程

  • 保护措施和防御性编程

  • 避免错误的最佳实践

数据清理和自我检测编程

当我们在 Python 中编写一个函数时,我们通常会接收一些参数并返回一些值。毕竟,这就是函数的本质。由于 Python 是一种鸭子类型语言,很容易看到一个接受数字的函数被字符串调用。例如:

def add(a, b):
    return a + b

c = add("one", "two")

这段代码完全正常,因为 Python 字符串中的 + 运算符表示连接。因此没有语法错误;只是它不是我们想要的函数行为。

这本不应该是个大问题,但如果函数较长,我们不应该只在后期才发现有问题。例如,我们的程序因为这样一个错误而失败和终止,这只发生在训练机器学习模型和浪费了几个小时的等待之后。如果我们能主动验证我们所假设的情况,那将会更好。这也是一个很好的实践,有助于我们向阅读我们代码的其他人传达我们在代码中期望的内容。

一个常见的做法是清理输入。例如,我们可以将上面的函数重写如下:

def add(a, b):
    if not isinstance(a, (int, float)) or not isinstance(b, (int, float)):
        raise ValueError("Input must be numbers")
    return a + b

或者,更好的是,在可能的情况下将输入转换为浮点数:

def add(a, b):
    try:
        a = float(a)
        b = float(b)
    except ValueError:
        raise ValueError("Input must be numbers")
    return a + b

这里的关键是在函数开始时进行一些“清理”,这样后续我们可以假设输入是某种格式。这样不仅可以更有信心地认为我们的代码按预期工作,而且可能使我们的主要算法更简单,因为我们通过清理排除了某些情况。为了说明这个想法,我们可以看看如何重新实现内置的range()函数:

def range(a, b=None, c=None):
    if c is None:
        c = 1
    if b is None:
        b = a
        a = 0
    values = []
    n = a
    while n < b:
        values.append(n)
        n = n + c
    return values

这是我们可以从 Python 的内置库中获取的range()的简化版本。但是,通过函数开始的两个if语句,我们知道变量abc总是有值的。然后,while循环可以像这样编写。否则,我们必须考虑调用range()的三种不同情况,即range(10)range(2,10)range(2,10,3),这会使我们的while循环变得更复杂且容易出错。

清理输入的另一个原因是为了标准化。这意味着我们应该将输入格式化为标准化格式。例如,URL 应以“http://”开头,而文件路径应始终是完整的绝对路径,如/etc/passwd,而不是像/tmp/../etc/././passwd这样的路径。标准化后的输入更容易检查其一致性(例如,我们知道/etc/passwd包含敏感的系统数据,但对/tmp/../etc/././passwd不太确定)。

你可能会想知道是否有必要通过添加这些清理操作来使代码变得更长。确实,这是一个你需要决定的平衡。通常,我们不会在每个函数上都这样做,以节省精力并且不影响计算效率。我们只在可能出错的地方这样做,即在我们作为 API 向其他用户公开的接口函数或在从用户命令行获取输入的主要函数中。

然而,我们要指出的是,以下是一种错误但常见的清理方式:

def add(a, b):
    assert isinstance(a, (int, float)), "`a` must be a number"
    assert isinstance(b, (int, float)), "`b` must be a number"
    return a + b

Python 中的assert语句如果第一个参数不为True,将引发AssertError异常(如果提供了可选消息)。尽管引发AssertError与引发ValueError在处理意外输入时没有实际上的不同,但不推荐使用assert,因为我们可以通过使用-O选项运行 Python 命令来“优化”我们的代码,即,

$ python -O script.py

在这种情况下,代码script.py中的所有assert都将被忽略。因此,如果我们的意图是停止代码的执行(包括你想在更高层次捕获异常),你应该使用if并明确地引发异常,而不是使用assert

使用assert的正确方式是帮助我们在开发代码时进行调试。例如,

def evenitems(arr):
    newarr = []
    for i in range(len(arr)):
        if i % 2 == 0:
            newarr.append(arr[i])
    assert len(newarr) * 2 >= len(arr)
    return newarr

在我们开发这个函数时,我们不能确定算法是否正确。有许多事情需要检查,但在这里我们希望确保如果我们从输入中提取了每个偶数索引的项,它的长度应该至少是输入数组长度的一半。当我们尝试优化算法或修饰代码时,这个条件必须不会被破坏。我们在关键位置保留 assert 语句,以确保在修改后代码没有被破坏。你可以将这看作是另一种单元测试方法。但通常,当我们检查函数的输入和输出是否符合预期时,我们称之为单元测试。以这种方式使用 assert 是为了检查函数内部的步骤。

如果我们编写复杂的算法,添加 assert 来检查循环不变量是很有帮助的,即循环应该遵守的条件。考虑以下对排序数组进行二分查找的代码:

def binary_search(array, target):
    """Binary search on array for target

    Args:
        array: sorted array
        target: the element to search for
    Returns:
        index n on the array such that array[n]==target
        if the target not found, return -1
    """
    s,e = 0, len(array)
    while s < e:
        m = (s+e)//2
        if array[m] == target:
            return m
        elif array[m] > target:
            e = m
        elif array[m] < target:
            s = m+1
        assert m != (s+e)//2, "we didn't move our midpoint"
    return -1

最后的 assert 语句是为了维护我们的循环不变量。这是为了确保我们在更新起始游标 s 和结束游标 e 时没有逻辑错误,使得中点 m 在下一次迭代中不会更新。如果我们在最后的 elif 分支中将 s = m+1 替换为 s = m 并在数组中不存在的特定目标上使用该函数,断言语句将会警告我们这个错误。这就是为什么这种技术可以帮助我们编写更好的代码。

保护机制与进攻性编程

看到 Python 内置了一个 NotImplementedError 异常真是令人惊讶。这对于我们所说的进攻性编程非常有用。

虽然输入清理旨在将输入对齐到我们的代码期望的格式,有时候清理所有内容并不容易,或者对我们未来的开发不方便。以下是一个例子,其中我们定义了一个注册装饰器和一些函数:

import math

REGISTRY = {}

def register(name):
    def _decorator(fn):
        REGISTRY[name] = fn
        return fn
    return _decorator

@register("relu")
def rectified(x):
    return x if x > 0 else 0

@register("sigmoid")
def sigmoid(x):
    return 1/(1 + math.exp(-x))

def activate(x, funcname):
    if funcname not in REGISTRY:
        raise NotImplementedError(f"Function {funcname} is not implemented")
    else:
        func = REGISTRY[funcname]
        return func(x)

print(activate(1.23, "relu"))
print(activate(1.23, "sigmoid"))
print(activate(1.23, "tanh"))

我们在函数 activate() 中引发了 NotImplementedError 并附带了自定义错误消息。运行这段代码将会打印前两个调用的结果,但在第三个调用时失败,因为我们还没有定义 tanh 函数:

1.23
0.7738185742694538
Traceback (most recent call last):
  File "/Users/MLM/offensive.py", line 28, in <module>
    print(activate(1.23, "tanh"))
  File "/Users/MLM/offensive.py", line 21, in activate
    raise NotImplementedError(f"Function {funcname} is not implemented")
NotImplementedError: Function tanh is not implemented

正如你所想象的,我们可以在条件不是完全无效的地方引发 NotImplementedError,只是因为我们还没有准备好处理这些情况。当我们逐步开发程序时,这一点特别有用,我们可以一次实现一个案例,并在稍后处理一些边角情况。设置这些保护机制可以确保我们的半成品代码不会以不应该的方式被使用。这也是一种让代码更难被滥用的好做法,即不让变量在未被察觉的情况下超出我们的预期范围。

事实上,Python 中的异常处理系统非常成熟,我们应该使用它。当你从未预期输入为负值时,应引发一个带有适当消息的ValueError。类似地,当发生意外情况时,例如,你创建的临时文件在中途消失,引发一个RuntimeError。在这些情况下你的代码无论如何都无法正常工作,抛出适当的异常有助于未来的重用。从性能角度来看,你还会发现抛出异常比使用 if 语句检查要更快。这就是为什么在 Python 中,我们更倾向于使用“请宽恕而不是许可”(EAFP)而不是“跃前先看”(LBYL)。

这里的原则是你绝不应让异常静默地进行,因为你的算法将无法正确运行,有时还会产生危险的效果(例如,删除错误的文件或产生网络安全问题)。

想要开始使用 Python 进行机器学习?

现在就参加我的免费 7 天电子邮件速成课程(附带示例代码)。

点击注册,还可以获得课程的免费 PDF 电子书版本。

避免错误的良好实践

无法说我们写的代码没有错误。它就像我们测试过的一样好,但我们不知道自己不知道什么。总有潜在的方式会意外地破坏代码。然而,有一些实践可以促进良好的代码并减少错误。

首先是使用函数式编程范式。虽然我们知道 Python 有允许我们用函数式语法编写算法的构造,但函数式编程背后的原则是函数调用不产生副作用。我们从不改变任何东西,也不使用函数外部声明的变量。“无副作用”原则在避免大量错误方面非常强大,因为我们永远不会错误地改变任何东西。

当我们在 Python 中编程时,经常会发现数据结构无意中被修改。考虑以下情况:

def func(a=[]):
    a.append(1)
    return a

这个函数的作用很简单。然而,当我们在没有任何参数的情况下调用这个函数时,使用了默认值,并返回了[1]。当我们再次调用它时,使用了不同的默认值,返回了[1,1]。这是因为我们在函数声明时创建的列表[]作为参数a的默认值是一个初始化的对象。当我们向其中添加一个值时,这个对象会发生变化。下次调用函数时会看到这个变化后的对象。

除非我们明确想这样做(例如,原地排序算法),否则我们不应该将函数参数用作变量,而应将其作为只读使用。如果合适,我们应当对其进行复制。例如,

LOGS = []

def log(action):
    LOGS.append(action)

data = {"name": None}
for n in ["Alice", "Bob", "Charlie"]:
    data["name"] = n
    ...  # do something with `data`
    log(data)  # keep a record of what we did

这段代码原本是为了记录我们在列表LOGS中所做的操作,但它并没有实现。当我们处理名字“Alice”、“Bob”以及“Charlie”时,LOGS中的三条记录都会是“Charlie”,因为我们在其中保留了可变的字典对象。应将其修改如下:

import copy

def log(action):
    copied_action = copy.deepcopy(action)
    LOGS.append(copied_action)

然后我们将在日志中看到三个不同的名称。总的来说,如果我们函数的参数是一个可变对象,我们应该小心。

避免错误的另一种方法是不要重复造轮子。在 Python 中,我们有许多优秀的容器和优化过的操作。你不应该尝试自己创建一个栈数据结构,因为列表支持append()pop()。你的实现不会更快。同样,如果你需要一个队列,我们在标准库的collections模块中有deque。Python 没有平衡搜索树或链表,但字典是高度优化的,我们应该在可能的情况下使用字典。函数也是如此,我们有 JSON 库,不应自行编写。如果我们需要一些数值算法,可以检查一下 NumPy 是否有合适的实现。

避免错误的另一种方法是使用更好的逻辑。一个包含大量循环和分支的算法很难跟踪,甚至可能让我们自己感到困惑。如果我们能使代码更清晰,就更容易发现错误。例如,创建一个检查矩阵上三角部分是否包含负数的函数,可以这样做:

def neg_in_upper_tri(matrix):
    n_rows = len(matrix)
    n_cols = len(matrix[0])
    for i in range(n_rows):
        for j in range(n_cols):
            if i > j:
                continue  # we are not in upper triangular
            if matrix[i][j] < 0:
                return True
    return False

但我们也使用 Python 生成器将其拆分成两个函数:

def get_upper_tri(matrix):
    n_rows = len(matrix)
    n_cols = len(matrix[0])
    for i in range(n_rows):
        for j in range(n_cols):
            if i > j:
                continue  # we are not in upper triangular
            yield matrix[i][j]

def neg_in_upper_tri(matrix):
    for element in get_upper_tri(matrix):
        if element[i][j] < 0:
            return True
    return False

我们多写了几行代码,但保持了每个函数专注于一个主题。如果函数更复杂,将嵌套循环拆分成生成器可能有助于使代码更易于维护。

让我们考虑另一个例子:我们想编写一个函数来检查输入字符串是否看起来像一个有效的浮点数或整数。我们要求字符串是“0.12”,而不接受“.12”。我们需要整数像“12”,而不是“12.”。我们也不接受科学记数法,比如“1.2e-1”或千位分隔符,如“1,234.56”。为了简化,我们也不考虑符号,比如“+1.23”或“-1.23”。

我们可以编写一个函数,从第一个字符扫描到最后一个字符,并记住到目前为止看到的内容。然后检查我们看到的内容是否符合预期。代码如下:

def isfloat(floatstring):
    if not isinstance(floatstring, str):
        raise ValueError("Expects a string input")
    seen_integer = False
    seen_dot = False
    seen_decimal = False
    for char in floatstring:
        if char.isdigit():
            if not seen_integer:
                seen_integer = True
            elif seen_dot and not seen_decimal:
                seen_decimal = True
        elif char == ".":
            if not seen_integer:
                return False  # e.g., ".3456"
            elif not seen_dot:
                seen_dot = True
            else:
                return False  # e.g., "1..23"
        else:
            return False  # e.g. "foo"
    if not seen_integer:
        return False   # e.g., ""
    if seen_dot and not seen_decimal:
        return False  # e.g., "2."
    return True

print(isfloat("foo"))       # False
print(isfloat(".3456"))     # False
print(isfloat("1.23"))      # True
print(isfloat("1..23"))     # False
print(isfloat("2"))         # True
print(isfloat("2."))        # False
print(isfloat("2,345.67"))  # False

上面的函数isfloat()在 for 循环内部有许多嵌套分支,显得很杂乱。即使在 for 循环之后,逻辑也不完全清晰如何确定布尔值。实际上,我们可以用不同的方法来编写代码,以减少错误的可能性,比如使用状态机模型:

def isfloat(floatstring):
    if not isinstance(floatstring, str):
        raise ValueError("Expects a string input")
    # States: "start", "integer", "dot", "decimal"
    state = "start"
    for char in floatstring:
        if state == "start":
            if char.isdigit():
                state = "integer"
            else:
                return False  # bad transition, can't continue
        elif state == "integer":
            if char.isdigit():
                pass  # stay in the same state
            elif char == ".":
                state = "dot"
            else:
                return False  # bad transition, can't continue
        elif state == "dot":
            if char.isdigit():
                state = "decimal"
            else:
                return False  # bad transition, can't continue
        elif state == "decimal":
            if not char.isdigit():
                return False  # bad transition, can't continue
    if state in ["integer", "decimal"]:
        return True
    else:
        return False

print(isfloat("foo"))       # False
print(isfloat(".3456"))     # False
print(isfloat("1.23"))      # True
print(isfloat("1..23"))     # False
print(isfloat("2"))         # True
print(isfloat("2."))        # False
print(isfloat("2,345.67"))  # False

从视觉上,我们将下面的图示转化为代码。我们维护一个状态变量,直到扫描完输入字符串。状态将决定接受输入中的一个字符并移动到另一个状态,还是拒绝字符并终止。该函数只有在停留在可接受状态,即“整数”或“小数”,时才返回 True。这段代码更容易理解且结构更清晰。

实际上,更好的方法是使用正则表达式来匹配输入字符串,即,

import re

def isfloat(floatstring):
    if not isinstance(floatstring, str):
        raise ValueError("Expects a string input")
    m = re.match(r"\d+(\.\d+)?$", floatstring)
    return m is not None

print(isfloat("foo"))       # False
print(isfloat(".3456"))     # False
print(isfloat("1.23"))      # True
print(isfloat("1..23"))     # False
print(isfloat("2"))         # True
print(isfloat("2."))        # False
print(isfloat("2,345.67"))  # False

然而,正则表达式匹配器在背后也在运行一个状态机。

这个主题还有很多值得探索的内容。例如,我们如何更好地分离函数和对象的职责,以使代码更易于维护和理解。有时,使用不同的数据结构可以让我们编写更简单的代码,从而使代码更强健。这不是一种科学,但几乎总是,如果代码更简单,就能避免错误。

最后,考虑为你的项目采用一种编码风格。保持一致的编码方式是你将来阅读自己编写代码时减少心理负担的第一步。这也使你更容易发现错误。

进一步阅读

本节提供了更多关于该主题的资源,如果你想深入了解。

文章
书籍

总结

在本教程中,你已经看到了提升代码质量的高级技巧。这些技巧可以让代码为不同的情况做好更好的准备,使其更为稳健。它们还可以使代码更易于阅读、维护和扩展,从而适合未来的重用。这里提到的一些技巧在其他编程语言中也很通用。

具体来说,你学到了:

  • 为什么我们希望清理输入,以及这如何帮助简化程序

  • assert 作为开发工具的正确使用方法

  • 如何适当地使用 Python 异常来在意外情况下发出信号

  • 处理可变对象时 Python 编程中的陷阱