Python 类型:可选的可以是强制的
原文:
towardsdatascience.com/python-types-optional-can-mean-mandatory-8e3b7ac2e805
PYTHON 编程
了解如何避免对 typing.Optional
的常见误用和误解。
·发表于Towards Data Science ·8 分钟阅读·2023 年 11 月 21 日
–
照片由Caroline Hall拍摄,发布在Unsplash
根据Python 文档,typing.Optional
是一种方便的方式来表示一个对象可以是 None
。这是一种简洁而优雅的方式来表达这个概念,但它是否也非常清晰?
让我换一种说法:当你在 Python 环境中看到“optional”这个词时,你认为它意味着什么?假设你看到一个名为 x
的参数,它的类型是 Optional[int]
。int
部分相当明确,因为它很可能表示一个整数,但 Optional
代表什么呢?你的第一反应是什么?
我们来考虑以下两个选项:
-
我不需要提供
x
的值,因为它是可选的。 -
x
的值可以是int
或None
。
如果你对 Python 类型提示足够了解,你会知道选项 2 是正确的。但当你不了解时……也许我错了,但我无法想象任何一个不懂 Python 的人会选择选项 2。选项 1 似乎更有意义。当我看到信息说某物是可选的,我会认为……嗯,就是说它是可选的……
这个问题导致了 typing.Optional
类型的常见误用。本文旨在揭示这种误用,并引导你正确理解这个类型。
typing.Optional
的含义
这三种类型提示是等效的:
from typing import Optional, Union
x: Union[str, None]
x: Optional[str]
x: str | None
它们都传达了相同的信息:x
可以是字符串或 None
。虽然完全有效,但第一个 (Union[str, None]
) 代表了 Python 类型提示的早期阶段:这是最初的方法,但现在不一定是首选方法。随后,Optional
被添加到 typing
模块中,提供了一种更简洁和直接的方式来表达这一概念。根据 the [mypy](https://mypy.readthedocs.io/en/stable/kinds_of_types.html?highlight=union#optional-types-and-the-none-type)
documentation:
你可以使用
[Optional](https://docs.python.org/3/library/typing.html#typing.Optional)
类型修饰符来定义允许None
的类型变体,例如Optional[int]
(Optional[X]
是Union[X, None]
的首选简写)。
最终,在 Python 3.10 中,引入了 |
运算符用于类型提示。正如 mypy 文档 所述,
PEP 604 引入了一种拼写联合类型的替代方式。在 Python 3.10 及更高版本中,你可以将
Union[int, str]
写作int | str
。
如你所见,这是一种用于联合类型的一般运算符,并非专门设计用于表示变量可以为 None
。
尽管这三种版本都是有效的,但选择应取决于几个因素。首先,如果你使用的 Python 版本低于 3.10,则 |
运算符不可用。即使使用 __future__
导入:
from __future__ import annotations
在某些情况下,它仍然可能会失败。你可以在 the [mypy](https://mypy.readthedocs.io/en/stable/runtime_troubles.html#using-x-y-syntax-for-unions)
documentation 中阅读相关内容。
尽管如此,我建议不要使用 Union
类型来表示可能为 None
,因为这不必要地冗长,而且正如上面引用的文档所述,这已经不是首选选项了。Mypy
推荐使用 typing.Optional
(quote:“Optional[X]
是 Union[X, None]
的首选简写”)。我同样支持这一点,原因很简单:Optional
类型正是为这种用例创建的,并且它还适用于旧版本的 Python,区别于 |
运算符。
以下是几个正确的类型提示示例,这些示例使用了 Optional
:
在 Python 3.12 中使用 typing.Optional 的类型提示示例。来自 Visual Studio Code 的截图,作者提供
如你所见,Optional
可以用于简单和复杂的类型提示。我们只分析其中间的一个。dict[str, Optional[int]]
类型表示一个变量应该是一个字典,键是字符串,值可以是整数或None
。
但是,这篇文章并不是关于做出这种选择的。我想讨论typing.Optional
类型的一个常见误用——并展示如何避免它。在此过程中,我将解释我认为这种误用的来源、如何纠正它,以及如何理解typing.Optional
类型。
对Optional
的误解
请考虑以下函数签名:
from typing import Optional
def foo(s: str, n: Optional[int] = 1) -> list[str]:
...
让我们分析一下这个函数签名中的类型提示。但是,不要过于依赖这个分析!因为这些类型提示可能(虽然不一定)是错误的。这里是:
-
s
是一个字符串(str
)参数;它可以是位置参数或关键字参数,并且是必需的; -
n
是一个可选的整数(int
)参数,可以是位置参数或关键字参数,默认值是1
; -
foo()
函数返回一个字符串列表(list[str]
)。
现在有一个问题:上述分析有什么问题?
第二个要点是错误的。它说n
是一个可选的整数。从某种程度上说,这是一句完全有效的英语句子。n
参数确实是可选的,因为你不必提供它的值;当你省略它时,将使用默认值1
。
另一方面,这不一定是一个有效的typing
句子。我的意思是,这种说法对typing.Optional
的理解是不正确的。上面,我们用Optional[int]
来表示以下含义:你不必提供n
的值。这意义是不正确的。typing
语法中Optional[int]
的正确含义是:n
可以是int
或None
。
下图总结了这两种理解:
正确和不正确理解typing.Optional
。图像由作者提供
让我们改进函数签名。我们有四个选项可以选择,每个选项代表不同的情况。选择适合你特定场景的选项。
如你所见,将有选项 0,其中签名保持不变。是的,这种类型提示可以是正确的——但它的含义很少是你需要的。
选项 0:保持原样
from typing import Optional
def foo(s: str, n: Optional[int] = 1) -> list[str]:
...
类型提示n: Optional[int] = 1
是完全正确的。重点是,它的含义与许多人认为的不同,因为它表示
-
n
可以是int
或None
,并且 -
n
的默认值是 1。
所以,默认值是1
,但用户仍然可以提供None
。
虽然技术上是正确的,但我只会在非常特定且罕见的情况下使用这种类型提示,因为这些情况非常少见,我从未遇到过需要这种类型提示的情况。这对我来说听起来不自然。
我对这个选项非常苛刻,因为在我看来,正是这种类型提示使得typing.Optional
被频繁误用:它暗示n
是可选的,因为它有一个默认值,因此你根本不必为这个变量或参数提供值。
我包括这个选项是因为它在技术上是正确的——但实际上你几乎不应该考虑它。至少,记住许多经验较少的 Python 用户很可能会误解这种类型提示。
选项 1:使用 Optional
并且 None
作为默认值
from typing import Optional
def foo(s: str, n: Optional[int] = None) -> list[str]:
if n is None:
...
...
当你需要None
作为整数或其他任何类型的默认值时,这是一种最常见的情况。这里使用了默认值(None
),作为触发特定操作的某种情感标志。因此,如果用户提供了一个整数,则会进行一些基于整数的处理。但当n
是None
时,这种处理可以完全关闭。当然,这只是一个示例场景,但它非常常见。
注意if
块,它旨在进行显式的None
检查。也许在所有这种情况下都不需要,但根据the [mypy](https://mypy.readthedocs.io/en/stable/kinds_of_types.html?highlight=union#optional-types-and-the-none-type)
documentation:
对于未加保护的
None
或[Optional](https://docs.python.org/3/library/typing.html#typing.Optional)
值,大多数操作是不允许的[…] 相反,需要进行显式的None
检查。Mypy 具有强大的类型推断功能,可以使用常规 Python 习惯来防范None
值。
选项 2:不要使用 Optional
def foo(s: str, n: int = 1) -> list[str]:
...
在这个选项中,你真正需要的是n
的默认值,但n
不能是None
。在这里,你完全不需要提供n
的值,因为有了默认值,你在调用foo()
时不必提供它的值。因此,这就是英语中optional的正确含义(参数是可选的,因为你不必提供它的值),但在typing
语法中是不正确的(参数不是Optional
,因为n
不能是None
)。
选项 3. 使用 Optional
但要求其值
这个用例展示了为什么使用typing.Optional
并不会使参数optional。如上所述,typing.Optional
类型意味着一个变量可以是None
,但这并不意味着当它用于参数时,你不必提供它的值。因此,这段代码是完全有效的:
def foo(s: str, n: Optional[int]) -> list[str]:
if n is None:
...
...
尽管n
是Optional
,但它不是可选的——你必须提供它的值。因此,你必须提供n
,但它仍然可以是None
。在这里,可选意味着n
是可选的int
,因为它也可以是None
。
如同选项 1 一样,你通常应该对n
使用显式的None
检查,因为我所写的关于使用None
的内容在这里也适用。
对于使用 typing.Optional 处理必需参数的肯定。图片作者
结论
我们讨论了一个与typing.Optional
类型相关的常见错误。这个错误源于这样一个事实:尽管名称暗示typing.Optional
处理可选参数,但它实际上指的是一个变量是否可以是None
——与是否必须在函数调用中提供参数值无关。
在我看来,“optional”一词并不能准确传达typing.Optional
的含义。然而,这是一个已经存在一段时间的公认 Python 术语,因此我不预期会有任何变化——无论如何。因此,意识到这种潜在的误解很重要。希望随着时间的推移,Python 代码库会减少对typing.Optional
的误用和误解。
感谢阅读。如果你喜欢这篇文章,你可能也会喜欢我写的其他文章;你可以在这里看到它们。如果你想加入 Medium,请使用下面的推荐链接:
## 通过我的推荐链接加入 Medium - Marcin Kozak
作为 Medium 会员,你的会员费用的一部分将会给你阅读的作者,而你可以全面访问每一个故事……
Python 水质 EDA 和可饮性分析
理解数据分析和可视化技术
·
关注 发表在 Towards Data Science · 11 分钟阅读 · 2023 年 7 月 8 日
–
图片来源:Amritanshu Sikdar 在 Unsplash
能够提供足够的饮用水是一个核心要求。在气候变化辩论中,最大的挑战之一是确保足够的淡水供生存使用。水质是一个影响所有生物的重要问题。地球上只有约三百分之一的水是淡水。而其中只有 1.2%可以作为饮用水,其余的被锁在冰川、冰盖和永久冻土中,或深埋在地下。使用数据驱动的方法来评估影响水质的特性,可以大大改善我们对使水可饮用的理解。
在最基本的层面上,水的可饮用性与水的安全性相关。数据技术可以用来审查这个目标特性。还有一些问题超出了我们目前的审查范围:
我们可以饮用所有类型的淡水吗?
世界上可以获取的淡水比例是多少?
随着海平面上升,地下水位是否也增加了?
在本文中,我们将与一个小型水质数据集一起进行探讨。我们将通过使用 pandas 和 numpy 的数据分析技术,从数据中寻找隐藏的见解。对于数据可视化,将使用 matplotlib 和 seaborn 库。将采用一系列探索性数据分析(EDA)技术,以进一步明确数据质量。
每个数据可视化都旨在突出数据的不同特征。它们还将为用户提供模板,以应用于其他挑战。
数据集
对于这项分析,水质数据集取自 Kaggle¹。
饮用水的可饮用性
使用了带有 Python 代码的 jupyter notebook 实例进行处理。
import sys
print(sys.version) # displays the version of python installed
运行上述脚本后,输出将显示使用了版本 3.7.10 的 Python。为了能够复制接下来的结果,用户应确保在工作环境中使用 Python 3。
理解数据
首先,我们需要了解我们正在使用的数据。由于文件格式为 csv 文件,将使用标准的 pandas 导入语句 read_csv。
# Import the dataset for review as a DataFrame
df = pd.read_csv("../input/water-potability/water_potability.csv")
# Review the first five observations
df.head()
导入数据后,代码将变量 df 分配为 pandas 方法生成的 DataFrame 输出结果。
与任何数据集一样,查看样本记录将帮助你获得信心。DataFrame 具有大量与之相关的方法,pandas API 是一个很好的资源。在 API 中,可以使用 head 方法。输出 1.1 默认显示 DataFrame 的前 5 行。为了显示更多的行,需要在括号内输入一个数值。可以应用两种替代方法来对 DataFrame 进行采样:i) sample (df.sample()) 从索引中选择随机行,或者 ii) tail (df.tail()) 从索引中选择最后的 n 行。
输出 1.1 DataFrame 的前五条记录详细信息
运行任何方法时,括号会在方法名后面出现,以便 Python 解释器产生结果。
显示 DataFrame 的内存可能是一个常见的任务,特别是在涉及内存限制时。例如,当要导入的数据集可能大于 Python 会话中可用的内存时。通过使用 pandas 库,DataFrame 会在内存中创建,因此用户应该了解在执行这些处理步骤时可以使用的内存。
# Display information about the DataFrame - contains memory details
df.info(memory_usage="deep")
上面的代码可以用作显示输出 1.2 的方法。通过包含关键字 memory_usage
,Python 解释器会进行更深入的搜索,以了解下面显示的内存使用情况。默认选项会执行一般搜索,因此如果需要评估的准确性,请确保应用上述关键字短语。
输出 1.2 提供了内存使用情况的功能和详细信息概述
从输出 1.2 显示的结果中,可以查看一系列详细信息,从列名称和数据类型,到确认变量的类别和非空值的数量。我们可以看到整个表格中显示了 3,276 行。然而,对于 Sulfate 列,只有 2,495 个非空值。因此,可以查看一些缺失值,以了解这些缺失条目是否与其他列存在模式。我们将在文章后面回顾一种数据可视化技术,可以帮助识别模式。
根据之前的导入语句,用户可以调整列的 Dtype,如果默认选项不符合预期的话。上述结果显示,对于十进制数字,应用了 float Dtype,而整数显示为 int。此外,还包括了这些数字列的最大字节内存类型,以提供潜在输入值的全面覆盖。用户应当评估这些 Dtypes 是否保持了正确的值范围,如果未来预期的范围较小,则可以分配较小的字节值。应用这一逻辑将有助于提高 DataFrame 的内存效率,并在处理时提升性能。
上述 info 方法展示的一个特性是 DataFrame 的结构,这可以通过许多其他方法进行查看。这样的元数据可以让程序员查看行数和列数等基本组件。
# Shape of the DataFrame - shows tuple of (#Rows, #Columns)
print(df.shape)
# Find the number of rows within a DataFrame
print(len(df))
# Extracting information from the shape tuple
print(f'Number of rows: {df.shape[0]} \nNumber of columns: {df.shape[1]}')
在 Python 中调用诸如 shape 这样的属性时,不需要加上括号。属性是可以通过类及其对象访问的数据结果。之前我们回顾了一个方法,它是包含在类中的一个函数。要进一步了解 Python 类语句的细节,需要深入研究。然而,我们可以继续使用所展示的代码,并且展示了输出 1.3 中显示的一些值。
输出 1.3 显示了 DataFrame 的结构的元数据
第一行显示了形状输出,这是一个元组,由两个值组成的括号表示。从上面展示的代码中,我们能够访问该元组中的相对位置,以显示第一个和第二个位置的值。由于 Python 使用 0 索引约定,应用 0 在方括号内将返回第一个值。我们可以看到,元组中包含了第一位置的行数,第二位置的列数。找到行数的另一种方法是使用 len 函数,它显示 DataFrame 的长度。
摘要统计
在这一部分,我们开始回顾 DataFrame 列的摘要细节。一个简单的 describe 方法可以用于对数字列进行高层次的数据分析。由于我们的 DataFrame 仅包含数字列,因此所有摘要属性都被生成。当存在字符和数字列的混合时,需要包含其他关键字参数以显示相关输出。
# Review the high level summary details for each variable
df.describe()
输出 1.4 显示每列的默认总结值。计数值可以解释为非空值的计数。任何总计小于 DataFrame 中行总数的显示列都有缺失值。对于每个变量,我们可以看到一系列值。我们可以使用四种矩方法来理解 i) 平均值,ii) 方差,iii) 偏度,和 iv) 峰度,基于显示的数据。
输出 1.4 标准细节显示 DataFrame 中每个特征的高级指标
在回顾总结细节时,具有外部视角来理解特征属性也是至关重要的。我们从经验中知道,pH 值应在 0 到 14 之间。如果值超出此范围,则必须检查和更正特征值。用于评估水质的数据中,平均值和中位数(由第 50 百分位数显示)接近 7 是水的中性属性的适当值。
如果 DataFrame 中有更多特征,则前一个代码块的输出可能会很难解释。输出可能会横向扩展到比没有滚动显示的范围更宽的范围。
# Transpose the summary details - easier to review larger number of features
df.desribe().T
能够转置输出是一种有用的方法。在上面的代码块中,链式调用 T 方法生成了下面的输出 1.5。现在用户可以更容易地查看行索引上显示的列名以及以列标题显示的总结指标。这一小的调整使得 describe 方法在列数较多时效果很好。
输出 1.5 总结细节转置以显示沿行索引的特征名称
要进一步了解 describe 方法的详细信息,我们可以通过使用 jupyter notebook 的问号魔法函数来解释文档字符串。
# Magic function in jupyter to display docstring
df.describe?
使用这种方法将帮助用户查看任何方法的默认参数值(关键字和位置)。
输出 1.6 在 jupyter 控制台中显示关于文档字符串和方法参数的详细信息
输出 1.6 提供了方法的内部工作原理,供用户查看。每个参数的默认值范围以及定义,有助于方法的应用。提供了一系列 jupyter 魔法函数,可以帮助提高程序员的生产力。
缺失值
如前所述,从元数据和总结统计数据中可以看到 DataFrame 中存在一些缺失值。为了确认这一点,我们可以应用下面的代码块。
# Check for the missing values by column
df.isnull().sum()
代码将第一个 isnull 方法与 sum 方法链在一起,以创建每列的缺失值数量。isnull 评估将检查列中的非空值。sum 方法用于执行计数。输出 1.7 突出显示三列显示缺失值。
输出 1.7 每列的缺失值计数
拥有缺失值总行数是一个很好的起点。然而,更好的是审查每列中缺失值的比例。
# Proportion of missing values by column
def isnull_prop(df):
total_rows = df.shape[0]
missing_val_dict = {}
for col in df.columns:
missing_val_dict[col] = [df[col].isnull().sum(), (df[col].isnull().sum() / total_rows)]
return missing_val_dict
# Apply the missing value method
null_dict = isnull_prop(df)
print(null_dict.items())
创建 isnull_prop 用户定义函数使我们能够为每列创建一个值字典。通过此函数,我们生成了上述计数值,并使用 shape 属性理解缺失值的百分比。
输出 1.8 将缺失值百分比函数应用于每列
输出 1.8 显示的结果难以可视化。为确保不遗漏最终信息,可以生成一个 DataFrame。
# Create a dataframe of the missing value information
df_missing = pd.DataFrame.from_dict(null_dict,
orient="index",
columns=['missing', 'miss_percent'])
df_missing
将字典变量应用于 pandas DataFrame 方法将使我们更容易理解每列的差异。输出 1.9 现在包括 miss_percent 列。我们现在可以应用阈值来评估缺失值的百分比是否在我们期望的范围内。如果值过高,例如硫酸盐值大于 20%,可以设置用户定义的控制,以突出显示此列需要排除在未来使用之外或更详细地审查。
输出 1.9 创建 DataFrame 以了解每列的缺失百分比
另一种通过缺失值检查是否存在模式的方法是应用来自 seaborn 数据可视化库的热图方法。
# Display missing values using a heatmap to understand any patterns
plt.figure(figsize=(15,8))
sns.heatmap(df.isnull());
应用上述代码块将生成输出 1.10。此可视化提供了进一步的上下文,以查看是否有多个行对所有三个变量都有缺失值。这可能是因为填充数据的用户在原始数据集中一致地显示缺失值。获得这一见解将使我们能够生成数据驱动的洞察,以更有效地减少缺失值数量。
输出 1.10 使用 seaborn 数据可视化生成每列每行的缺失值热图
理解 pH 变量分布
最终评估将是对我们已知的变量进行审查。使用 seaborn 库,我们能够生成 pH 变量的直方图。
# set the histogram, mean and median
sns.displot(df["ph"], kde=False)
plt.axvline(x=df.ph.mean(), linewidth=3, color='g', label="mean", alpha=0.5)
plt.axvline(x=df.ph.median(), linewidth=3, color='y', label="median", alpha=0.5)
# set title, legends and labels
plt.xlabel("ph")
plt.ylabel("Count")
plt.title("Distribution of ph", size=14)
plt.legend(["mean", "median"]);
print(f'Mean pH value {df.ph.mean()}
\n Median pH value {df.ph.median()}
\n Min pH value {df.ph.min()}
\n Max pH value {df.ph.max()}')
与之前的打印语句类似,f 字符串语句允许我们添加均值、中位数、最小值和最大值,以便更容易地查看分布。
输出 1.11 使用直方图查看 pH 变量分布
输出 1.11 显示大多数 pH 值接近中间。其分布类似于正态分布,我们可以利用这一见解在向外部用户展示细节时提供帮助。
结论
在本文中,我们旨在回顾 EDA 评估的早期阶段。最初审查了导入数据的元数据,以展示早期洞察。深入挖掘摘要统计数据让我们关注缺失值。最后,我们审查了 pH 变量的直方图,以确保该变量符合外部预期。
后续文章将继续这一旅程,并寻求开发旨在预测水质的模型。将使用分类机器学习技术提供基线模型。
留下你的评论,非常感谢你的阅读!
你可以通过 LinkedIn 与我联系,进行关于数据的一次友好聊天。其他我分享的故事包括:
确保在 SQL 代码的开头声明关键变量,可以帮助自动化代码的重用。
[towardsdatascience.com ## 高级 SQL 操作
审查更高级的 SQL 操作,以从爱尔兰天气数据集中提取更多的数据洞察。
[towardsdatascience.com ## 开发 SQL 表
只有通过创建和开发 SQL 表,我们才能理解如何最佳利用可用内存。
[towardsdatascience.com ## Python 中的 NLP 入门
开始进入自然语言处理领域
[towardsdatascience.com
[1] : Kaggle 数据集水质来自 www.kaggle.com/datasets/adityakadiwal/water-potability
,许可证协议见 creativecommons.org/publicdomain/zero/1.0/
Python 水印:旧 vs 新,笨重 vs 干净 — 你会选择哪个?
图片由Siegfried Frech提供,来源于Pixabay
Python 水印制作简化:OpenCV、PIL 和 filestools 的全面比较
·发表于数据科学前沿 ·8 分钟阅读·2023 年 3 月 28 日
–
对图像进行水印处理是摄影师、艺术家以及任何希望保护其视觉内容免受未经授权使用的人的重要任务。在 Python 世界中,有许多库可以让你为图像添加水印。在本文中,我们将比较三种流行的 Python 图像水印方法:OpenCV、PIL(Python Imaging Library)和filestools。对于最后一种方法,你只需一行代码!
在这篇文章中,我将演示使用我在澳大利亚维多利亚州菲利普岛拍摄的照片的水印功能。原始照片在这里。请随意下载以便使用。
作者拍摄的照片
1. OpenCV — 小任务的大工具
OpenCV 是一个综合性的计算机视觉库,提供了广泛的图像处理功能,包括向图像添加文本水印的能力。虽然 OpenCV 并非专门为添加水印设计,但它仍然提供了实现这一目标的灵活性和控制力。然而,使用 OpenCV 添加水印可能会有些挑战,尤其是对于那些不熟悉该库的人来说。此外,使用 OpenCV 实现基于图像的水印需要一些手动处理。
无论如何,让我们看看 OpenCV 如何为我们完成这项任务。
在一切之前,请确保如果你还没有安装库,需要安装它。只需使用 pip
如下。
pip install opencv-python
要在 Python 代码中使用 OpenCV,我们需要导入 cv2
模块。为了使这个示例更简单,我还想导入 matplotlib
,这样我就可以即时显示图像。
import cv2
import matplotlib.pyplot as plt
OpenCV 使从本地路径读取图像变得非常简单。你只需使用 imread
函数即可。
img = cv2.imread("my_photo.jpeg")
以下函数是可选的,我创建了这个函数以方便在 Jupyter Notebook 环境中内联显示图像。如果你想查看图像对象的样子,可以随意使用它。
def show_image(img, is_cv=False):
if is_cv:
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
plt.figure(figsize=(16, 9))
plt.imshow(img)
plt.axis("off")
plt.show()
在上述函数中,我添加了 is_cv
来指定这个图像对象是否来自 OpenCV。我们需要这样做,因为我们可能希望以后将这个函数用于 PIL 库。OpenCV 图像对象默认使用 BGR 而不是 RGB。因此,我们需要使用 cvtColor()
函数来转换编码方法。
之后,使用 matplotlib
来显示图像。在我的例子中,我指定了一个适合浏览器窗口的大小。此外,由于我们只是显示图像,可以关闭坐标轴。imshow()
是显示图像对象的关键函数。
因此,我们可以简单地通过调用我们刚刚创建的函数来显示图像。
show_image(img, is_cv=True)
现在,让我们创建一个字符串,这个字符串就是我们想要添加到图像上的水印文本。接下来,我们需要配置字体。有几种 OpenCV 内置的字体样式可以选择。font_scale
将决定水印文本的大小。最后,我们可以创建一个元组作为颜色。(255, 255, 255)
将使水印文本为白色。
watermark_text = "Christopher Tao @TDS"
# Set the font, font scale, and color of the text
font = cv2.FONT_HERSHEY_TRIPLEX
font_scale = 5
color = (255, 255, 255)
接下来是决定水印位置。getTextSize()
方法将帮助我们获取文本的大小。同时,我们可以从图像的 shape
属性中获取图像的维度。
# Get the size of the text
text_size, _ = cv2.getTextSize(watermark_text, font, font_scale, thickness=20)
# Calculate the position of the text
x = int((img.shape[1] - text_size[0]) / 2)
y = int((img.shape[0] + text_size[1]) / 2)
然而,需要强调的是,图像的大小是“H x W”,而文本的大小是“W x H”。因此,当我们计算坐标时,需要使用图像形状中的第二项(宽度)减去文本大小中的第一项(宽度),反之亦然。
最后,我们可以使用 putText()
方法将水印文本添加到图像中,使用我们定义的所有参数。
# Add the text watermark to the image
cv2.putText(img, watermark_text, (x, y), font, font_scale, color, thickness=2)
让我们看看结果。成功!
2. PIL — 简化水印处理
PIL(Python Imaging Library)是一个流行的第三方图像处理库,它提供了比 OpenCV 更简单直接的方式来给图像添加水印。然而,它仍然需要一些步骤来实现水印。PIL 是那些需要可靠且相对简单的方式来给图像添加水印的用户的不错选择,无需复杂的计算机视觉能力。
同样,在使用 PIL 库之前,我们需要按照如下方式安装它。
pip install pillow
对于 PIL 库,我们需要以下 3 个模块。
-
Image
模块:提供了一个用于表示和操作 PIL 中图像的类。 -
ImageDraw
模块:提供了一组用于在图像上绘制的函数,包括线条、矩形、圆形和文本。 -
ImageFont
模块:提供了一个用于加载和操作字体的类,包括设置字体大小、样式和颜色。
from PIL import Image, ImageDraw, ImageFont
然后,我们可以使用Image
模块打开图像,如下所示。我们也可以重用之前定义的show_image()
方法来显示原始图像。
img = Image.open('my_photo.jpeg')
show_image(img)
要操作图像,我们需要从图像对象创建一个ImageDraw
实例。
# Create an ImageDraw object
draw = ImageDraw.Draw(img)
下一步有点棘手。与 OpenCV 内置的字体样式不同,PIL 只能使用单独的“.ttf”文件。虽然所有操作系统都有一些字体样式,但我们仍需要了解现有的字体样式,以便可以使用它们。
在这种情况下,我建议最简单的方法是使用matplotlib
来显示可用的字体,如下所示,除非你有特定的字体样式需要使用。
import matplotlib
matplotlib.font_manager.findSystemFonts(fontpaths=None, fontext='ttf')
以下是我所使用的一些可用字体。
现在,我们可以开始设置参数。
# Prepare watermark text
font = ImageFont.truetype('Humor-Sans.ttf', size=150)
# Calculate the size of the watermark text
t_width, t_height = draw.textsize(watermark_text, font)
# Calculate the x and y coordinates for the text
x = int((img.size[0] - t_width) / 2)
y = int((img.size[1] - t_height) / 2)
我们可以使用ImageFont.truetype
创建一个特定大小的水印字体。之后,我们可以通过draw
对象使用textsize()
方法获取文本大小。之后,计算坐标的方式与我们在 OpenCV 演示中做的一样。
最后,我们可以使用draw
对象的text()
方法添加水印。
# Add the text as a watermark on the image
draw.text((x, y), watermark_text, font=font, fill=(255, 255, 255))
3. Filestools — 一行代码奇迹
filestools
是一个第三方 Python 库,提供了一系列有用的文件和图像处理工具。它包括显示目录结构的功能,如 Linux 中的 tree
命令,比较文件差异的功能,如 diff
命令,以及使用 marker
命令给图像添加水印。此外,filestools 还可以用于将 curl 请求转换为 Python 请求代码。尽管该库由一位中国开发者创建,但它仍然被广泛访问和使用,尽管一些日志是中文的。
同样,要使用这个库,我们可以按如下方式安装它。
pip install filestools
然后,我们将水印文字添加到图像中。我们可以按如下方式从库的 watermarker
模块中导入 add_mark()
函数。然后,这个函数将完成我们需要的一切。
from watermarker.marker import add_mark
add_mark(file="my_photo.jpeg",
out="watermarked",
mark=watermark_text,
size=60,
color="#ffffff",
opacity=0.5,
angle=30,
space=60)
out
参数是一个目录名称,因此带水印的图像将被输出到这个目录中。opacity
指定了水印的透明度。我们确实可以使用 OpenCV 和 PIL 实现这一点,但需要更多的步骤和复杂的逻辑。除此之外,水印还会被渲染为图像上的“图案”。因此,我们可以给文字指定一个 angle
,以及定义文本实例之间间距的 space
。
运行这个函数后,会显示“成功保存”。现在,我们可以检查我们的工作目录。我们应该能够找到一个包含带水印图像的新子目录。
这是我们打开后的带水印照片。
作者拍摄的照片
总结
图片由 Nikolett Emmert 提供,来自 Pixabay
在这篇文章中,我们比较了三种流行的 Python 库用于给图像加水印:OpenCV、PIL(Python Imaging Library)和 filestools。OpenCV 是一个综合的计算机视觉库,提供广泛的图像处理功能,而 PIL 提供了一种更简单直接的方法来给图像加水印。然而,这两个库都需要多个步骤和一些手动处理才能实现水印。另一方面,filestools 提供了一行代码的解决方案来添加水印,使其成为三者中最简单和最流线型的库。总体而言,虽然 OpenCV 和 PIL 提供了更高级的图像处理功能,但在水印添加的易用性方面,filestools 是明显的赢家。
[## 使用我的推荐链接加入 Medium - Christopher Tao
感谢你阅读我的文章!如果你不介意,请请我喝杯咖啡 😃 你的会员费用支持成千上万的…
如果你觉得我的文章有帮助,请考虑加入 Medium 会员来支持我和其他成千上万的作者!(点击上面的链接)
除非另有说明,所有图片均由作者提供
对 Python 3.12 的期待
即将发布的 Python 3.12 版本中的新特性、更新和移除项
·发表于Towards Data Science ·阅读时间 6 分钟·2023 年 1 月 4 日
–
摄影:由Yulia Matvienko提供,来源于Unsplash
尽管开发者和团队仍在将项目升级到具有革命性(在性能方面)的Python 3.11,但新版本的发布正在进行中。
Python 3.12 预计将在接下来的几个月内发布,但 alpha 版本(当前为3.12.0a3
)已发布,预发布用户现在可以访问它以测试新功能、报告错误并提供进一步建议——如果需要的话。
在这篇文章中,我们将深入探讨根据相关 PEP 预计将引入的一些更改,包括 Python 3.12 的更新和新增内容。
移除和弃用
从 Python 3.10 开始,distutils
被标记为弃用——根据PEP 632——Python 3.12 将彻底移除该模块。请注意,不提供向后兼容性,这意味着任何来自distutils
的导入都将导致错误。
多年来,作为标准库的一部分的distutils
是 Python 中首选的包管理模块,但由于setuptools
的出现改变了这一格局,即使是Python 包装用户指南也会推荐setuptools
,因为它旨在克服一些distutils
的限制。
尽管 setuptools
仍然使用 distutils
的功能,但前者现在已经整合了后者的副本,这意味着它不再依赖于标准库,而 pip
长期以来一直在用 setuptools
替代 distutils
。因此,将遗留的 distutils
从 Python 3.12 版本中移除是有意义的。
此外,在 Python 3.12 中,wstr
和 wstr_length
成员将从 Unicode 中移除,如 PEP-623 所示。这一变更与 PEP-393 中某些 Unicode API 的弃用一致。这些成员的移除使得 64 位平台上的对象大小减少了 8 或 16 字节。
改进的错误消息
Python 3.12 还将对某些错误报告的消息进行改进。
每当在顶层模块中引发 NameError
时,Python 3.12 解释器将会在错误信息中报告建议:
>>> sys.version
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'sys' is not defined. Did you forget to import 'sys'?
同样,如果在方法中发生 NameError
并且实例具有与异常同名的属性,纠正错误的建议将是 self.<NAME>
,而不是方法范围内的最接近匹配。例如,
class Foo:
def __init__(self):
self.my_var = 'Hello'
def bar(self):
another_var = my_var
>>> Foo().bar()
File "<stdin>", line 1
another_var = my_var
^^^^^^
NameError: name 'my_var' is not defined. Did you mean: 'self.my_var'?
此外,每当因无效的导入语法(遵循模式 import x from y
而不是有效的语法 from y import x
)引发 SyntaxError
时,错误信息中将会报告一个提示,告知用户这个潜在的问题,如下例所示。
>>> import mymodule.a from anothermodule.b
File "<stdin>", line 1
import mymodule.a from anothermodule.b
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
SyntaxError: Did you mean to use 'from ... import ...' instead?
最后,错误信息的另一个改进与 ImportError
相关,以及当 from <module> import <name>
因名称未解析而失败时引发的错误。在 Python 3.12 中,这些错误信息将包括有关未解析名称 <name>
的建议,基于模块 <module>
中实际包含的名称。
>>> from collections import ordereddict
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ImportError: cannot import name 'ordereddict' from 'collections'. Did you mean: 'OrderedDict'?
现在,Linux perf profile 可用于 Python 函数
Linux perf profiler 是一个有用的工具,可以帮助用户分析应用程序并获取与其性能相关的信息。在较早的 Python 版本中,profile 能够报告原生函数和过程的信息,这些函数和过程用 C 编写。从 Python 3.12 开始,解释器能够以一种特殊模式运行,使得 perf
profiler 也可以报告 Python 函数的信息。
perf
性能分析支持可以通过环境变量[PYTHONPERFSUPPORT](https://docs.python.org/3.12/using/cmdline.html#envvar-PYTHONPERFSUPPORT)
或[-X perf](https://docs.python.org/3.12/using/cmdline.html#cmdoption-X)
选项启用,也可以通过动态方式使用[sys.activate_stack_trampoline()](https://docs.python.org/3.12/library/sys.html#sys.activate_stack_trampoline)
和[sys.deactivate_stack_trampoline()](https://docs.python.org/3.12/library/sys.html#sys.deactivate_stack_trampoline)
启用。
sqlite3 命令行接口
Python 3.12 还将引入一个 sqlite3
的命令行接口。这意味着 sqlite3
模块也可以通过使用语言解释器的 -m
标志作为脚本调用,从而启动 SQLite Shell。
python -m sqlite3 [-h] [-v] [filename] [sql]
此外,sqlite3 默认适配器和转换器 现在已被弃用。
移除已弃用的 unittest 特性
作为 Python 3.12 发布的一部分,一些在早期版本(主要是 v3.1 和 3.2)中已被弃用的 unittest
模块特性将被移除。移除的特性包括:
-
许多
TestCase
方法别名,包括failUnless
、failIf
、failUnlessEqual
、failIfEqual
、failUnlessAlmostEqual
、failIfAlmostEqual
、failUnlessRaises
、assert_
、assertEquals
、assertNotEquals
、assertAlmostEquals
、assertNotAlmostEquals
、assertRegexpMatches
、assertRaisesRegexp
和assertNotRegexpMatches
-
TestCase 方法
assertDictContainsSubset
-
TestLoader.loadTestsFromModule
参数*use_load_tests*
-
TextTestResult
的别名_TextTestResult
其他语言添加和改进
尽管不可能分享 Python 3.12 中每一个修改的详细信息,这里有一些我个人觉得相当有趣的额外改进和变化的高层概述:
-
在标准库的
os
模块中引入了[os.path.isjunction()](https://docs.python.org/3.12/library/os.path.html#os.path.isjunction)
成员,允许用户检查给定路径是否为连接点 -
引入了一个新的
[pathlib.Path.walk()](https://docs.python.org/3.12/library/pathlib.html#pathlib.Path.walk)
方法,允许遍历目录树,类似于os.walk()
要全面了解作为版本 3.12 正式发布的一部分计划引入的所有更改,可以参考相关文档,在 官方文档中。
结束语
Python 3.12 alpha 版本最近已向预发布用户公开。在本文中,我们讨论了一些最有趣的新特性和更新,基于目前分享的发布细节。有关即将发布的完整详细信息,可以参考 官方变更日志。本文讨论的一些变化包括:
-
移除
distutils
模块 -
从 Unicode 中移除了
wstr
和wstr_length
-
改进了
NameError
、SyntaxError
和ImportError
的错误消息 -
增加对 Linux perf profiler 的支持
-
添加了 sqlite3 命令行界面,并弃用了默认适配器和转换器。
-
移除了一些之前被弃用的
unittest
功能。
如 官方 Python 文档 所述,讨论的新功能和更新目前处于草稿阶段,预计在实际发布时会有更多更新。
预发布用户应注意,本文件目前处于草稿阶段。随着 Python 3.12 接近发布,它将会有 substantial 更新,因此即使阅读了早期版本,也值得回访查看最新内容。
成为会员 并阅读 Medium 上的每一个故事。您的会员费直接支持我和您阅读的其他作家。您还将获得对 Medium 上每一个故事的完全访问权限。
使用我的推荐链接加入 Medium — Giorgos Myrianthous
作为 Medium 会员,您的会员费的一部分将用于支持您阅读的作家,并且您可以完全访问每一个故事……
您可能还喜欢的相关文章
Python 中的代码图示 [## Python 中的代码图示
使用 Python 创建云系统架构图。
如何编写 Python 中的 Switch 语句 [## 如何编写 Python 中的 Switch 语句
理解如何使用模式匹配或字典在 Python 中编写 Switch 语句。
如何使用 Docker 本地运行 Airflow [## 如何使用 Docker 本地运行 Airflow
在本地机器上使用 Docker 运行 Airflow 的逐步指南。
PyTorch 初学者图像分类教程
原文:
towardsdatascience.com/pytorch-image-classification-tutorial-for-beginners-94ea13f56f2
在 Python 中微调预训练的深度学习模型
·发布于 Towards Data Science ·22 min 阅读·2023 年 5 月 9 日
–
“不确定这应该是狮子还是猎豹……”
这个实用教程将展示如何使用 PyTorch 框架中的预训练深度学习模型进行图像分类。
这个面向初学者的图像分类教程与其他教程的不同之处在于,我们并不会从头开始构建和训练深度神经网络。在实际操作中,只有少数人会从头训练神经网络。相反,大多数深度学习从业者会使用预训练模型,并将其微调以适应新任务。
在实际操作中,只有少数人会从头训练神经网络。
特定的问题设置是构建一个二分类图像分类模型,以根据小型数据集对猎豹和狮子的图像进行分类。为此,我们将使用 PyTorch 微调一个预训练的图像分类模型。
数据集的示例图像 [1]。
本教程遵循基本的机器学习工作流程:
-
准备和探索数据
-
建立基准
-
运行实验
-
做出预测
你可以在 我的相关 Kaggle 笔记本中跟随教程。
前提条件和设置
理想情况下,你应该对 Python 有一定的了解。
由于这是一个实用教程,我们只会在高层次上覆盖如何构建图像分类模型。我们不会涵盖很多理论,比如卷积层或反向传播的工作原理。一旦你对这个话题感到舒适,我会用这个标志 ⚒️ 标记你可以深入了解的部分。
如果你想为本指南补充一些理论背景信息,我推荐免费的 Kaggle Learn 课程,内容包括深度学习和计算机视觉。
让我们开始导入 PyTorch 和其他相关库:
import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader
import cv2
import albumentations as A
from albumentations.pytorch import ToTensorV2
import numpy as np # data processing
import matplotlib.pyplot as plt # Data visualization
from tqdm import tqdm # Progress bar
关键的库包括用于深度学习的 PyTorch(版本 1.13.0)、用于图像处理的 OpenCV(版本 4.5.4)以及用于数据增强的 Albumentations(版本 1.3.0)。
第一步:准备和探索数据
第一步是熟悉数据。对于本教程,我们将简短地进行探索性数据分析。
首先,我们将加载数据。示例数据集[1]包含两个文件夹,每个文件夹中都有图像——每个类别一个文件夹。
二分类图像分类的示例数据集[1]。
以下代码遍历所有子文件夹,并创建一个包含文件名及其标签的 Pandas 数据框。
import os
import pandas as pd
root_dir = ... # Insert your data here
sub_folders = ["Cheetahs", "Lions"] # Insert your classes here
labels = [0, 1]
data = []
for s, l in zip(sub_folders, labels):
for r, d, f in os.walk(root_dir + s):
for file in f:
if ".jpg" in file:
data.append((os.path.join(s,file), l))
df = pd.DataFrame(data, columns=['file_name','label'])
在此处插入你的数据! — 为了跟上本文的内容,你的数据集应该类似于此:
二分类图像分类的示例数据集[1]。在此处插入你的数据。
我们大约有 170 张照片:大致 85 张狮子照片和 85 张猎豹照片(见[1]中的备注)。这是一个非常小但平衡的数据集,非常适合微调!
import seaborn as sns
sns.countplot(data = df, x = 'label');
使用 seaborn 绘制的图像分类样本数据集的类别分布
为了对数据集有一个感觉,绘制一些样本总是个好主意:
fig, ax = plt.subplots(2, 3, figsize=(10, 6))
idx = 0
for i in range(2):
for j in range(3):
label = df.label[idx]
file_path = os.path.join(root_dir, df.file_name[idx])
# Read an image with OpenCV
image = cv2.imread(file_path)
# Convert the image to RGB color space.
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
# Resize image
image = cv2.resize(image, (256, 256))
ax[i,j].imshow(image)
ax[i,j].set_title(f"Label: {label} ({'Lion' if label == 1 else 'Cheetah'})")
ax[i,j].axis('off')
idx = idx+1
plt.tight_layout()
plt.show()
数据集[1]中的样本图像。
通过探索这样的数据集,你可以获得一些见解。例如,正如你在这里看到的,图像不仅限于动物,还包括雕像。
在我们进一步操作之前,让我们将数据集拆分为训练数据和测试数据。训练数据将用于构建我们的模型,而测试数据将作为保留数据集来评估最终模型在未见数据上的性能。在本示例中,我们将把 10%的数据留作测试用。
from sklearn.model_selection import train_test_split
train_df, test_df = train_test_split(df,
test_size = 0.1,
random_state = 42)
将数据拆分为训练集和测试集(灵感来源于scikit-learn)
第二步:建立基线
接下来,我们将构建一个基线。基线包括三个关键组件:
-
一个加载图像的数据管道
-
一个模型,包含损失函数和优化器
-
一个训练管道,包括一个交叉验证策略
在本节中,我们将逐个组件进行讲解,并最终整理好。
由于训练深度学习模型涉及大量实验,我们希望能够快速切换代码的特定部分。因此,我们将尽可能使以下代码模块化,并使用配置进行调优:
from types import SimpleNamespace
cfg = SimpleNamespace(**{})
我们将随着进展添加可配置参数。
构建用于加载图像的数据管道
首先,你必须构建一个管道,以批次方式加载、预处理和喂入图像到神经网络中(而不是一次性加载)。PyTorch 提供了两个核心类供你使用:
-
Dataset
类:加载和预处理数据集。你需要根据你的需求自定义这个类。 -
Dataloader
类:将数据样本批次加载到神经网络中。
首先,你需要自定义 Dataset
类。其关键组件是:
-
构造函数:用于加载数据集,例如 Pandas Dataframe
-
__len__()
:获取数据集的长度。这通常只需要对数据集传递方式进行最小的调整。 -
__getitem__()
:通过索引从数据集中获取样本。这通常是你根据需要执行预处理时修改代码的地方。
下面你可以找到一个用于自定义 Dataset
类的模板。
class CustomDataset(Dataset):
def __init__(self, df):
# Initialize anything you need later here ...
self.df = df
self.X = ...
self.y = ...
# ...
# Get the number of rows in the dataset
def __len__(self):
return len(self.df)
# Get a sample of the dataset
def __getitem__(self, idx):
return [self.X[idx], self.y[idx]]
在加载数据集时,你也可以执行任何需要的预处理,如变换或图像标准化。这发生在 __getitem__()
中。
在这个示例中,我们首先使用 OpenCV 从根目录 (cfg.root_dir
) 加载图像,并将其转换为 RGB 颜色空间。然后我们将应用基本转换:调整图像大小 (cfg.image_size
),并将图像从 NumPy 数组转换为张量。最后,我们将图像的值标准化到 [0, 1] 范围,通过除以 255 实现。
cfg.root_dir = ... # Insert your data here
cfg.image_size = 256
class CustomDataset(Dataset):
def __init__(self,
cfg,
df,
transform=None,
mode = "val"):
self.root_dir = cfg.root_dir
self.df = df
self.file_names = df['file_name'].values
self.labels = df['label'].values
if transform:
self.transform = transform
else:
self.transform = A.Compose([
A.Resize(cfg.image_size, cfg.image_size),
ToTensorV2(),
])
def __len__(self):
return len(self.df)
def __getitem__(self, idx):
# Get file_path and label for index
label = self.labels[idx]
file_path = os.path.join(self.root_dir, self.file_names[idx])
# Read an image with OpenCV
image = cv2.imread(file_path)
# Convert the image to RGB color space.
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
# Apply augmentations
augmented = self.transform(image=image)
image = augmented['image']
# Normalize because ToTensorV2() doesn't normalize the image
image = image/255
return image, label
接下来,我们需要一个 Dataloader
来将 Dataset
的样本批次喂入神经网络,因为我们(可能)没有足够的 RAM 一次性喂入所有图像。
你需要提供 Dataloader
你要遍历的 Dataset
实例、批次大小 (cfg.batch_size
),以及是否打乱数据的信息。
cfg.batch_size = 32
example_dataset = CustomDataset(cfg, df)
example_dataloader = DataLoader(example_dataset,
batch_size = cfg.batch_size,
shuffle = True,
num_workers=0,
)
批次大小应在训练过程中保持固定,不应调整 [2]。因为训练速度与批次大小相关,我们希望使用尽可能大的批次大小。首先使用 32 的批次大小,然后按二的幂次增加(64, 128 等),直到出现内存错误,然后使用最后的批次大小。
当你遍历 Dataloader
时,它会给你来自自定义 Dataset
的样本批次。让我们取出第一个批次进行验证:
for (image_batch, label_batch) in example_dataloader:
print(image_batch.shape)
print(label_batch.shape)
break
torch.Size([32, 3, 256, 256])
torch.Size([32])
Dataloader
返回图像批次和标签批次。image_batch
是形状为 (32, 3, 256, 256)
的张量。这是一个包含 32 张图像的批次,每张图像的形状为 (3, 256, 256)
(color_channels, image_height, image_width
)。label_batch
是形状为 (32)
的张量。这些是与 32 张图像对应的标签。
自定义数据集的 Dataloader 示例输出
本节解释了如何构建数据管道。在后面的章节中(见 设置训练管道),我们将使用 Dataset
和 Dataloader
创建用于训练、验证和测试的独立管道。
在训练模型之前,我们需要将训练数据再次拆分为训练集和验证集。在一个数据集上训练模型然后在相同数据上评估模型是一种方法上的错误,因为模型只需记住已见样本的标签。因此,模型会过拟合训练数据,而不是进行泛化。
为了避免过拟合,我们暂时使用 train_test_split()
函数将训练数据随机分割为训练集和验证集。本节稍后将被 交叉验证策略 替代。
X = df
y = df.label
train_df, valid_df, y_train, y_test = train_test_split(X,
y,
test_size = 0.2,
random_state = 42)
将训练数据再次拆分为训练集和验证集(灵感来源于 scikit-learn)
有了这个分割,我们现在可以为训练和验证数据创建 Datasets
和 Dataloaders
:
train_dataset = CustomDataset(cfg, train_df)
valid_dataset = CustomDataset(cfg, valid_df)
train_dataloader = DataLoader(train_dataset,
batch_size = cfg.batch_size,
shuffle = True)
valid_dataloader = DataLoader(valid_dataset,
batch_size = cfg.batch_size,
shuffle = False)
准备模型
这部分将学习如何在 PyTorch 中构建神经网络。当我开始学习深度学习时,我认为构建神经网络是训练深度学习模型的重要部分。但实际上,这是研究人员为我们完成的工作。我们这些从业者只需使用最终模型即可。
研究人员尝试不同的模型架构,例如卷积神经网络(CNN),通常会在大型基准数据集(如 ImageNet [3])上训练图像分类模型。我们称这些模型为 骨干网。
期望与现实:实际上,只有少数人从头开始训练用于图像分类的神经网络
微调预训练神经网络之所以有效,是因为前几层通常会学习到通用特征(如边缘检测)。
⚒️ 当然,你应该了解神经网络的一般工作原理,包括反向传播,以及不同层(如卷积层)的工作方式。然而,为了跟上这个实际教程,你现在不需要理解这些细节。完成本教程后,你可以通过免费的 Kaggle Learn 课程填补一些理论空白,课程包括 深度学习 和 计算机视觉。
绝妙的骨干网及其获取途径 — 现在,你应该选择哪些预训练模型,以及从哪里获得这些模型?
在本教程中,我们将使用 [timm](https://timm.fast.ai/)
— 一个包含由 Ross Wightman 创建的先进计算机视觉模型集合的深度学习库 — 来获取预训练模型。(你可以使用 torchvision.models
来获取预训练模型,但我个人觉得在实验中使用 timm
更容易更换骨干网络。)
import timm
cfg.n_classes = 2
cfg.backbone = 'resnet18'
model = timm.create_model(cfg.backbone,
pretrained = True,
num_classes = cfg.n_classes)
这段代码包含很多内容。让我们一步步来解读:
backbone = 'resnet18'
— 在这个例子中,我们使用一个 18 层的 ResNet [5]。ResNet 代表残差网络,它是一种使用所谓的残差块的 CNN。
⚒️我们将跳过 ResNet 和残差块的详细内容。如果你对技术细节感兴趣, 你可以深入了解这篇文章,例如。
ResNet 系列中有许多不同的模型,如 ResNet18、ResNet34 等,其中的数字表示网络的层数。一个(非常粗略的)经验法则是:层数越多,性能越好。你可以打印 timm.list_models('*resnet*')
来查看其他可用的模型。
⚒️ 了解不同的计算机视觉/图像分类骨干网络,如 ResNet、DenseNet 和 EfficientNet。
pretrained = True
— 这意味着我们希望使用在 ImageNet [3] 上训练的模型权重。如果设置为 False
,你将只得到模型的结构而没有权重 [6]。
num_classes = cfg.n_classes
— 由于模型是在 ImageNet [3] 上预训练的,你将得到一个包含 ImageNet 中 1000 个类别的分类器。因此,你需要移除 ImageNet 分类器并定义你问题中的类别数量 [6]。如果你设置 num_classes = 0
,你将得到没有分类器的模型 [6]。
要检查输出大小,你可以传入一个具有随机值的 3 通道样本批次 X
,尺寸为 [6]。
X = torch.randn(cfg.batch_size, 3, cfg.image_size, cfg.image_size)
y = model(X)
它将输出 torch.Size([1, cfg.n_classes])
[6]。
模型的输入和输出
准备损失函数和优化器
接下来,训练一个模型有两个关键要素:
-
一个损失函数(准则),
-
一个优化算法(优化器),和
-
可选的学习率调度器。
损失函数 — 常见的损失函数有:
-
二分类交叉熵(BCE)损失用于二分类。
-
分类交叉熵损失用于多分类。
-
均方损失用于回归。
虽然我们有一个二分类问题,但你也可以使用分类交叉熵损失。如果你愿意,可以将损失函数更换为 BCE。
criterion = nn.CrossEntropyLoss()
优化器 — 优化算法通过最小化损失函数(在我们的例子中是交叉熵损失)来优化模型。有很多不同的优化器可以选择。我们使用一个流行的优化器:Adam。
cfg.learning_rate = 1e-4
optimizer = torch.optim.Adam(
model.parameters(),
lr = cfg.learning_rate,
weight_decay = 0,
)
学习率调度器— 学习率调度器会在训练过程中调整学习率的值。虽然你不必使用学习率调度器,但使用它可以帮助算法更快收敛。这是因为如果学习率保持不变,如果学习率过大,它可能会阻碍你找到最佳解,如果学习率过小,则可能需要很长时间才能收敛。
有许多不同的学习率调度器可用,但 Kaggle 大师建议将余弦衰减作为微调的学习率调度器 [2]。
cfg.lr_min = 1e-5
cfg.epochs = 5
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(
optimizer,
T_max = np.ceil(len(train_dataloader.dataset) / cfg.batch_size) * cfg.epochs,
eta_min = cfg.lr_min
)
T_max
定义了半周期,应该等于最大迭代次数(np.ceil(len(train_dataloader.dataset) /cfg.batch_size)*cfg.epochs
)。
训练过程中,学习率的变化情况如下所示:
余弦衰减学习率调度器
指标 — 既然我们在谈论它,我们还需要定义一个指标来评估模型的整体性能。同样,有许多不同的指标。对于这个示例,我们将使用准确率作为指标:
from sklearn.metrics import accuracy_score
def calculate_metric(y, y_pred):
metric = accuracy_score(y, y_pred)
return metric
不要将指标与损失函数混淆。损失函数用于在训练过程中优化学习函数,而指标则在训练后衡量模型的性能。
⚒️ 了解不同的指标以及哪些指标适用于哪些问题。
设置训练管道
这可能是本教程中最复杂但也是最有趣的部分。你准备好了吗?
模型通常以迭代的方式进行训练。一轮迭代称为一个 epoch。从头开始训练通常需要许多 epochs,而微调只需要几个(大约 5 到 10)epochs。
在每个 epoch 中,模型会在完整的训练数据上进行训练,然后在完整的验证数据上进行验证。我们现在将定义两个函数:一个用于训练(train_an_epoch()
),另一个用于在一个 epoch 上验证模型(validate_an_epoch()
)。
下面你可以看到训练函数:
cfg.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
def train_one_epoch(dataloader, model, optimizer, scheduler, cfg):
# Training mode
model.train()
# Init lists to store y and y_pred
final_y = []
final_y_pred = []
final_loss = []
# Iterate over data
for step, batch in tqdm(enumerate(dataloader), total=len(dataloader)):
X = batch[0].to(cfg.device)
y = batch[1].to(cfg.device)
# Zero the parameter gradients
optimizer.zero_grad()
with torch.set_grad_enabled(True):
# Forward: Get model outputs
y_pred = model(X)
# Forward: Calculate loss
loss = criterion(y_pred, y)
# Covert y and y_pred to lists
y = y.detach().cpu().numpy().tolist()
y_pred = y_pred.detach().cpu().numpy().tolist()
# Extend original list
final_y.extend(y)
final_y_pred.extend(y_pred)
final_loss.append(loss.item())
# Backward: Optimize
loss.backward()
optimizer.step()
scheduler.step()
# Calculate statistics
loss = np.mean(final_loss)
final_y_pred = np.argmax(final_y_pred, axis=1)
metric = calculate_metric(final_y, final_y_pred)
return metric, loss
让我们一步步来看:
-
将模型设置为训练模式。模型也可以处于评估模式。这种模式会影响模型中
[Dropout](https://pytorch.org/docs/stable/_modules/torch/nn/modules/dropout.html)
和[BatchNorm](https://pytorch.org/docs/stable/_modules/torch/nn/modules/batchnorm.html)
层的行为。 -
以小批量迭代训练数据。如果你使用 GPU 进行更快的训练,则需要将样本和标签移动到 GPU (
cfg.device
)。 -
清除优化器的最后一个误差梯度。
-
通过模型进行一次前向传播。
-
计算模型输出的损失。
-
通过模型反向传播误差。
-
更新模型以减少损失。
-
步进学习率调度器。
-
计算损失和指标以获取统计数据。由于预测将是 GPU 上的张量,就像输入一样,我们需要分离张量以便将它们从自动微分图中分离,并调用 NumPy 函数将其转换为 NumPy 数组。
接下来,我们定义如下所示的验证函数:
def validate_one_epoch(dataloader, model, cfg):
# Validation mode
model.eval()
final_y = []
final_y_pred = []
final_loss = []
# Iterate over data
for step, batch in tqdm(enumerate(dataloader), total=len(dataloader)):
X = batch[0].to(cfg.device)
y = batch[1].to(cfg.device)
with torch.no_grad():
# Forward: Get model outputs
y_pred = model(X)
# Forward: Calculate loss
loss = criterion(y_pred, y)
# Covert y and y_pred to lists
y = y.detach().cpu().numpy().tolist()
y_pred = y_pred.detach().cpu().numpy().tolist()
# Extend original list
final_y.extend(y)
final_y_pred.extend(y_pred)
final_loss.append(loss.item())
# Calculate statistics
loss = np.mean(final_loss)
final_y_pred = np.argmax(final_y_pred, axis=1)
metric = calculate_metric(final_y, final_y_pred)
return metric, loss
让我们再一步步看一遍:
-
将模型设置为评估模式。
-
对验证数据进行小批量迭代。如果使用 GPU 进行更快的训练,样本和标签需要移动到 GPU 上。
-
通过模型进行前向传播。
-
计算损失和指标以获取统计数据。
初看,训练和验证一个时期看起来很相似。让我们看看代码比较,以使差异更清晰:
在 PyTorch 中训练和验证代码的对比截图
你可以看到以下差异:
-
模型必须处于训练或评估模式。
-
训练模型时,我们需要一个优化器和一个可选的调度器。对于验证,我们只需要模型。
-
梯度计算仅在训练时激活。对于验证,我们不需要它。
交叉验证策略
现在,我们还没有完成训练管道。之前,我们将训练数据分为训练数据和验证数据。但是,将可用数据分为两个固定的集合限制了训练样本的数量。
相反,我们将使用交叉验证策略,将训练数据分为k个折叠。然后,模型将在k次独立的迭代中训练,其中每次迭代模型在k-1 个折叠上训练,并在一个折叠上进行验证,每次迭代折叠都会切换,如下所示:
将训练数据再次分为训练和验证(灵感来源于scikit-learn)
在这个例子中,我们使用了[StratifiedKFold](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.StratifiedKFold.html)
来创建分割。你也可以使用KFold
,但StratifiedKFold
的优点是它保持了类分布。
from sklearn.model_selection import StratifiedKFold
cfg.n_folds = 5
# Create a new column for cross-validation folds
df["kfold"] = -1
# Initialize the kfold class
skf = StratifiedKFold(n_splits=cfg.n_folds)
# Fill the new column
for fold, (train_, val_) in enumerate(skf.split(X = df, y = df.label)):
df.loc[val_ , "kfold"] = fold
for fold in range(cfg.n_folds):
train_df = df[df.kfold != fold].reset_index(drop=True)
valid_df = df[df.kfold == fold].reset_index(drop=True)
添加数据增强
当训练和验证指标之间的差异显著时,这表明模型正在过拟合训练数据。过拟合发生在模型仅在少量示例上进行训练,并从训练数据中学习无关的细节或噪声。这会对模型在呈现新示例时的表现产生负面影响。结果,模型在新图像上的泛化能力受限。
为了在训练过程中克服过拟合,你可以使用数据增强。数据增强通过随机变换现有图像生成额外的训练数据。这种技术让模型接触到数据的更多方面,从而帮助它更好地泛化。
我们可以使用albumentations
包中的一些准备好的数据增强方法,例如:
-
图像旋转(
A.Rotate()
) -
水平翻转(
A.HorizontalFlip()
) -
切割 [4](
A.CoarseDropout()
)
之前,我们定义了一个基本的变换来调整图像大小并将其转换为张量。我们将继续在验证和测试数据集中使用它,因为它们不需要任何增强。对于训练数据集,我们创建了一个新的变换transform_soft
,它在调整大小和转换为张量之外,还包含了上述三种增强。
transform_soft = A.Compose([A.Resize(cfg.image_size, cfg.image_size),
A.Rotate(p=0.6, limit=[-45,45]),
A.HorizontalFlip(p = 0.6),
A.CoarseDropout(max_holes = 1, max_height = 64, max_width = 64, p=0.3),
ToTensorV2()])
你可以通过参数p
来控制增强应用到图像的百分比。
如果我们可视化从增强数据集中提取的一些样本,我们可以看到三种增强成功应用:
-
图像 0、1、2、4 中的旋转
-
水平翻转很难检测,如果你不知道原始图像,但我们可以看到图像 2 必须是水平翻转的
-
图像 1 和 4 中的切割(粗略丢弃)
增强后的训练数据集
⚒️ 接下来,你可以审查并添加其他图像增强技术,例如 Mixup 和 Cutmix,到你的管道中。
## Cutout, Mixup, and Cutmix: 在 PyTorch 中实现现代图像增强
在 Python 中实现的计算机视觉数据增强技术
towardsdatascience.com
将所有内容整合在一起
现在我们已经讨论了从 data pipeline 到 model 的基线的每个组件,包括 loss function and optimizer,到 training pipeline,以及 cross-validation strategy,我们可以将它们综合起来,如下图所示:
基线代码的流程图
我们将遍历交叉验证策略的每一个折叠。在每个折叠中,我们为训练和验证数据设置一个 data pipeline 和一个 model,并配备 loss function and optimizer。然后,对于每个时期,我们将训练和验证模型。
在动手之前,让我们为成功做好准备,固定随机种子以确保可重复的结果。
import random
def set_seed(seed=1234):
random.seed(seed)
os.environ["PYTHONHASHSEED"] = str(seed)
np.random.seed(seed)
# In general seed PyTorch operations
torch.manual_seed(seed)
# If you are using CUDA on 1 GPU, seed it
torch.cuda.manual_seed(seed)
# If you are using CUDA on more than 1 GPU, seed them all
torch.cuda.manual_seed_all(cfg.seed)
# Certain operations in Cudnn are not deterministic, and this line will force them to behave!
torch.backends.cudnn.deterministic = True
# Disable the inbuilt cudnn auto-tuner that finds the best algorithm to use for your hardware.
torch.backends.cudnn.benchmark = False
接下来,我们将编写一个fit()
函数,该函数为所有周期拟合模型。该函数会迭代周期数,而训练和验证函数包含内循环,这些内循环会迭代训练和验证数据集中的批次,如训练管道部分所述。
cfg.seed = 42
def fit(model, optimizer, scheduler, cfg, train_dataloader, valid_dataloader=None):
acc_list = []
loss_list = []
val_acc_list = []
val_loss_list = []
for epoch in range(cfg.epochs):
print(f"Epoch {epoch + 1}/{cfg.epochs}")
set_seed(cfg.seed + epoch)
acc, loss = train_one_epoch(train_dataloader, model, optimizer, scheduler, cfg)
if valid_dataloader:
val_acc, val_loss = validate_one_epoch(valid_dataloader, model, cfg)
print(f'Loss: {loss:.4f} Acc: {acc:.4f}')
acc_list.append(acc)
loss_list.append(loss)
if valid_dataloader:
print(f'Val Loss: {val_loss:.4f} Val Acc: {val_acc:.4f}')
val_acc_list.append(val_acc)
val_loss_list.append(val_loss)
return acc_list, loss_list, val_acc_list, val_loss_list, model
拟合函数的日志
为了可视化目的,我们还将创建训练和验证集上的损失和准确性图:
def visualize_history(acc, loss, val_acc, val_loss):
fig, ax = plt.subplots(1,2, figsize=(12,4))
ax[0].plot(range(len(loss)), loss, color='darkgrey', label = 'train')
ax[0].plot(range(len(val_loss)), val_loss, color='cornflowerblue', label = 'valid')
ax[0].set_title('Loss')
ax[1].plot(range(len(acc)), acc, color='darkgrey', label = 'train')
ax[1].plot(range(len(val_acc)), val_acc, color='cornflowerblue', label = 'valid')
ax[1].set_title('Metric (Accuracy)')
for i in range(2):
ax[i].set_xlabel('Epochs')
ax[i].legend(loc="upper right")
plt.show()
绘制的指标和损失随周期变化的历史
当我们将所有内容结合起来时,它将如下所示:
for fold in range(cfg.n_folds):
train_df = df[df.kfold != fold].reset_index(drop=True)
valid_df = df[df.kfold == fold].reset_index(drop=True)
train_dataset = CustomDataset(cfg, train_df, transform = transform_soft)
valid_dataset = CustomDataset(cfg, valid_df)
train_dataloader = DataLoader(train_dataset,
batch_size = cfg.batch_size,
shuffle = True,
num_workers = 0,
)
valid_dataloader = DataLoader(valid_dataset,
batch_size = cfg.batch_size,
shuffle = False,
num_workers = 0,
)
model = timm.create_model(cfg.backbone,
pretrained = True,
num_classes = cfg.n_classes)
model = model.to(cfg.device)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(),
lr = cfg.learning_rate,
weight_decay = 0,
)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer,
T_max= np.ceil(len(train_dataloader.dataset) / cfg.batch_size) * cfg.epochs,
eta_min=cfg.lr_min)
acc, loss, val_acc, val_loss, model, lrs = fit(model, optimizer, scheduler, cfg, train_dataloader, valid_dataloader)
visualize_history(acc, loss, val_acc, val_loss)
步骤 3:运行实验
数据科学是一门实验科学。因此,这一步的目的是找到实现最佳性能的超参数配置、数据增强、模型主干和交叉验证策略(或任何你的目标,例如,性能和推理时间之间的最佳权衡)。
设置实验跟踪
在进入这一步之前,花点时间考虑一下你将如何跟踪实验。实验跟踪可以简单到用笔和纸记录一切。或者,你可以在电子表格中跟踪所有内容,甚至使用实验跟踪系统来自动化整个过程。
这为什么重要,以及你可以用笔和纸、电子表格等三种不同方式记录和组织你的 ML 实验。
medium.com](https://medium.com/@iamleonie/intro-to-mlops-experiment-tracking-for-machine-learning-858e432bd133?source=post_page-----94ea13f56f2--------------------------------)
如果你是绝对初学者,我建议一开始简单地手动在电子表格中跟踪你的实验。打开一个空电子表格,并为所有输入创建列,例如:
-
主干,
-
学习率,
-
周期,
-
增强方式,以及
-
图像大小
以及你想要跟踪的输出,如训练和验证的损失和指标。
结果电子表格可能会如下所示:
初学者跟踪实验的示例电子表格
⚒️ 一旦你对深度学习技术感到舒适,你可以通过 实现实验跟踪系统 来提升你的水平,以自动化实验跟踪,例如 Weights & Biases, Neptune,或 MLFlow。
实验和超参数调整
现在你有了实验跟踪系统,让我们开始进行一些实验。你可以从调整以下超参数开始:
-
训练步骤数:范围为 2 到 10
-
学习率:范围为 0.0001 到 0.001
-
图像大小:范围为 128 到 1028
-
主干网络:尝试不同的主干网络。首先,尝试 ResNet 家族的更深模型(打印
timm.list_models('*resnet*')
查看其他可用模型),然后尝试不同的主干网络家族,如timm.list_models('*densenet*')
或timm.list_models('*efficientnet*')
⚒️ 当你对深度学习技术感到熟练时,可以通过使用 Optuna 或 Weights & Biases* 来自动化这一步,进一步提升自己。*
现在轮到你了!— 调整几个参数,看看模型的性能如何变化。一旦你对结果满意,就可以进入下一步。
实验日志示例
第四步:进行预测(推断)
请敲鼓!现在我们已经找到能够给我们最优模型的配置,我们希望将其充分利用。
首先,让我们用最佳配置在完整数据集上微调模型,以利用每个数据样本。在这一步中,我们不会将数据拆分为训练数据和验证数据。相反,我们只有一个大的训练数据集。
train_df = df.copy()
train_dataset = CustomDataset(cfg, train_df, transform = transform_soft)
train_dataloader = DataLoader(train_dataset,
batch_size = cfg.batch_size,
shuffle = True,
num_workers = 0,
)
但其余的训练流程保持不变。
model = timm.create_model(cfg.backbone,
pretrained = True,
num_classes = cfg.n_classes)
model = model.to(cfg.device)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(),
lr = cfg.learning_rate,
weight_decay = 0,
)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer,
T_max= np.ceil(len(train_dataloader.dataset) / cfg.batch_size) * cfg.epochs,
eta_min=cfg.lr_min)
acc, loss, val_acc, val_loss, model = fit(model, optimizer, scheduler, cfg, train_dataloader)
推断 — 最后,我们将使用模型来预测保留的测试集。
test_dataset = CustomDataset(cfg, test_df)
test_dataloader = DataLoader(test_dataset,
batch_size = cfg.batch_size,
shuffle = False,
num_workers = 0,
)
dataloader = test_dataloader
# Validation mode
model.eval()
final_y = []
final_y_pred = []
# Iterate over data
for step, batch in tqdm(enumerate(dataloader), total=len(dataloader)):
X = batch[0].to(cfg.device)
y = batch[1].to(cfg.device)
with torch.no_grad():
# Forward: Get model outputs
y_pred = model(X)
# Covert y and y_pred to lists
y = y.detach().cpu().numpy().tolist()
y_pred = y_pred.detach().cpu().numpy().tolist()
# Extend original list
final_y.extend(y)
final_y_pred.extend(y_pred)
# Calculate statistics
final_y_pred_argmax = np.argmax(final_y_pred, axis=1)
metric = calculate_metric(final_y, final_y_pred_argmax)
test_df['prediction'] = final_y_pred_argmax
下面你可以看到我们模型的结果:
预测
总结与下一步
本教程向你展示了如何微调预训练的图像分类模型以适应你的特定任务,评估它,并使用 Python 中的 PyTorch 框架对未见数据进行推断。
当你感到熟练后,可以通过查看标有 ⚒️ 的部分来提升到中级水平。
深度学习模型在计算机视觉和自然语言处理中的微调实用指南
towardsdatascience.com
喜欢这个故事吗?
免费订阅 以便在我发布新故事时获得通知。
[## 每当 Leonie Monigatti 发布新内容时获取电子邮件通知。
每当 Leonie Monigatti 发布新内容时获取电子邮件通知。通过注册,如果你还没有的话,你将创建一个 Medium 账户……
medium.com](https://medium.com/@iamleonie/subscribe?source=post_page-----94ea13f56f2--------------------------------)
在 LinkedIn,Twitter,以及 Kaggle上找到我!
参考文献
数据集
[1] MikołajFish99 (2023). 狮子还是猎豹——图像分类 在 Kaggle 数据集中。
许可: 根据原始图像来源 (开放图像数据集 V6),注释由 Google LLC 根据CC BY 4.0许可授权,图像的许可列为CC BY 2.0。
注意,原始数据集包含 200 张图像,每个类别各 100 张图像。但数据集需要一些清理,包括移除其他动物的图像;因此,最终数据集略小。为了保持教程简短,我们将跳过数据清理过程。
图像
如果没有其他说明,所有图像均由作者创作。
文献
[2] S. Bhutani 与 H20.ai (2023). 训练 ML 模型的最佳实践 | @ChaiTimeDataScience #160 在 2023 年 1 月于 YouTube 上发布。
[3] Deng, J., Dong, W., Socher, R., Li, L. J., Li, K., & Fei-Fei, L. (2009 年 6 月). Imagenet:一个大规模的分层图像数据库。见 2009 年 IEEE 计算机视觉与模式识别会议(第 248–255 页)。Ieee。
[4] DeVries, T., & Taylor, G. W. (2017). 使用 cutout 改进卷积神经网络的正则化。 arXiv 预印本 arXiv:1708.04552。
[5] K. He, X. Zhang, S. Ren, & J. Sun (2016). 深度残差学习用于图像识别。见 IEEE 计算机视觉与模式识别会议论文集(第 770–778 页)。
[6] timmdocs (2022). Pytorch 图像模型(timm)(访问日期:2023 年 4 月 10 日)。
PyTorch 介绍 — 构建你的第一个线性模型
原文:
towardsdatascience.com/pytorch-introduction-building-your-first-linear-model-d868a8681a41
学习如何通过使用“神奇”的 Linear 层来构建你的第一个 PyTorch 模型。
·发表于 Towards Data Science ·阅读时长 8 分钟·2023 年 12 月 12 日
–
回归模型 — AI 生成的图像
在我上一篇博客中,我们学习了如何使用 PyTorch 张量,这是 PyTorch 库中最重要的对象。张量是深度学习模型的骨架,因此我们可以利用它们来将更简单的机器学习模型拟合到我们的数据集上。
尽管 PyTorch 以其深度学习能力而闻名,但我们也可以使用该框架来拟合简单的线性模型——这实际上是熟悉torch
API 的最佳方式之一!
在这篇博客中,我们将继续 PyTorch 介绍系列,查看如何使用 torch
库开发一个简单的线性回归模型。在这个过程中,我们将了解 torch
的优化器、权重和其他学习模型参数,这对于更复杂的架构将非常有用。
让我们开始吧!
加载和处理数据
在这篇博客中,我们将使用歌曲流行度数据集,我们希望根据一些歌曲特征来预测某首歌曲的流行度。让我们先看一下数据集的前几行:
songPopularity = pd.read_csv(‘./data/song_data.csv’)
歌曲流行度特征列 — 作者提供的图像
这个数据集的一些特征包括关于每首歌曲的有趣指标,例如:
-
歌曲的“能量”级别。
-
对歌曲的关键(例如 A、B、C、D 等)进行标签编码
-
歌曲响度
-
歌曲节奏。
我们的目标是利用这些特征来预测歌曲流行度,这是一个从 0 到 100 的指数。在我们上面展示的示例中,我们旨在预测以下歌曲流行度:
歌曲受欢迎程度目标列— 图片由作者提供
我们将使用 PyTorch 模块来预测这个连续变量,而不是使用 sklearn
。学习如何在 pytorch
中拟合线性回归的好处是什么?我们将获得的知识可以应用于其他复杂模型,如深层神经网络!
让我们从准备数据集开始,首先对特征和目标进行子集划分:
features = ['song_duration_ms',
'acousticness', 'danceability',
'energy', 'instrumentalness',
'key', 'liveness', 'loudness',
'audio_mode', 'speechiness',
'tempo', 'time_signature', 'audio_valence']
target = 'song_popularity'
songPopularityFeatures = songPopularity[features]
songPopularityTarget = songPopularity[target]
我们将使用 train_test_split
将数据划分为训练集和测试集。我们将在将数据转换为 tensors
之前执行此转换,因为 sklearn
的方法会自动将数据转换为 pandas
或 numpy
格式:
X_train, X_test, y_train, y_test = train_test_split(songPopularityFeatures, songPopularityTarget, test_size = 0.2)
创建了 X_train
、 X_test
、 y_train
和 y_test
后,我们现在可以将数据转换为 torch.tensor
— 通过将数据传递给 torch.Tensor
函数来完成这个过程很简单:
import torch
def dataframe_to_tensor(df):
return torch.tensor(df.values, dtype=torch.float32)
# Transform DataFrames into PyTorch tensors using the function
X_train = dataframe_to_tensor(X_train)
X_test = dataframe_to_tensor(X_test)
y_train = dataframe_to_tensor(y_train)
y_test = dataframe_to_tensor(y_test)
我们的对象现在是 torch.Tensor
格式,这是 nn.Module
期望的格式。下面是 X_train
:
X_train 张量 — 图片由作者提供
很棒——我们已经将训练和测试数据转换为 tensor
格式。我们准备好创建我们的第一个 torch
模型了,接下来我们将做这件事!
构建我们的线性模型
我们将使用一个继承自 nn.Module
父类的 LinearRegressionModel class
来训练我们的模型。nn.Module
类是 Pytorch 所有神经网络的基础类。
from torch import nn
class LinearRegressionModel(nn.Module):
'''
Torch Module class.
Initializes weight randomly and gets trained via train method.
'''
def __init__(self, optimizer):
super().__init__()
self.optimizer = optimizer
# Initialize Weights and Bias
self.weights = nn.Parameter(
torch.randn(1, 5, dtype=torch.float),
requires_grad=True)
self.bias = nn.Parameter(
torch.randn(1, 5, dtype=torch.float),
requires_grad=True
)
在这个类中,我们创建对象时只需要一个参数——optimizer
。我们这样做是因为我们希望在训练过程中测试不同的优化器。在上面的代码中,让我们聚焦于 # Initialize Weights and Bias
之后的权重初始化:
self.weights = nn.Parameter(
torch.randn(1, 13, dtype=torch.float),
requires_grad=True)
self.bias = nn.Parameter(
torch.randn(1, dtype=torch.float),
requires_grad=True
)
线性回归是一个非常简单的函数,公式为 y = b0 + b1x1 + ... bnxn
其中:
-
y 等于我们想要预测的目标
-
b0 等于偏差项。
-
b1, …, bn 等于模型的权重(每个变量在最终决策中的权重以及它是负面还是正面贡献)。
-
x1, …, xn 是特征的值。
nn.Parameter
的理念是初始化 b0(在 self.bias
中初始化的偏差)和 b1, … , bn(在 self.weights
中初始化的权重)。我们正在初始化 13 个权重,因为我们的训练数据集中有 13 个特征。
由于我们处理的是线性回归,因此只有一个偏差值,所以我们只需初始化一个随机标量(如果这个名字对你来说很陌生,可以查看我的第一篇文章!)。此外,请注意,我们正在使用 torch.randn
随机初始化这些参数。
现在,我们的目标是通过反向传播优化这些权重——为此,我们需要设置我们的线性层,包括回归公式:
def forward(self, x: torch.Tensor) -> torch.Tensor:
return (self.weights * x + self.bias).sum(axis=1)
trainModel
方法将帮助我们执行反向传播和权重调整:
def trainModel(
self,
epochs: int,
X_train: torch.Tensor,
X_test: torch.Tensor,
y_train: torch.Tensor,
y_test: torch.Tensor,
lr: float
):
'''
Trains linear model using pytorch.
Evaluates the model against test set for every epoch.
'''
torch.manual_seed(42)
# Create empty loss lists to track values
self.train_loss_values = []
self.test_loss_values = []
loss_fn = nn.L1Loss()
if self.optimizer == 'SGD':
optimizer = torch.optim.SGD(
params=self.parameters(),
lr=lr
)
elif self.optimizer == 'Adam':
optimizer = torch.optim.Adam(
params=self.parameters(),
lr=lr
)
for epoch in range(epochs):
self.train()
y_pred = self(X_train)
loss = loss_fn(y_pred, y_train)
optimizer.zero_grad()
loss.backward()
optimizer.step()
# Set the model in evaluation mode
self.eval()
with torch.inference_mode():
self.evaluate(X_test, y_test, epoch, loss_fn, loss)
在这个方法中,我们可以选择使用随机梯度下降(SGD)或自适应矩估计(Adam)优化器。更重要的是,让我们深入了解每个训练周期(对整个数据集的一次遍历)之间发生了什么:
self.train()
y_pred = self(X_train)
loss = loss_fn(y_pred, y_train)
optimizer.zero_grad()
loss.backward()
optimizer.step()
这段代码在神经网络的背景下极为重要。它包括了典型的torch
模型的训练过程:
-
我们使用
self.train()
将模型设置为训练模式 -
接下来,我们使用
self(X_train)
将数据传递给模型 — 这将把数据传递通过前向层。 -
loss_fn
计算训练数据上的损失。我们的损失函数是torch.L1Loss
,由平均绝对误差组成。 -
optimizer.zero_grad()
将梯度设置为零(它们会在每个周期累积,因此我们希望在每次遍历时从零开始)。 -
loss.backward()
计算每个权重相对于损失函数的梯度。这是优化权重的步骤。 -
最后,我们使用
optimizer.step()
更新模型的参数
最后的步骤是揭示如何使用evaluate
方法评估我们的模型:
def evaluate(self, X_test, y_test, epoch_nb, loss_fn, train_loss):
'''
Evaluates current epoch performance on the test set.
'''
test_pred = self(X_test)
test_loss = loss_fn(test_pred, y_test.type(torch.float))
if epoch_nb % 10 == 0:
self.train_loss_values.append(train_loss.detach().numpy())
self.test_loss_values.append(test_loss.detach().numpy())
print(f"Epoch: {epoch_nb} - MAE Train Loss: {train_loss} - MAE Test Loss: {test_loss} ")
这段代码计算测试集中的损失。此外,我们将使用这种方法在每 10 个周期中打印训练和测试集的损失。
模型准备好后,让我们在数据上训练它,并可视化训练和测试的学习曲线!
拟合模型
让我们使用构建的代码来训练模型并观察训练过程 — 首先,我们将使用Adam
优化器和0.001
学习率训练模型 500 个周期:
adam_model = LinearRegressionModel('Adam')
adam_model.trainModel(200, X_train, X_test, y_train, y_test, 0.001)
这里我使用adam
优化器训练模型 200 个周期。训练和测试损失的概述如下:
前 150 个周期的训练和测试演变 — 作者提供的图片
我们还可以绘制整个周期的训练和测试损失:
训练和测试损失 — 作者提供的图片
我们的损失仍然有点高(在最后一个周期,MAE 约为 21),因为线性回归可能无法解决这个问题。
最后,让我们只用SGD
拟合模型:
sgd_model = LinearRegressionModel(‘SGD’)
sgd_model.trainModel(500, X_train, X_test, y_train, y_test, 0.001)
SGD 模型的训练和测试损失 — 作者提供的图片
有趣的是 — 训练和测试损失没有改善!这发生是因为 SGD 对特征缩放非常敏感,可能在处理不同尺度的特征时难以计算梯度。作为挑战,尝试对特征进行缩放,并用SGD
检查结果。缩放后,你还会注意到Adam
优化器模型的行为更稳定!
结论
感谢您抽出时间阅读这篇文章!在这篇博客文章中,我们检查了如何使用torch
训练一个简单的线性回归模型。虽然 PyTorch 因其深度学习(更多层和复杂功能)而闻名,但学习简单模型是玩转这个框架的好方法。此外,这也是熟悉“损失”函数和梯度概念的绝佳用例。
我们还看到了 SGD
和 Adam
优化器的工作原理,特别是它们对未缩放特征的敏感程度。
最后,我希望你保留这个过程,它可以扩展到其他类型的模型、函数和过程:
-
train()
将模型设置为训练模式。 -
使用
torch.model
将数据传递给模型。 -
使用
nn.L1Loss()
进行回归问题。其他损失函数可以在这里找到。 -
optimizer.zero_grad()
将梯度设置为零。 -
loss.backward()
计算每个权重相对于损失函数的梯度。 -
使用
optimizer.step()
更新模型的权重。
下次 PyTorch 的帖子见!我还推荐你访问 PyTorch 从零到精通课程,这是一个精彩的免费资源,激发了这篇文章的方法论。
欢迎加入我新创建的 YouTube 频道——数据之旅。
本博客文章中使用的数据集可以在 Kaggle 平台上获得,并通过 Spotify 官方 APP 提取(www.kaggle.com/datasets/yasserh/song-popularity-dataset/data
)。 数据集的许可证为 CC0:公有领域*
PyTorch 简介——张量与张量计算
原文:
towardsdatascience.com/pytorch-introduction-tensors-and-tensor-calculations-412ff818bd5b
了解张量以及如何在最著名的机器学习库之一 pytorch
中使用它们
·发表于 Towards Data Science ·8 分钟阅读·2023 年 11 月 30 日
–
数学魔法——由 AI 生成的图像
在深度学习领域(包括 ChatGPT 的构建基础)中最重要的库之一是 pytorch
。与 Tensorflow 框架一起,pytorch
是可供软件开发人员和数据科学家使用的最著名的神经网络训练框架之一。除了其可用性和简单的 API 外,它在灵活性和内存使用方面表现出色,使其在多维微积分中极其快速(这是反向传播的一个主要组成部分,反向传播是优化神经网络权重的重要技术)——这些细节使其成为公司在构建深度学习模型时最受欢迎的库之一。
在这篇博客文章中,我们将检查一些使用pytorch
的基本操作,并了解如何处理tensor
对象!张量是数据的数学表示,通常有不同的名称:
-
1 维张量:通常称为标量,由一个数学值组成。
-
1 维张量:由 n 个示例组成,通常称为 1-D 向量,能够在单一维度中存储不同的数学元素。
-
2 维张量:通常称为矩阵,能够在两个维度中存储数据。可以想象成一个普通的 SQL 表或一个 Excel 电子表格。
-
3 维张量及以上:具有这种维度的数据通常更难以可视化,通常称为 n-维 张量*。
在对数学概念进行简单介绍后,让我们探索如何在 Python 中使用pytorch
!
张量对象
如我们所述,张量对象是 n 维 对象的数学泛化,可以扩展到几乎任何维度。尽管在深度学习的上下文中,tensors
通常是多维的,但我们也可以使用 torch
创建单元素张量(通常称为标量)(尽管名为 pytorch
,我们使用 torch
来操作 Python 中的库)。
如果张量是 torch
(或 pytorch
)中的核心对象,我们如何在库中创建它们?
超简单!让我们创建第一个单元素张量:
import torch
scalar = torch.tensor(5)
我们的 scalar
对象包含一个数字 — 5。让我们通过在 Python 控制台中调用它来可视化我们的张量:
标量对象 — 作者图像
事实 1:
torch.tensor
用于创建张量对象
当然,我们不仅限于单元素张量 — 我们还可以创建具有多个元素的 1 维对象。让我们将一个列表传递到 torch.tensor
中,看看结果如何:
vector = torch.tensor([7, 7])
vector
vector 对象 — 作者图像
我们的对象 vector
现在包含一个维度上的两个元素。可以将这些数据视为 1 行或 1 列的数据。
拥有“维度”允许我们访问张量中的有趣属性 — 例如 ndim
:
vector.ndim
vector 对象的 ndim — 作者图像
事实 2:
*tensor.ndim*
用于获取张量对象的维度数量
在我们的例子中,vector
对象只有一个维度。我们如何知道张量对象包含多少元素?通过使用另一个属性 - shape
!
vector.shape
vector 对象的 shape — 作者图像
事实 3:
*tensor.shape*
用于获取张量对象的形状
我们的张量对象在一个维度中包含两个元素。我们将看看这个输出与多维对象的比较。
torch
张量还包含一个附加的数据类型。要了解是哪个,我们可以使用:
vector.dtype
vector 对象的 dtype — 作者图像
事实 4:
*tensor.dtype*
输出张量对象的类型。
我们的张量包含 int64
格式的数据。
现在让我们将对象扩展为一个 2-D 张量:
matrix = torch.tensor([[10.0, 20.0],
[30.0, 40.0]])
matrix
matrix 对象 — 作者图像
让我们看看关于我们的 matrix
对象的一些属性:
print(matrix.ndim)
print(matrix.shape)
print(matrix.dtype)
ndim、shape 和 dtype 的矩阵对象 — 作者图像
我们的 matrix
对象包含两个维度中每个有 2 个元素的数据,dtype 为 float32
。
为了完成我们对创建张量的探索,让我们看看如何使用 torch.rand
生成随机张量:
torch.rand(size=(4, 4))
随机张量 — 作者图像
例如,在上面的张量中,我们使用tensor.rand
生成一个 4x4 的矩阵。这是在深度学习中非常常见的操作(例如,生成随机的神经网络层权重以便后续优化)。
张量操作
现在我们来看看如何对张量进行操作。如果你已经熟悉numpy
,这应该很简单!从一个简单的加法操作开始:
tensor = torch.tensor([1, 2, 3])
tensor + 20
张量 + 10 计算 — 作者提供的图片
向张量添加标量很简单 — 只需使用普通的数学操作即可!你能猜到如何将张量与标量相乘吗?
简单!
tensor * 10
张量 * 10 计算 — 作者提供的图片
你也可以使用抽象的torch.multiply
:
torch.multiply(tensor, 10)
张量 * 10 计算 — 作者提供的图片
张量的两个最常见的操作是Hadamard和点积,后者是注意力机制中广泛使用的著名计算之一。
让我们创建两个 2 维张量来检查这些操作:
tensor_1 = torch.tensor([[1,2,3],[2,3,4]])
tensor_2 = torch.tensor([[1,2],[2,3],[3,4]])
张量 _1,一个 2x3 张量 — 作者提供的图片
张量 _2,一个 3x2 张量 — 作者提供的图片
要执行 Hadamard 积,张量的形状必须匹配。让我们计算tensor_1
与其自身的 Hadamard 积:
# Hadamard product
tensor_1 * tensor_1
张量 _1 乘以张量 _1 — 作者提供的图片
对于点积操作,张量的内维度必须匹配。让我们将张量 _1(一个 2x3 张量)与张量 _2(一个 3x2 张量)相乘:
torch.matmul(tensor_1, tensor_2)
张量 _1 与张量 _2 的点积 — 作者提供的图片
我们也可以使用优雅的@操作,它执行相同的操作:
tensor_1 @ tensor_2
张量 _1 与张量 _2 的点积 — 作者提供的图片
张量索引
在最后的示例中,让我们看看如何从张量中提取某些元素。对于这些示例,我们将使用:
indexing_example = torch.tensor([[10,20,30],[40,50,60],[70,80,90]])
indexing_example
2-D 张量示例 — 作者提供的图片
pytorch
中的索引与其他 Python 对象类似 — 让我们尝试索引第一列:
indexing_example[0,:]
第 1 行示例 — 作者提供的图片
使用[]
中的 0 索引将使我们能够提取对象的第一行。:
符号使我们能够从某一维度提取所有元素。在我们的例子中,我们想要从列(第 2 维)中提取所有元素。
你能猜到如何提取第一列吗?只需交换索引的位置!
indexing_example[:,0]
第 1 列示例 — 作者提供的图片
对于更复杂的对象,我们也可以使用相同的逻辑。让我们尝试从一个 3D tensor
中索引一个元素:
indexing_example_3d = torch.tensor([[[10,20,30],[40,50,60],[70,80,90]], [[100,200,300],[400,500,600],[700,800,900]]])
indexing_example_3d
3D 张量 — 图片由作者提供
我们如何从这个张量中提取元素“100”?让我们来看看,我们需要:
-
第一行
-
第一列
-
第二个矩阵
使用索引逻辑,我们可以轻松实现这一点:
indexing_example_3d[1,0,0]
从 indexing_example_3d 提取的 100 元素 — 图片由作者提供
在torch
中,3D 对象的索引顺序如下:矩阵、行、列。
你能尝试索引一个 4D 对象吗?
额外内容 — 张量存储在哪里?
使用torch
相对于其他数组库(如numpy
)的一个优点是能够将张量保存在gpu
中——这在我们需要加速神经网络计算时特别有用。
默认情况下,你的张量存储在cpu
上(而大多数计算机仅有一个 cpu 可用),但你可以通过以下操作将张量发送到gpu
:
device = "cuda" if torch.cuda.is_available() else "cpu"
如果torch.cuda.is_available()
在你的计算机上找到特定的 NVIDIA gpu,它会让你将张量发送到该 gpu。
想象一下你有一个存储在tensor
命名对象中的张量,你可以使用.to
方法将其发送到设备:
tensor_on_gpu = tensor.to(device)
结论
感谢你抽时间阅读这篇文章!处理张量非常有趣,确实能为你提供一个坚实的基础,来使用高级神经网络。
torch
API 非常优雅且易于可视化。之后,你可以使用这些张量来训练神经网络(这是我将在本系列的下一篇博客中展示的内容)。此外,在学习过程中稍微掌握一些线性代数将对学习其他数据科学和机器学习算法极有帮助。
这篇文章的灵感来源于www.learnpytorch.io/
——这是一个关于 Pytorch 的优秀免费课程,我强烈推荐。在DareData我们参与了许多深度学习项目,我不能强调这门课程对我们培训人员学习这种机器学习范式及其相关框架的重要性。
在下一篇文章中,我们将探讨如何使用torch
训练线性回归——敬请期待!
如果你想参加我的 Python 课程,随时加入 我的 16 小时 Python 课程 (初学者完整 Python 训练营)。我的 Python 课程适合初学者/中级开发者,我非常希望你能加入我的课堂!
Python 初学者训练营 — 图片由作者提供