Python 机器学习秘籍第二版(三)
原文:
annas-archive.org/md5/343c5e6c97737f77853e89eacb95df75译者:飞龙
第七章:处理日期和时间
7.0 引言
处理日期和时间(datetime),例如特定销售的时间或公共卫生统计的日期,在机器学习预处理中经常遇到。纵向数据(或时间序列数据)是重复收集同一变量的数据,随时间点变化。在本章中,我们将构建处理时间序列数据的策略工具箱,包括处理时区和创建滞后时间特征。具体来说,我们将关注 pandas 库中的时间序列工具,该工具集集中了许多其他通用库(如 datetime)的功能。
7.1 字符串转换为日期
问题
给定一个表示日期和时间的字符串向量,你想要将它们转换为时间序列数据。
解决方案
使用 pandas 的 to_datetime,并指定日期和/或时间的格式在 format 参数中:
# Load libraries
import numpy as np
import pandas as pd
# Create strings
date_strings = np.array(['03-04-2005 11:35 PM',
'23-05-2010 12:01 AM',
'04-09-2009 09:09 PM'])
# Convert to datetimes
[pd.to_datetime(date, format='%d-%m-%Y %I:%M %p') for date in date_strings]
[Timestamp('2005-04-03 23:35:00'),
Timestamp('2010-05-23 00:01:00'),
Timestamp('2009-09-04 21:09:00')]
我们也可能想要向 errors 参数添加一个参数来处理问题:
# Convert to datetimes
[pd.to_datetime(date, format="%d-%m-%Y %I:%M %p", errors="coerce")
for date in date_strings]
[Timestamp('2005-04-03 23:35:00'),
Timestamp('2010-05-23 00:01:00'),
Timestamp('2009-09-04 21:09:00')]
如果 errors="coerce",则任何发生的问题不会引发错误(默认行为),而是将导致错误的值设置为 NaT(缺失值)。这允许你通过将其填充为 null 值来处理异常值,而不是为数据中的每个记录进行故障排除。
讨论
当日期和时间以字符串形式提供时,我们需要将它们转换为 Python 能够理解的数据类型。虽然有许多用于将字符串转换为日期时间的 Python 工具,但在其他示例中使用 pandas 后,我们可以使用 to_datetime 进行转换。使用字符串表示日期和时间的一个障碍是,字符串的格式在数据源之间可能有很大的变化。例如,一个日期向量可能将 2015 年 3 月 23 日表示为“03-23-15”,而另一个可能使用“3|23|2015”。我们可以使用 format 参数来指定字符串的确切格式。以下是一些常见的日期和时间格式代码:
| 代码 | 描述 | 示例 |
|---|---|---|
%Y | 完整年份 | 2001 |
%m | 带零填充的月份 | 04 |
%d | 带零填充的日期 | 09 |
%I | 带零填充的小时(12 小时制) | 02 |
%p | 上午或下午 | AM |
%M | 带零填充的分钟数 | 05 |
%S | 带零填充的秒数 | 09 |
参见
7.2 处理时区
问题
你有时间序列数据,想要添加或更改时区信息。
解决方案
除非指定,否则 pandas 对象没有时区。我们可以在创建时使用 tz 添加时区:
# Load library
import pandas as pd
# Create datetime
pd.Timestamp('2017-05-01 06:00:00', tz='Europe/London')
Timestamp('2017-05-01 06:00:00+0100', tz='Europe/London')
我们可以使用 tz_localize 为先前创建的日期时间添加时区:
# Create datetime
date = pd.Timestamp('2017-05-01 06:00:00')
# Set time zone
date_in_london = date.tz_localize('Europe/London')
# Show datetime
date_in_london
Timestamp('2017-05-01 06:00:00+0100', tz='Europe/London')
我们也可以转换为不同的时区:
# Change time zone
date_in_london.tz_convert('Africa/Abidjan')
Timestamp('2017-05-01 05:00:00+0000', tz='Africa/Abidjan')
最后,pandas 的 Series 对象可以对每个元素应用 tz_localize 和 tz_convert:
# Create three dates
dates = pd.Series(pd.date_range('2/2/2002', periods=3, freq='M'))
# Set time zone
dates.dt.tz_localize('Africa/Abidjan')
0 2002-02-28 00:00:00+00:00
1 2002-03-31 00:00:00+00:00
2 2002-04-30 00:00:00+00:00
dtype: datetime64[ns, Africa/Abidjan]
讨论
pandas 支持两组表示时区的字符串;然而,建议使用pytz库的字符串。我们可以通过导入all_timezones查看表示时区的所有字符串:
# Load library
from pytz import all_timezones
# Show two time zones
all_timezones[0:2]
['Africa/Abidjan', 'Africa/Accra']
7.3 选择日期和时间
问题
您有一个日期向量,想要选择一个或多个。
解决方案
使用两个布尔条件作为开始和结束日期:
# Load library
import pandas as pd
# Create data frame
dataframe = pd.DataFrame()
# Create datetimes
dataframe['date'] = pd.date_range('1/1/2001', periods=100000, freq='H')
# Select observations between two datetimes
dataframe[(dataframe['date'] > '2002-1-1 01:00:00') &
(dataframe['date'] <= '2002-1-1 04:00:00')]
| 日期 | |
|---|---|
| 8762 | 2002-01-01 02:00:00 |
| 8763 | 2002-01-01 03:00:00 |
| 8764 | 2002-01-01 04:00:00 |
或者,我们可以将日期列设置为 DataFrame 的索引,然后使用loc进行切片:
# Set index
dataframe = dataframe.set_index(dataframe['date'])
# Select observations between two datetimes
dataframe.loc['2002-1-1 01:00:00':'2002-1-1 04:00:00']
| 日期 | 日期 |
|---|---|
| 2002-01-01 01:00:00 | 2002-01-01 01:00:00 |
| 2002-01-01 02:00:00 | 2002-01-01 02:00:00 |
| 2002-01-01 03:00:00 | 2002-01-01 03:00:00 |
| 2002-01-01 04:00:00 | 2002-01-01 04:00:00 |
讨论
是否使用布尔条件或索引切片取决于具体情况。如果我们想要进行一些复杂的时间序列操作,将日期列设置为 DataFrame 的索引可能值得开销,但如果我们只想进行简单的数据处理,布尔条件可能更容易。
7.4 将日期数据拆分为多个特征
问题
您有一个包含日期和时间的列,并且希望创建年、月、日、小时和分钟的特征。
解决方案
使用 pandas Series.dt中的时间属性:
# Load library
import pandas as pd
# Create data frame
dataframe = pd.DataFrame()
# Create five dates
dataframe['date'] = pd.date_range('1/1/2001', periods=150, freq='W')
# Create features for year, month, day, hour, and minute
dataframe['year'] = dataframe['date'].dt.year
dataframe['month'] = dataframe['date'].dt.month
dataframe['day'] = dataframe['date'].dt.day
dataframe['hour'] = dataframe['date'].dt.hour
dataframe['minute'] = dataframe['date'].dt.minute
# Show three rows
dataframe.head(3)
| 日期 | 年 | 月 | 日 | 小时 | 分钟 | |
|---|---|---|---|---|---|---|
| 0 | 2001-01-07 | 2001 | 1 | 7 | 0 | 0 |
| 1 | 2001-01-14 | 2001 | 1 | 14 | 0 | 0 |
| 2 | 2001-01-21 | 2001 | 1 | 21 | 0 | 0 |
讨论
有时将日期列分解为各个组成部分会很有用。例如,我们可能希望有一个特征仅包括观察年份,或者我们可能只想考虑某些观测的月份,以便无论年份如何都可以比较它们。
7.5 计算日期之间的差异
问题
您有两个日期时间特征,想要计算每个观测值之间的时间间隔。
解决方案
使用 pandas 减去两个日期特征:
# Load library
import pandas as pd
# Create data frame
dataframe = pd.DataFrame()
# Create two datetime features
dataframe['Arrived'] = [pd.Timestamp('01-01-2017'), pd.Timestamp('01-04-2017')]
dataframe['Left'] = [pd.Timestamp('01-01-2017'), pd.Timestamp('01-06-2017')]
# Calculate duration between features
dataframe['Left'] - dataframe['Arrived']
0 0 days
1 2 days
dtype: timedelta64[ns]
我们经常希望删除days的输出,仅保留数值:
# Calculate duration between features
pd.Series(delta.days for delta in (dataframe['Left'] - dataframe['Arrived']))
0 0
1 2
dtype: int64
讨论
有时我们需要的特征是两个时间点之间的变化(delta)。例如,我们可能有客户入住和退房的日期,但我们想要的特征是客户住店的持续时间。pandas 使用TimeDelta数据类型使得这种计算变得简单。
参见
7.6 编码星期几
问题
您有一个日期向量,想知道每个日期的星期几。
解决方案
使用 pandas Series.dt方法的day_name():
# Load library
import pandas as pd
# Create dates
dates = pd.Series(pd.date_range("2/2/2002", periods=3, freq="M"))
# Show days of the week
dates.dt.day_name()
0 Thursday
1 Sunday
2 Tuesday
dtype: object
如果我们希望输出为数值形式,因此更适合作为机器学习特征使用,可以使用weekday,其中星期几表示为整数(星期一为 0)。
# Show days of the week
dates.dt.weekday
0 3
1 6
2 1
dtype: int64
讨论
如果我们想要比较过去三年每个星期日的总销售额,知道星期几可能很有帮助。pandas 使得创建包含星期信息的特征向量变得很容易。
另请参阅
7.7 创建滞后特征
问题
你想要创建一个滞后n个时间段的特征。
解决方案
使用 pandas 的shift方法:
# Load library
import pandas as pd
# Create data frame
dataframe = pd.DataFrame()
# Create data
dataframe["dates"] = pd.date_range("1/1/2001", periods=5, freq="D")
dataframe["stock_price"] = [1.1,2.2,3.3,4.4,5.5]
# Lagged values by one row
dataframe["previous_days_stock_price"] = dataframe["stock_price"].shift(1)
# Show data frame
dataframe
| 日期 | 股票价格 | 前几天的股票价格 | |
|---|---|---|---|
| 0 | 2001-01-01 | 1.1 | NaN |
| 1 | 2001-01-02 | 2.2 | 1.1 |
| 2 | 2001-01-03 | 3.3 | 2.2 |
| 3 | 2001-01-04 | 4.4 | 3.3 |
| 4 | 2001-01-05 | 5.5 | 4.4 |
讨论
数据往往基于定期间隔的时间段(例如每天、每小时、每三小时),我们有兴趣使用过去的值来进行预测(通常称为滞后一个特征)。例如,我们可能想要使用前一天的价格来预测股票的价格。使用 pandas,我们可以使用shift将值按一行滞后,创建一个包含过去值的新特征。
在我们的解决方案中,previous_days_stock_price的第一行是一个缺失值,因为没有先前的stock_price值。
7.8 使用滚动时间窗口
问题
给定时间序列数据,你想要计算一个滚动时间的统计量。
解决方案
使用 pandas DataFrame rolling方法:
# Load library
import pandas as pd
# Create datetimes
time_index = pd.date_range("01/01/2010", periods=5, freq="M")
# Create data frame, set index
dataframe = pd.DataFrame(index=time_index)
# Create feature
dataframe["Stock_Price"] = [1,2,3,4,5]
# Calculate rolling mean
dataframe.rolling(window=2).mean()
| 股票价格 | |
|---|---|
| 2010-01-31 | NaN |
| 2010-02-28 | 1.5 |
| 2010-03-31 | 2.5 |
| 2010-04-30 | 3.5 |
| 2010-05-31 | 4.5 |
讨论
滚动(也称为移动)时间窗口在概念上很简单,但一开始可能难以理解。假设我们有一个股票价格的月度观察数据。经常有用的是设定一个特定月数的时间窗口,然后在观察数据上移动,计算时间窗口内所有观察数据的统计量。
例如,如果我们有一个三个月的时间窗口,并且想要一个滚动均值,我们可以计算:
-
mean(一月, 二月, 三月) -
mean(二月, 三月, 四月) -
mean(三月, 四月, 五月) -
等等
另一种表达方式:我们的三个月时间窗口“漫步”过观测值,在每一步计算窗口的平均值。
pandas 的rolling方法允许我们通过使用window指定窗口大小,然后快速计算一些常见统计量,包括最大值(max())、均值(mean())、值的数量(count())和滚动相关性(corr())。
滚动均值通常用于平滑时间序列数据,因为使用整个时间窗口的均值可以抑制短期波动的影响。
另请参阅
7.9 处理时间序列中的缺失数据
问题
你在时间序列数据中有缺失值。
解决方案
除了前面讨论的缺失数据策略之外,当我们有时间序列数据时,我们可以使用插值来填补由缺失值引起的间隙:
# Load libraries
import pandas as pd
import numpy as np
# Create date
time_index = pd.date_range("01/01/2010", periods=5, freq="M")
# Create data frame, set index
dataframe = pd.DataFrame(index=time_index)
# Create feature with a gap of missing values
dataframe["Sales"] = [1.0,2.0,np.nan,np.nan,5.0]
# Interpolate missing values
dataframe.interpolate()
| 销售 | |
|---|---|
| 2010-01-31 | 1.0 |
| 2010-02-28 | 2.0 |
| 2010-03-31 | 3.0 |
| 2010-04-30 | 4.0 |
| 2010-05-31 | 5.0 |
或者,我们可以用最后一个已知值替换缺失值(即向前填充):
# Forward fill
dataframe.ffill()
| 销售 | |
|---|---|
| 2010-01-31 | 1.0 |
| 2010-02-28 | 2.0 |
| 2010-03-31 | 2.0 |
| 2010-04-30 | 2.0 |
| 2010-05-31 | 5.0 |
我们还可以用最新的已知值替换缺失值(即向后填充):
# Backfill
dataframe.bfill()
| 销售 | |
|---|---|
| 2010-01-31 | 1.0 |
| 2010-02-28 | 2.0 |
| 2010-03-31 | 5.0 |
| 2010-04-30 | 5.0 |
| 2010-05-31 | 5.0 |
讨论
插值是一种填补由缺失值引起的间隙的技术,实质上是在已知值之间绘制一条直线或曲线,并使用该线或曲线来预测合理的值。当时间间隔恒定、数据不易受到嘈杂波动影响、缺失值引起的间隙较小时,插值尤为有用。例如,在我们的解决方案中,两个缺失值之间的间隙由 2.0 和 5.0 所界定。通过在 2.0 和 5.0 之间拟合一条直线,我们可以推测出 3.0 到 4.0 之间的两个缺失值的合理值。
如果我们认为两个已知点之间的线是非线性的,我们可以使用 interpolate 的 method 参数来指定插值方法:
# Interpolate missing values
dataframe.interpolate(method="quadratic")
| 销售 | |
|---|---|
| 2010-01-31 | 1.000000 |
| 2010-02-28 | 2.000000 |
| 2010-03-31 | 3.059808 |
| 2010-04-30 | 4.038069 |
| 2010-05-31 | 5.000000 |
最后,我们可能有大量的缺失值间隙,但不希望在整个间隙内插值。在这些情况下,我们可以使用 limit 限制插值值的数量,并使用 limit_direction 来设置是从间隙前的最后已知值向前插值,还是反之:
# Interpolate missing values
dataframe.interpolate(limit=1, limit_direction="forward")
| 销售 | |
|---|---|
| 2010-01-31 | 1.0 |
| 2010-02-28 | 2.0 |
| 2010-03-31 | 3.0 |
| 2010-04-30 | NaN |
| 2010-05-31 | 5.0 |
向后填充和向前填充是一种朴素插值的形式,其中我们从已知值开始绘制一条平直线,并用它来填充缺失值。与插值相比,向后填充和向前填充的一个(轻微)优势在于它们不需要在缺失值的两侧都有已知值。
第八章:处理图像
8.0 介绍
图像分类是机器学习中最激动人心的领域之一。计算机从图像中识别模式和物体的能力是我们工具箱中非常强大的工具。然而,在将机器学习应用于图像之前,我们通常需要将原始图像转换为我们的学习算法可用的特征。与文本数据一样,也有许多预训练的分类器可用于图像,我们可以使用这些分类器来提取我们自己模型的输入中感兴趣的特征或对象。
为了处理图像,我们将主要使用开源计算机视觉库(OpenCV)。虽然市面上有许多优秀的库,但 OpenCV 是处理图像最流行和文档最完善的库。安装时可能会遇到一些挑战,但如果遇到问题,网上有很多指南。本书特别使用的是 opencv-python-headless==4.7.0.68。您也可以使用 Python Cookbook Runner 中的 ML 确保所有命令可复现。
在本章中,我们将使用一组图像作为示例,可以从 GitHub 下载。
8.1 加载图像
问题
您想要加载一幅图像进行预处理。
解决方案
使用 OpenCV 的 imread:
# Load libraries
import cv2
import numpy as np
from matplotlib import pyplot as plt
# Load image as grayscale
image = cv2.imread("images/plane.jpg", cv2.IMREAD_GRAYSCALE)
如果我们想查看图像,我们可以使用 Python 绘图库 Matplotlib:
# Show image
plt.imshow(image, cmap="gray"), plt.axis("off")
plt.show()
讨论
从根本上讲,图像是数据,当我们使用imread时,我们将该数据转换为我们非常熟悉的数据类型——一个 NumPy 数组:
# Show data type
type(image)
numpy.ndarray
我们已将图像转换为一个矩阵,其元素对应于各个像素。我们甚至可以查看矩阵的实际值:
# Show image data
image
array([[140, 136, 146, ..., 132, 139, 134],
[144, 136, 149, ..., 142, 124, 126],
[152, 139, 144, ..., 121, 127, 134],
...,
[156, 146, 144, ..., 157, 154, 151],
[146, 150, 147, ..., 156, 158, 157],
[143, 138, 147, ..., 156, 157, 157]], dtype=uint8)
我们图像的分辨率是 3600 × 2270,正好是我们矩阵的确切尺寸:
# Show dimensions
image.shape
(2270, 3600)
矩阵中的每个元素实际上表示什么?在灰度图像中,单个元素的值是像素强度。强度值从黑色(0)到白色(255)变化。例如,我们图像左上角像素的强度值为 140:
# Show first pixel
image[0,0]
140
在表示彩色图像的矩阵中,每个元素实际上包含三个值,分别对应蓝色、绿色和红色的值(BGR):
# Load image in color
image_bgr = cv2.imread("images/plane.jpg", cv2.IMREAD_COLOR)
# Show pixel
image_bgr[0,0]
array([195, 144, 111], dtype=uint8)
有一个小细节:默认情况下,OpenCV 使用 BGR,但许多图像应用程序——包括 Matplotlib——使用红色、绿色、蓝色(RGB),这意味着红色和蓝色值被交换了。为了在 Matplotlib 中正确显示 OpenCV 彩色图像,我们首先需要将颜色转换为 RGB(对于硬拷贝读者,没有彩色图像我们深感抱歉)。
# Convert to RGB
image_rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB)
# Show image
plt.imshow(image_rgb), plt.axis("off")
plt.show()
参见
8.2 保存图像
问题
您想要保存一幅图像进行预处理。
解决方案
使用 OpenCV 的 imwrite:
# Load libraries
import cv2
import numpy as np
from matplotlib import pyplot as plt
# Load image as grayscale
image = cv2.imread("images/plane.jpg", cv2.IMREAD_GRAYSCALE)
# Save image
cv2.imwrite("images/plane_new.jpg", image)
True
讨论
OpenCV 的imwrite将图像保存到指定的文件路径。图像的格式由文件名的扩展名(.jpg,.png等)定义。要注意的一个行为是:imwrite会覆盖现有文件而不输出错误或要求确认。
8.3 调整图像大小
问题
您想要调整图像的大小以进行进一步的预处理。
解决方案
使用resize来改变图像的大小:
# Load libraries
import cv2
import numpy as np
from matplotlib import pyplot as plt
# Load image as grayscale
image = cv2.imread("images/plane_256x256.jpg", cv2.IMREAD_GRAYSCALE)
# Resize image to 50 pixels by 50 pixels
image_50x50 = cv2.resize(image, (50, 50))
# View image
plt.imshow(image_50x50, cmap="gray"), plt.axis("off")
plt.show()
讨论
调整图像大小是图像预处理中的常见任务,有两个原因。首先,图像以各种形状和大小出现,为了作为特征可用,图像必须具有相同的尺寸。标准化(调整大小)图像的过程会丢失较大图像中存在的一些信息,就像飞机图片中所看到的那样。图像是信息的矩阵,当我们减小图像的尺寸时,我们减少了该矩阵及其所包含信息的大小。其次,机器学习可能需要成千上万张图像。当这些图像非常大时,它们会占用大量内存,通过调整它们的大小,我们可以显著减少内存使用量。机器学习中常见的一些图像尺寸包括 32 × 32、64 × 64、96 × 96 和 256 × 256。总的来说,我们选择的图像调整方法往往是模型统计性能与训练计算成本之间的权衡。出于这个原因,Pillow 库提供了许多调整图像大小的选项。
8.4 裁剪图像
问题
您想要删除图像的外部部分以更改其尺寸。
解决方案
图像被编码为二维 NumPy 数组,因此我们可以通过切片数组轻松地裁剪图像:
# Load libraries
import cv2
import numpy as np
from matplotlib import pyplot as plt
# Load image in grayscale
image = cv2.imread("images/plane_256x256.jpg", cv2.IMREAD_GRAYSCALE)
# Select first half of the columns and all rows
image_cropped = image[:,:128]
# Show image
plt.imshow(image_cropped, cmap="gray"), plt.axis("off")
plt.show()
讨论
由于 OpenCV 将图像表示为元素的矩阵,通过选择我们想保留的行和列,我们可以轻松地裁剪图像。如果我们知道我们只想保留每个图像的特定部分,裁剪可以特别有用。例如,如果我们的图像来自固定的安全摄像机,我们可以裁剪所有图像,使它们仅包含感兴趣的区域。
参见
8.5 模糊图像
问题
您想要使图像变得平滑。
解决方案
为了模糊图像,每个像素被转换为其邻居的平均值。数学上将这个邻居和操作表示为一个核(如果你不知道核是什么也不用担心)。这个核的大小决定了模糊的程度,较大的核产生更平滑的图像。在这里,我们通过对每个像素周围的 5 × 5 核的值取平均来模糊图像:
# Load libraries
import cv2
import numpy as np
from matplotlib import pyplot as plt
# Load image as grayscale
image = cv2.imread("images/plane_256x256.jpg", cv2.IMREAD_GRAYSCALE)
# Blur image
image_blurry = cv2.blur(image, (5,5))
# Show image
plt.imshow(image_blurry, cmap="gray"), plt.axis("off")
plt.show()
为了突出显示核大小的效果,这里是使用 100 × 100 核进行的相同模糊处理的图像:
# Blur image
image_very_blurry = cv2.blur(image, (100,100))
# Show image
plt.imshow(image_very_blurry, cmap="gray"), plt.xticks([]), plt.yticks([])
plt.show()
讨论
卷积核在图像处理中被广泛使用,从锐化到边缘检测等方面,本章节将反复讨论。我们使用的模糊核如下所示:
# Create kernel
kernel = np.ones((5,5)) / 25.0
# Show kernel
kernel
array([[ 0.04, 0.04, 0.04, 0.04, 0.04],
[ 0.04, 0.04, 0.04, 0.04, 0.04],
[ 0.04, 0.04, 0.04, 0.04, 0.04],
[ 0.04, 0.04, 0.04, 0.04, 0.04],
[ 0.04, 0.04, 0.04, 0.04, 0.04]])
核心元素在内核中是被检查的像素,而其余元素是其邻居。由于所有元素具有相同的值(归一化为总和为 1),因此每个元素对感兴趣像素的结果值都有相同的影响力。我们可以使用filter2D手动将内核应用于图像,以产生类似的模糊效果:
# Apply kernel
image_kernel = cv2.filter2D(image, -1, kernel)
# Show image
plt.imshow(image_kernel, cmap="gray"), plt.xticks([]), plt.yticks([])
plt.show()
参见
8.6 锐化图像
问题
您想要锐化图像。
解决方案
创建一个突出显示目标像素的内核。然后使用filter2D将其应用于图像:
# Load libraries
import cv2
import numpy as np
from matplotlib import pyplot as plt
# Load image as grayscale
image = cv2.imread("images/plane_256x256.jpg", cv2.IMREAD_GRAYSCALE)
# Create kernel
kernel = np.array([[0, -1, 0],
[-1, 5,-1],
[0, -1, 0]])
# Sharpen image
image_sharp = cv2.filter2D(image, -1, kernel)
# Show image
plt.imshow(image_sharp, cmap="gray"), plt.axis("off")
plt.show()
讨论
锐化的工作原理与模糊类似,但不同于使用内核来平均周围值,我们构建了一个内核来突出像素本身。其结果效果使得边缘处的对比更加明显。
8.7 增强对比度
问题
我们希望增加图像中像素之间的对比度。
解决方案
直方图均衡化 是一种图像处理工具,可以使物体和形状更加突出。当我们有一个灰度图像时,可以直接在图像上应用 OpenCV 的equalizeHist:
# Load libraries
import cv2
import numpy as np
from matplotlib import pyplot as plt
# Load image
image = cv2.imread("images/plane_256x256.jpg", cv2.IMREAD_GRAYSCALE)
# Enhance image
image_enhanced = cv2.equalizeHist(image)
# Show image
plt.imshow(image_enhanced, cmap="gray"), plt.axis("off")
plt.show()
然而,当我们有一幅彩色图像时,我们首先需要将图像转换为 YUV 颜色格式。Y 代表亮度,U 和 V 表示颜色。转换后,我们可以将equalizeHist应用于图像,然后再转换回 BGR 或 RGB(对于只有黑白图像的读者表示抱歉):
# Load image
image_bgr = cv2.imread("images/plane.jpg")
# Convert to YUV
image_yuv = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2YUV)
# Apply histogram equalization
image_yuv[:, :, 0] = cv2.equalizeHist(image_yuv[:, :, 0])
# Convert to RGB
image_rgb = cv2.cvtColor(image_yuv, cv2.COLOR_YUV2RGB)
# Show image
plt.imshow(image_rgb), plt.axis("off")
plt.show()
讨论
虽然详细解释直方图均衡化的工作原理超出了本书的范围,简短的解释是它转换图像,使其使用更广泛的像素强度范围。
虽然生成的图像通常看起来不够“真实”,但我们需要记住,图像只是底层数据的视觉表示。如果直方图均衡化能够使感兴趣的对象与其他对象或背景更易于区分(这并非总是如此),那么它可以成为我们图像预处理流程中的有价值的补充。
8.8 分离颜色
问题
您想要在图像中隔离一种颜色。
解决方案
定义一个颜色范围,然后将掩码应用于图像(对于只有黑白图像的读者表示抱歉):
# Load libraries
import cv2
import numpy as np
from matplotlib import pyplot as plt
# Load image
image_bgr = cv2.imread('images/plane_256x256.jpg')
# Convert BGR to HSV
image_hsv = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2HSV)
# Define range of blue values in HSV
lower_blue = np.array([50,100,50])
upper_blue = np.array([130,255,255])
# Create mask
mask = cv2.inRange(image_hsv, lower_blue, upper_blue)
# Mask image
image_bgr_masked = cv2.bitwise_and(image_bgr, image_bgr, mask=mask)
# Convert BGR to RGB
image_rgb = cv2.cvtColor(image_bgr_masked, cv2.COLOR_BGR2RGB)
# Show image
plt.imshow(image_rgb), plt.axis("off")
plt.show()
讨论
在 OpenCV 中隔离颜色是直接的。首先我们将图像转换为 HSV(色调、饱和度和值)。其次,我们定义我们想要隔离的值范围,这可能是最困难和耗时的部分。第三,我们为图像创建一个掩码。图像掩码是一种常见的技术,旨在提取感兴趣的区域。在这种情况下,我们的掩码仅保留白色区域:
# Show image
plt.imshow(mask, cmap='gray'), plt.axis("off")
plt.show()
最后,我们使用 bitwise_and 将掩码应用于图像,并将其转换为我们期望的输出格式。
8.9 图像二值化
问题
给定一张图像,您想要输出一个简化版本。
解决方案
阈值化 是将像素强度大于某个值的像素设置为白色,小于该值的像素设置为黑色的过程。更高级的技术是 自适应阈值化,其中像素的阈值由其邻域的像素强度决定。当图像中不同区域的光照条件发生变化时,这可能会有所帮助:
# Load libraries
import cv2
import numpy as np
from matplotlib import pyplot as plt
# Load image as grayscale
image_grey = cv2.imread("images/plane_256x256.jpg", cv2.IMREAD_GRAYSCALE)
# Apply adaptive thresholding
max_output_value = 255
neighborhood_size = 99
subtract_from_mean = 10
image_binarized = cv2.adaptiveThreshold(image_grey,
max_output_value,
cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY,
neighborhood_size,
subtract_from_mean)
# Show image
plt.imshow(image_binarized, cmap="gray"), plt.axis("off")
plt.show()
讨论
对图像进行二值化的过程涉及将灰度图像转换为其黑白形式。我们的解决方案在 adaptiveThreshold 中有四个重要参数。max_output_value 简单地确定输出像素强度的最大值。cv2.ADAPTIVE_THRESH_GAUSSIAN_C 将像素的阈值设置为其相邻像素强度的加权和。权重由高斯窗口确定。或者,我们可以将阈值简单地设置为相邻像素的平均值,使用 cv2.ADAPTIVE_THRESH_MEAN_C:
# Apply cv2.ADAPTIVE_THRESH_MEAN_C
image_mean_threshold = cv2.adaptiveThreshold(image_grey,
max_output_value,
cv2.ADAPTIVE_THRESH_MEAN_C,
cv2.THRESH_BINARY,
neighborhood_size,
subtract_from_mean)
# Show image
plt.imshow(image_mean_threshold, cmap="gray"), plt.axis("off")
plt.show()
最后两个参数是块大小(用于确定像素阈值的邻域大小)和从计算阈值中减去的常数(用于手动微调阈值)。
阈值化的一个主要好处是 去噪 图像 —— 仅保留最重要的元素。例如,经常将阈值应用于印刷文本的照片,以隔离页面上的字母。
8.10 移除背景
问题
您想要隔离图像的前景。
解决方案
在所需前景周围标记一个矩形,然后运行 GrabCut 算法:
# Load library
import cv2
import numpy as np
from matplotlib import pyplot as plt
# Load image and convert to RGB
image_bgr = cv2.imread('images/plane_256x256.jpg')
image_rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB)
# Rectangle values: start x, start y, width, height
rectangle = (0, 56, 256, 150)
# Create initial mask
mask = np.zeros(image_rgb.shape[:2], np.uint8)
# Create temporary arrays used by grabCut
bgdModel = np.zeros((1, 65), np.float64)
fgdModel = np.zeros((1, 65), np.float64)
# Run grabCut
cv2.grabCut(image_rgb, # Our image
mask, # The Mask
rectangle, # Our rectangle
bgdModel, # Temporary array for background
fgdModel, # Temporary array for background
5, # Number of iterations
cv2.GC_INIT_WITH_RECT) # Initiative using our rectangle
# Create mask where sure and likely backgrounds set to 0, otherwise 1
mask_2 = np.where((mask==2) | (mask==0), 0, 1).astype('uint8')
# Multiply image with new mask to subtract background
image_rgb_nobg = image_rgb * mask_2[:, :, np.newaxis]
# Show image
plt.imshow(image_rgb_nobg), plt.axis("off")
plt.show()
讨论
我们首先注意到,即使 GrabCut 做得相当不错,图像中仍然有一些背景区域。我们可以回去手动标记这些区域为背景,但在现实世界中,我们有成千上万张图片,逐个手动修复它们是不可行的。因此,我们最好接受图像数据仍然会包含一些背景噪声。
在我们的解决方案中,我们首先在包含前景的区域周围标记一个矩形。GrabCut 假定这个矩形外的所有内容都是背景,并利用这些信息来推断出正方形内部可能是背景的区域。(要了解算法如何做到这一点,请参阅Itay Blumenthal的解释。)然后创建一个标记不同明确/可能背景/前景区域的掩码:
# Show mask
plt.imshow(mask, cmap='gray'), plt.axis("off")
plt.show()
黑色区域是矩形外部被假定为明确背景的区域。灰色区域是 GrabCut 认为可能是背景的区域,而白色区域则是可能是前景的区域。
然后使用该掩码创建第二个掩码,将黑色和灰色区域合并:
# Show mask
plt.imshow(mask_2, cmap='gray'), plt.axis("off")
plt.show()
然后将第二个掩码应用于图像,以便仅保留前景。
8.11 检测边缘
问题
您希望在图像中找到边缘。
解决方案
使用像 Canny 边缘检测器这样的边缘检测技术:
# Load libraries
import cv2
import numpy as np
from matplotlib import pyplot as plt
# Load image as grayscale
image_gray = cv2.imread("images/plane_256x256.jpg", cv2.IMREAD_GRAYSCALE)
# Calculate median intensity
median_intensity = np.median(image_gray)
# Set thresholds to be one standard deviation above and below median intensity
lower_threshold = int(max(0, (1.0 - 0.33) * median_intensity))
upper_threshold = int(min(255, (1.0 + 0.33) * median_intensity))
# Apply Canny edge detector
image_canny = cv2.Canny(image_gray, lower_threshold, upper_threshold)
# Show image
plt.imshow(image_canny, cmap="gray"), plt.axis("off")
plt.show()
讨论
边缘检测是计算机视觉中的一个主要话题。边缘非常重要,因为它们是信息量最大的区域。例如,在我们的图像中,一片天空看起来非常相似,不太可能包含独特或有趣的信息。然而,背景天空与飞机相遇的区域包含大量信息(例如,物体的形状)。边缘检测允许我们去除低信息量的区域,并分离包含最多信息的图像区域。
边缘检测有许多技术(Sobel 滤波器、Laplacian 边缘检测器等)。然而,我们的解决方案使用常用的 Canny 边缘检测器。Canny 检测器的工作原理对本书来说过于详细,但有一点我们需要解决。Canny 检测器需要两个参数来指定低梯度阈值和高梯度阈值。低和高阈值之间的潜在边缘像素被认为是弱边缘像素,而高于高阈值的像素被认为是强边缘像素。OpenCV 的Canny方法包括所需的低和高阈值参数。在我们的解决方案中,我们将低和高阈值设置为图像中位数下方和上方的一个标准偏差。然而,在运行Canny处理整个图像集之前,我们经常通过手动在几幅图像上试错来确定一对好的低和高阈值,以获得更好的结果。
参见
8.12 检测角点
问题
您希望检测图像中的角点。
解决方案
使用 OpenCV 的 Harris 角检测器cornerHarris的实现:
# Load libraries
import cv2
import numpy as np
from matplotlib import pyplot as plt
# Load image
image_bgr = cv2.imread("images/plane_256x256.jpg")
image_gray = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2GRAY)
image_gray = np.float32(image_gray)
# Set corner detector parameters
block_size = 2
aperture = 29
free_parameter = 0.04
# Detect corners
detector_responses = cv2.cornerHarris(image_gray,
block_size,
aperture,
free_parameter)
# Large corner markers
detector_responses = cv2.dilate(detector_responses, None)
# Only keep detector responses greater than threshold, mark as white
threshold = 0.02
image_bgr[detector_responses >
threshold *
detector_responses.max()] = [255,255,255]
# Convert to grayscale
image_gray = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2GRAY)
# Show image
plt.imshow(image_gray, cmap="gray"), plt.axis("off")
plt.show()
讨论
Harris 角点检测器是一种常用的检测两条边交点的方法。我们对检测角点的兴趣与检测边缘的原因相同:角点是信息量很高的点。Harris 角点检测器的完整解释可以在本文末尾的外部资源中找到,但简化的解释是它寻找窗口(也称为邻域或补丁),在这些窗口中,窗口的微小移动(想象抖动窗口)导致窗口内像素内容的显著变化。cornerHarris包含三个重要参数,我们可以用它来控制检测到的边缘。首先,block_size是用于角点检测的每个像素周围的邻域的大小。其次,aperture是使用的 Sobel 核的大小(如果你不知道是什么也没关系),最后有一个自由参数,较大的值对应于识别更软的角点。
输出是一个灰度图像,描述了潜在的角点:
# Show potential corners
plt.imshow(detector_responses, cmap='gray'), plt.axis("off")
plt.show()
然后我们应用阈值处理,仅保留最可能的角点。或者,我们可以使用类似的检测器,即 Shi-Tomasi 角点检测器,它的工作方式与 Harris 检测器类似(goodFeaturesToTrack),用于识别固定数量的强角点。goodFeaturesToTrack有三个主要参数——要检测的角点数目,角点的最小质量(0 到 1 之间),以及角点之间的最小欧氏距离:
# Load images
image_bgr = cv2.imread('images/plane_256x256.jpg')
image_gray = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2GRAY)
# Number of corners to detect
corners_to_detect = 10
minimum_quality_score = 0.05
minimum_distance = 25
# Detect corners
corners = cv2.goodFeaturesToTrack(image_gray,
corners_to_detect,
minimum_quality_score,
minimum_distance)
corners = np.int16(corners)
# Draw white circle at each corner
for corner in corners:
x, y = corner[0]
cv2.circle(image_bgr, (x,y), 10, (255,255,255), -1)
# Convert to grayscale
image_rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2GRAY)
# Show image
plt.imshow(image_rgb, cmap='gray'), plt.axis("off")
plt.show()
另请参阅
8.13 为机器学习创建特征
问题
您希望将图像转换为机器学习的观察结果。
解决方案
使用 NumPy 的flatten将包含图像数据的多维数组转换为包含观察值的向量:
# Load libraries
import cv2
import numpy as np
from matplotlib import pyplot as plt
# Load image as grayscale
image = cv2.imread("images/plane_256x256.jpg", cv2.IMREAD_GRAYSCALE)
# Resize image to 10 pixels by 10 pixels
image_10x10 = cv2.resize(image, (10, 10))
# Convert image data to one-dimensional vector
image_10x10.flatten()
array([133, 130, 130, 129, 130, 129, 129, 128, 128, 127, 135, 131, 131,
131, 130, 130, 129, 128, 128, 128, 134, 132, 131, 131, 130, 129,
129, 128, 130, 133, 132, 158, 130, 133, 130, 46, 97, 26, 132,
143, 141, 36, 54, 91, 9, 9, 49, 144, 179, 41, 142, 95,
32, 36, 29, 43, 113, 141, 179, 187, 141, 124, 26, 25, 132,
135, 151, 175, 174, 184, 143, 151, 38, 133, 134, 139, 174, 177,
169, 174, 155, 141, 135, 137, 137, 152, 169, 168, 168, 179, 152,
139, 136, 135, 137, 143, 159, 166, 171, 175], dtype=uint8)
讨论
图像呈像素网格的形式呈现。如果图像是灰度的,每个像素由一个值表示(即,如果是白色,则像素强度为1,如果是黑色则为0)。例如,想象我们有一个 10 × 10 像素的图像:
plt.imshow(image_10x10, cmap="gray"), plt.axis("off")
plt.show()
在这种情况下,图像数据的尺寸将是 10 × 10:
image_10x10.shape
(10, 10)
如果我们展平数组,我们得到长度为 100 的向量(10 乘以 10):
image_10x10.flatten().shape
(100,)
这是我们图像的特征数据,可以与其他图像的向量合并,以创建我们将提供给机器学习算法的数据。
如果图像是彩色的,每个像素不是由一个值表示,而是由多个值表示(通常是三个),表示混合以形成该像素的最终颜色的通道(红色、绿色、蓝色等)。因此,如果我们的 10 × 10 图像是彩色的,每个观察值将有 300 个特征值:
# Load image in color
image_color = cv2.imread("images/plane_256x256.jpg", cv2.IMREAD_COLOR)
# Resize image to 10 pixels by 10 pixels
image_color_10x10 = cv2.resize(image_color, (10, 10))
# Convert image data to one-dimensional vector, show dimensions
image_color_10x10.flatten().shape
(300,)
图像处理和计算机视觉的一个主要挑战是,由于图像集合中每个像素位置都是一个特征,随着图像变大,特征数量会急剧增加:
# Load image in grayscale
image_256x256_gray = cv2.imread("images/plane_256x256.jpg", cv2.IMREAD_GRAYSCALE)
# Convert image data to one-dimensional vector, show dimensions
image_256x256_gray.flatten().shape
(65536,)
当图像为彩色时,特征数量甚至变得更大:
# Load image in color
image_256x256_color = cv2.imread("images/plane_256x256.jpg", cv2.IMREAD_COLOR)
# Convert image data to one-dimensional vector, show dimensions
image_256x256_color.flatten().shape
(196608,)
如输出所示,即使是小型彩色图像也有接近 200,000 个特征,这可能在训练模型时会引发问题,因为特征数量可能远远超过观察值的数量。
这个问题将推动后面章节讨论的降维策略,试图在不失去数据中过多信息的情况下减少特征数量。
8.14 将颜色直方图编码为特征
问题
您想创建一组表示图像中出现的颜色的特征。
解决方案
计算每个颜色通道的直方图:
# Load libraries
import cv2
import numpy as np
from matplotlib import pyplot as plt
np.random.seed(0)
# Load image
image_bgr = cv2.imread("images/plane_256x256.jpg", cv2.IMREAD_COLOR)
# Convert to RGB
image_rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB)
# Create a list for feature values
features = []
# Calculate the histogram for each color channel
colors = ("r","g","b")
# For each channel: calculate histogram and add to feature value list
for i, channel in enumerate(colors):
histogram = cv2.calcHist([image_rgb], # Image
[i], # Index of channel
None, # No mask
[256], # Histogram size
[0,256]) # Range
features.extend(histogram)
# Create a vector for an observation's feature values
observation = np.array(features).flatten()
# Show the observation's value for the first five features
observation[0:5]
array([ 1008., 217., 184., 165., 116.], dtype=float32)
讨论
在 RGB 颜色模型中,每种颜色都是三个颜色通道(即红色、绿色、蓝色)的组合。每个通道可以取 0 到 255 之间的 256 个值(由整数表示)。例如,我们图像中左上角的像素具有以下通道值:
# Show RGB channel values
image_rgb[0,0]
array([107, 163, 212], dtype=uint8)
直方图是数据值分布的表示。这里有一个简单的例子:
# Import pandas
import pandas as pd
# Create some data
data = pd.Series([1, 1, 2, 2, 3, 3, 3, 4, 5])
# Show the histogram
data.hist(grid=False)
plt.show()
在这个例子中,我们有一些数据,其中有两个1,两个2,三个3,一个4和一个5。在直方图中,每个条形表示数据中每个值(1,2等)出现的次数。
我们可以将这种技术应用到每个颜色通道上,但是不是五种可能的值,而是 256 种(通道值的可能数)。x 轴表示 256 个可能的通道值,y 轴表示图像中所有像素中特定通道值出现的次数(对于没有彩色图像的纸质读者表示歉意):
# Calculate the histogram for each color channel
colors = ("r","g","b")
# For each channel: calculate histogram, make plot
for i, channel in enumerate(colors):
histogram = cv2.calcHist([image_rgb], # Image
[i], # Index of channel
None, # No mask
[256], # Histogram size
[0,256]) # Range
plt.plot(histogram, color = channel)
plt.xlim([0,256])
# Show plot
plt.show()
正如我们在直方图中看到的,几乎没有像素包含蓝色通道值在 0 到约 180 之间,而许多像素包含蓝色通道值在约 190 到约 210 之间。该通道值分布显示了所有三个通道的情况。然而,直方图不仅仅是一种可视化工具;每个颜色通道有 256 个特征,总共为 768 个特征,代表图像中颜色分布。
参见
8.15 使用预训练的嵌入作为特征
问题
您想从现有的 PyTorch 模型中加载预训练的嵌入,并将其用作您自己模型的输入。
解决方案
使用torchvision.models选择模型,然后从中检索给定图像的嵌入:
# Load libraries
import cv2
import numpy as np
import torch
from torchvision import transforms
import torchvision.models as models
# Load image
image_bgr = cv2.imread("images/plane.jpg", cv2.IMREAD_COLOR)
# Convert to pytorch data type
convert_tensor = transforms.ToTensor()
pytorch_image = convert_tensor(np.array(image_rgb))
# Load the pretrained model
model = models.resnet18(pretrained=True)
# Select the specific layer of the model we want output from
layer = model._modules.get('avgpool')
# Set model to evaluation mode
model.eval()
# Infer the embedding with the no_grad option
with torch.no_grad():
embedding = model(pytorch_image.unsqueeze(0))
print(embedding.shape)
torch.Size([1, 1000])
讨论
在机器学习领域,迁移学习通常被定义为从一个任务学到的信息,并将其作为另一个任务的输入。我们可以利用已经从大型预训练图像模型(如 ResNet)学到的表示来快速启动我们自己的机器学习模型,而不是从零开始。更直观地说,你可以理解为,我们可以使用一个训练用于识别猫的模型的权重作为我们想要训练用于识别狗的模型的一个良好的起点。通过从一个模型向另一个模型共享信息,我们可以利用从其他数据集和模型架构学到的信息,而无需从头开始训练模型。
在计算机视觉中应用迁移学习的整个过程超出了本书的范围;然而,我们可以在 PyTorch 之外的许多不同方式中提取基于嵌入的图像表示。在 TensorFlow 中,另一个常见的深度学习库,我们可以使用tensorflow_hub:
# Load libraries
import cv2
import tensorflow as tf
import tensorflow_hub as hub
# Load image
image_bgr = cv2.imread("images/plane.jpg", cv2.IMREAD_COLOR)
image_rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB)
# Convert to tensorflow data type
tf_image = tf.image.convert_image_dtype([image_rgb], tf.float32)
# Create the model and get embeddings using the inception V1 model
embedding_model = hub.KerasLayer(
"https://tfhub.dev/google/imagenet/inception_v1/feature_vector/5"
)
embeddings = embedding_model(tf_image)
# Print the shape of the embedding
print(embeddings.shape)
(1, 1024)
参见
8.16 使用 OpenCV 检测对象
问题
您希望使用 OpenCV 中预训练的级联分类器来检测图像中的对象。
解决方案
下载并运行一个 OpenCV 的Haar 级联分类器。在这种情况下,我们使用一个预训练的人脸检测模型来检测图像中的人脸并画一个矩形框:
# Import libraries
import cv2
from matplotlib import pyplot as plt
# first run:
# mkdir models && cd models
# wget https://tinyurl.com/mrc6jwhp
face_cascade = cv2.CascadeClassifier()
face_cascade.load(
cv2.samples.findFile(
"models/haarcascade_frontalface_default.xml"
)
)
# Load image
image_bgr = cv2.imread("images/kyle_pic.jpg", cv2.IMREAD_COLOR)
image_rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB)
# Detect faces and draw a rectangle
faces = face_cascade.detectMultiScale(image_rgb)
for (x,y,w,h) in faces:
cv2.rectangle(image_rgb, (x, y),
(x + h, y + w),
(0, 255, 0), 5)
# Show the image
plt.subplot(1, 1, 1)
plt.imshow(image_rgb)
plt.show()
讨论
Haar 级联分类器是用于学习一组图像特征(特别是 Haar 特征)的机器学习模型,这些特征可以用于检测图像中的对象。这些特征本身是简单的矩形特征,通过计算矩形区域之间的和的差异来确定。随后,应用梯度提升算法来学习最重要的特征,并最终使用级联分类器创建相对强大的模型。
虽然这个过程的详细信息超出了本书的范围,但值得注意的是,这些预训练模型可以轻松从诸如OpenCV GitHub这样的地方下载为 XML 文件,并应用于图像,而无需自己训练模型。在你想要将简单的二进制图像特征(如contains_face或任何其他对象)添加到你的数据中的情况下,这非常有用。
参见
8.17 使用 Pytorch 对图像进行分类
问题
您希望使用 Pytorch 中预训练的深度学习模型对图像进行分类。
解决方案
使用torchvision.models选择一个预训练的图像分类模型,并将图像输入其中:
# Load libraries
import cv2
import json
import numpy as np
import torch
from torchvision import transforms
from torchvision.models import resnet18
import urllib.request
# Get imagenet classes
with urllib.request.urlopen(
"https://raw.githubusercontent.com/raghakot/keras-vis/master/resources/"
):
imagenet_class_index = json.load(url)
# Instantiate pretrained model
model = resnet18(pretrained=True)
# Load image
image_bgr = cv2.imread("images/plane.jpg", cv2.IMREAD_COLOR)
image_rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB)
# Convert to pytorch data type
convert_tensor = transforms.ToTensor()
pytorch_image = convert_tensor(np.array(image_rgb))
# Set model to evaluation mode
model.eval()
# Make a prediction
prediction = model(pytorch_image.unsqueeze(0))
# Get the index of the highest predicted probability
_, index = torch.max(prediction, 1)
# Convert that to a percentage value
percentage = torch.nn.functional.softmax(prediction, dim=1)[0] * 100
# Print the name of the item at the index along with the percent confidence
print(imagenet_class_index[str(index.tolist()[0])][1],
percentage[index.tolist()[0]].item())
airship 6.0569939613342285
讨论
许多预训练的深度学习模型用于图像分类,通过 PyTorch 和 TensorFlow 都很容易获取。在这个例子中,我们使用了 ResNet18,这是一个深度神经网络架构,它在 ImageNet 数据集上训练,深度为 18 层。在 PyTorch 中还有更深的 ResNet 模型,如 ResNet101 和 ResNet152,此外还有许多其他可供选择的图像模型。在 ImageNet 数据集上训练的模型能够为我们在之前代码片段中从 GitHub 下载的imagenet_class_index变量中定义的所有类别输出预测概率。
就像在 OpenCV 中的面部识别示例(参见 Recipe 8.16)一样,我们可以将预测的图像类别作为未来 ML 模型的下游特征,或者作为有用的元数据标签,为我们的图像添加更多信息。
参见
第九章:使用特征提取进行降维
9.0 介绍
通常情况下,我们可以接触到成千上万的特征。例如,在 第八章 中,我们将一个 256 × 256 像素的彩色图像转换成了 196,608 个特征。此外,因为每个像素可以取 256 个可能的值,我们的观察可以有 256¹⁹⁶⁶⁰⁸ 种不同的配置。许多机器学习算法在学习这样的数据时会遇到困难,因为收集足够的观察数据使算法能够正确运行是不现实的。即使在更结构化的表格数据集中,经过特征工程处理后,我们很容易就能得到数千个特征。
幸运的是,并非所有的特征都是相同的,特征提取 的目标是为了降低维度,将我们的特征集合 p[original] 转换成一个新的集合 p[new],使得 p[original] > p[new],同时保留大部分底层信息。换句话说,我们通过仅有少量数据丢失来减少特征数目,而数据仍能生成高质量的预测。在本章中,我们将涵盖多种特征提取技术来实现这一目标。
我们讨论的特征提取技术的一个缺点是,我们生成的新特征对人类来说是不可解释的。它们将具有与训练模型几乎相同的能力,但在人眼中看起来像是一组随机数。如果我们希望保留解释模型的能力,通过特征选择进行降维是更好的选择(将在 第十章 中讨论)。在特征选择期间,我们会移除我们认为不重要的特征,但保留其他特征。虽然这可能不像特征提取那样保留所有特征的信息,但它保留了我们不删除的特征——因此在分析过程中完全可解释。
9.1 使用主成分减少特征
问题
给定一组特征,你希望减少特征数目同时保留数据中的方差(重要信息)。
解决方案
使用 scikit 的 PCA 进行主成分分析:
# Load libraries
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
from sklearn import datasets
# Load the data
digits = datasets.load_digits()
# Standardize the feature matrix
features = StandardScaler().fit_transform(digits.data)
# Create a PCA that will retain 99% of variance
pca = PCA(n_components=0.99, whiten=True)
# Conduct PCA
features_pca = pca.fit_transform(features)
# Show results
print("Original number of features:", features.shape[1])
print("Reduced number of features:", features_pca.shape[1])
Original number of features: 64
Reduced number of features: 54
讨论
主成分分析 (PCA) 是一种流行的线性降维技术。PCA 将观察结果投影到特征矩阵的(希望较少的)主成分上,这些主成分保留了数据中最多的方差,从实际上来说,我们保留了信息。PCA 是一种无监督技术,意味着它不使用目标向量的信息,而只考虑特征矩阵。
要了解 PCA 的工作原理的数学描述,请参阅本食谱末尾列出的外部资源。然而,我们可以使用一个简单的例子来理解 PCA 的直觉。在图 9-1 中,我们的数据包含两个特征,x1和x2。通过观察可视化结果,应该清楚地看到观察结果像雪茄一样散开,长度很长,高度很低。更具体地说,我们可以说“长度”的方差显著大于“高度”的方差。我们将“方向”中的最大方差称为第一主成分,“方向”中的第二大方差称为第二主成分(依此类推)。
如果我们想要减少特征,一个策略是将我们二维空间中的所有观察投影到一维主成分上。我们会丢失第二主成分中捕获的信息,但在某些情况下,这是可以接受的权衡。这就是 PCA。
图 9-1. PCA 的第一和第二主成分
PCA 在 scikit-learn 中是通过PCA类实现的。n_components有两个操作,取决于提供的参数。如果参数大于 1,pca将返回那么多特征。这引出了如何选择最优特征数量的问题。幸运的是,如果n_components的参数在 0 到 1 之间,pca将返回保留原始特征方差百分之多少的最小特征数。通常使用 0.95 和 0.99 的值,分别表示保留原始特征方差的 95%和 99%。whiten=True将每个主成分的值转换为具有零均值和单位方差。另一个参数和参数是svd_solver="randomized",它实现了一种随机算法,通常能够在更短的时间内找到第一个主成分。
我们的解决方案的输出显示,PCA 使我们能够通过减少 10 个特征的维度,同时仍保留特征矩阵中 99%的信息(方差)。
参见
9.2 在数据线性不可分时减少特征
问题
您怀疑您的数据是线性不可分的,并希望降低维度。
解决方案
使用使用核函数的主成分分析的扩展,以实现非线性降维:
# Load libraries
from sklearn.decomposition import PCA, KernelPCA
from sklearn.datasets import make_circles
# Create linearly inseparable data
features, _ = make_circles(n_samples=1000, random_state=1, noise=0.1, factor=0.1)
# Apply kernel PCA with radius basis function (RBF) kernel
kpca = KernelPCA(kernel="rbf", gamma=15, n_components=1)
features_kpca = kpca.fit_transform(features)
print("Original number of features:", features.shape[1])
print("Reduced number of features:", features_kpca.shape[1])
Original number of features: 2
Reduced number of features: 1
讨论
PCA 能够减少我们特征矩阵的维度(即特征数量)。标准 PCA 使用线性投影来减少特征。如果数据是线性可分的(即你可以在不同类别之间画一条直线或超平面),那么 PCA 效果很好。然而,如果你的数据不是线性可分的(即你只能使用曲线决策边界来分离类别),线性变换效果就不那么好了。在我们的解决方案中,我们使用了 scikit-learn 的make_circles来生成一个模拟数据集,其中包含两个类别和两个特征的目标向量。make_circles生成线性不可分的数据;具体来说,一个类别被另一个类别包围在所有方向上,如图 9-2 所示。
图 9-2. 线性不可分数据上投影的第一个主成分
如果我们使用线性 PCA 来降低数据维度,那么这两个类别将线性投影到第一个主成分上,使它们变得交织在一起,如图 9-3 所示。
图 9-3. 线性不可分数据的第一个主成分,没有核 PCA
理想情况下,我们希望进行一种转换,能够减少维度并使数据线性可分。核 PCA 可以做到这两点,如图 9-4 所示。
图 9-4. 带有核 PCA 的线性不可分数据的第一个主成分
核函数允许我们将线性不可分的数据投影到一个更高维度,使其线性可分;这被称为“核技巧”。如果你不理解核技巧的细节也不要担心;只需把核函数看作是投影数据的不同方法。在 scikit-learn 的kernelPCA类中,我们可以使用多种核,通过kernel参数指定。一个常用的核是高斯径向基函数核rbf,但其他选项包括多项式核(poly)和 sigmoid 核(sigmoid)。我们甚至可以指定一个线性投影(linear),它将产生与标准 PCA 相同的结果。
核 PCA 的一个缺点是我们需要指定一些参数。例如,在第 9.1 节中,我们将n_components设置为0.99,以便使PCA选择保留 99%方差的成分数量。在核 PCA 中我们没有这个选项。相反,我们必须定义成分的数量(例如n_components=1)。此外,核函数自带它们自己的超参数,我们需要设置;例如,径向基函数需要一个gamma值。
那么我们如何知道使用哪些值?通过试错。具体而言,我们可以多次训练我们的机器学习模型,每次使用不同的核函数或不同的参数值。一旦找到产生最高质量预测值组合的值,我们就完成了。这是机器学习中的一个常见主题,我们将在第十二章深入学习这一策略。
参见
9.3 通过最大化类别可分性来减少特征
问题
您希望通过最大化类别之间的分离来减少分类器使用的特征数量。
解决方案
尝试线性判别分析(LDA)将特征投影到最大化类别分离的组件轴上:
# Load libraries
from sklearn import datasets
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
# Load Iris flower dataset:
iris = datasets.load_iris()
features = iris.data
target = iris.target
# Create and run an LDA, then use it to transform the features
lda = LinearDiscriminantAnalysis(n_components=1)
features_lda = lda.fit(features, target).transform(features)
# Print the number of features
print("Original number of features:", features.shape[1])
print("Reduced number of features:", features_lda.shape[1])
Original number of features: 4
Reduced number of features: 1
我们可以使用explained_variance_ratio_来查看每个组件解释的方差量。在我们的解决方案中,单个组件解释了超过 99%的方差:
lda.explained_variance_ratio_
array([0.9912126])
讨论
LDA 是一个分类技术,也是一种流行的降维技术。LDA 的工作方式类似于 PCA,即将我们的特征空间投影到较低维空间上。然而,在 PCA 中,我们只对最大化数据方差的组件轴感兴趣,而在 LDA 中,我们还有额外的目标是最大化类别之间的差异。在图 9-5 中,我们有包含两个目标类别和两个特征的数据。如果我们将数据投影到 y 轴上,两个类别不容易分开(即它们重叠),而如果我们将数据投影到 x 轴上,我们将得到一个特征向量(即我们通过减少一个维度),它仍然保留了类别可分性。在现实世界中,当然,类别之间的关系会更复杂,维度会更高,但概念保持不变。
图 9-5. LDA 试图最大化我们类别之间的差异
在 scikit-learn 中,LDA 使用LinearDiscriminantAnalysis实现,其中包括一个参数n_components,表示我们希望返回的特征数量。要确定n_components参数值(例如,要保留多少个参数),我们可以利用explained_variance_ratio_告诉我们每个输出特征解释的方差,并且是一个排序数组。例如:
lda.explained_variance_ratio_
array([0.9912126])
具体地,我们可以运行LinearDiscriminantAnalysis,将n_components设置为None,返回每个组件特征解释的方差比率,然后计算需要多少个组件才能超过一定阈值的方差解释(通常是 0.95 或 0.99):
# Create and run LDA
lda = LinearDiscriminantAnalysis(n_components=None)
features_lda = lda.fit(features, target)
# Create array of explained variance ratios
lda_var_ratios = lda.explained_variance_ratio_
# Create function
def select_n_components(var_ratio, goal_var: float) -> int:
# Set initial variance explained so far
total_variance = 0.0
# Set initial number of features
n_components = 0
# For the explained variance of each feature:
for explained_variance in var_ratio:
# Add the explained variance to the total
total_variance += explained_variance
# Add one to the number of components
n_components += 1
# If we reach our goal level of explained variance
if total_variance >= goal_var:
# End the loop
break
# Return the number of components
return n_components
# Run function
select_n_components(lda_var_ratios, 0.95)
1
参见
9.4 使用矩阵分解减少特征
问题
您有一个非负值的特征矩阵,并希望降低其维度。
解决方案
使用非负矩阵分解(NMF)来降低特征矩阵的维度:
# Load libraries
from sklearn.decomposition import NMF
from sklearn import datasets
# Load the data
digits = datasets.load_digits()
# Load feature matrix
features = digits.data
# Create, fit, and apply NMF
nmf = NMF(n_components=10, random_state=4)
features_nmf = nmf.fit_transform(features)
# Show results
print("Original number of features:", features.shape[1])
print("Reduced number of features:", features_nmf.shape[1])
Original number of features: 64
Reduced number of features: 10
讨论
NMF 是一种用于线性降维的无监督技术,分解(即将原始矩阵分解为多个矩阵,其乘积近似原始矩阵)特征矩阵为表示观察和其特征之间潜在关系的矩阵。直观地说,NMF 可以降低维度,因为在矩阵乘法中,两个因子(相乘的矩阵)的维度可以显著少于乘积矩阵。正式地说,给定所需的返回特征数r,NMF 将分解我们的特征矩阵,使之:
V ≈ W H
这里,V是我们的n × d特征矩阵(即d个特征,n个观察值),W是一个n × r的矩阵,H是一个r × d的矩阵。通过调整r的值,我们可以设置所需的降维量。
NMF 的一个主要要求是,正如其名称所示,特征矩阵不能包含负值。此外,与我们已经研究过的 PCA 和其他技术不同,NMF 不提供输出特征的解释方差。因此,我们找到n_components的最佳值的最佳方式是尝试一系列值,找到在我们最终模型中产生最佳结果的值(见第十二章)。
参见
9.5 在稀疏数据上减少特征
问题
您有一个稀疏的特征矩阵,并希望降低其维度。
解决方案
使用截断奇异值分解(TSVD):
# Load libraries
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import TruncatedSVD
from scipy.sparse import csr_matrix
from sklearn import datasets
import numpy as np
# Load the data
digits = datasets.load_digits()
# Standardize feature matrix
features = StandardScaler().fit_transform(digits.data)
# Make sparse matrix
features_sparse = csr_matrix(features)
# Create a TSVD
tsvd = TruncatedSVD(n_components=10)
# Conduct TSVD on sparse matrix
features_sparse_tsvd = tsvd.fit(features_sparse).transform(features_sparse)
# Show results
print("Original number of features:", features_sparse.shape[1])
print("Reduced number of features:", features_sparse_tsvd.shape[1])
Original number of features: 64
Reduced number of features: 10
讨论
TSVD 与 PCA 类似,并且事实上,PCA 在其步骤中经常使用非截断的奇异值分解(SVD)。给定d个特征,SVD 将创建d × d的因子矩阵,而 TSVD 将返回n × n的因子,其中n是由参数预先指定的。TSVD 的实际优势在于,与 PCA 不同,它适用于稀疏特征矩阵。
TSVD 的一个问题是:由于它如何使用随机数生成器,输出的符号在拟合过程中可能会翻转。一个简单的解决方法是仅在预处理流水线中使用fit一次,然后多次使用transform。
与线性判别分析类似,我们必须指定要输出的特征(组件)的数量。这通过参数n_components来完成。一个自然的问题是:什么是最佳的组件数量?一种策略是将n_components作为超参数包含在模型选择中进行优化(即选择使模型训练效果最佳的n_components值)。另一种方法是因为 TSVD 提供了原始特征矩阵每个组件解释的方差比例,我们可以选择解释所需方差的组件数量(常见值为 95%和 99%)。例如,在我们的解决方案中,前三个输出的组件大约解释了原始数据约 30%的方差:
# Sum of first three components' explained variance ratios
tsvd.explained_variance_ratio_[0:3].sum()
0.3003938537287226
我们可以通过创建一个函数来自动化这个过程,该函数将n_components设置为原始特征数减一,然后计算解释原始数据所需方差量的组件数量:
# Create and run a TSVD with one less than number of features
tsvd = TruncatedSVD(n_components=features_sparse.shape[1]-1)
features_tsvd = tsvd.fit(features)
# List of explained variances
tsvd_var_ratios = tsvd.explained_variance_ratio_
# Create a function
def select_n_components(var_ratio, goal_var):
# Set initial variance explained so far
total_variance = 0.0
# Set initial number of features
n_components = 0
# For the explained variance of each feature:
for explained_variance in var_ratio:
# Add the explained variance to the total
total_variance += explained_variance
# Add one to the number of components
n_components += 1
# If we reach our goal level of explained variance
if total_variance >= goal_var:
# End the loop
break
# Return the number of components
return n_components
# Run function
select_n_components(tsvd_var_ratios, 0.95)
40