使用 fastai 的图像分割
了解如何使用 U-net 对图像的每个像素进行颜色编码
介绍
图像分割是计算机视觉的一种应用,其中我们对图像中的每个像素进行颜色编码。每个像素代表图像中的一个特定对象。如果你看上面的图片,每条街道都是紫色的,每栋建筑都是橙色的,每棵树都是绿色的,等等。我们为什么要这样做,它与对象检测有何不同?
当我们关心边缘和区域时,当我们想从背景中分离出重要的对象时,通常会使用图像分割。我们希望了解一个对象的具体情况,并从那里对其进行进一步的分析。想象一下无人驾驶汽车。自动驾驶汽车不仅要识别街道,还要知道它的边缘或曲线,以便正确转弯。
图像分割在医学领域有着重要的意义。需要研究的部分用颜色编码,并从不同角度扫描观察。然后,它们被用于器官的自动测量、细胞计数或基于提取的边界信息的模拟。
该过程
我们将图像分割视为一个分类问题,对于图像中的每个像素,我们试图预测它是什么。是自行车,道路线,人行道,还是建筑?这样,我们产生了一个彩色编码图像,其中每个物体都有相同的颜色。
代码
像往常一样,我们从导入 fastai 库开始。
让我们先来看看其中的一张图片。
接下来,我们来看看分割后的图像是什么样子。由于标记图像中的值是整数,我们不能使用相同的函数来打开它。相反,我们使用open_mask
和show
来显示图像。
注意open_mask
内部的get_y_fn
功能。在每一个分割问题中,我们都有两组图像,原始图像和标记图像。我们需要将标签图像与正常图像进行匹配。我们用文件名来做这件事。让我们来看看一些图像的文件名。
我们看到正常图像和标记图像的文件名是相同的,除了相应的标记图像在末尾有一个_P
。因此,我们写了一个函数,对于每一个图像,识别其相应的标记副本。
我们还有一个名为codes.txt
的文件,它告诉我们标记图像中的整数对应于什么对象。让我们打开标签图像的数据。
现在让我们检查代码文件中这些整数的含义。
标签数据里有很多 26。从代码文件中的索引 0 开始计数,我们看到整数 26 引用的对象是一棵树。
既然我们已经理解了我们的数据,我们可以继续创建一个数据束并训练我们的模型。
我们不会使用整个数据集,我们还会保持相对较小的批量,因为对每个图像中的每个像素进行分类是一项资源密集型任务。
像往常一样,我们创建我们的数据束。阅读上面的代码:
- 从文件夹创建数据束
- 根据
valid.txt
中提到的文件名将数据分为训练和测试 - 使用功能
get_y_fn
找到带标签的图像,并将代码用作待预测的类别。 - 在图像上应用变换(注意这里的
tfm_y = True
。这意味着我们对从属图像应用的任何变换也应该应用到目标图像上。(例如:如果我们水平翻转图像,我们也应该翻转相应的标签图像))
为了训练,我们将使用一个名为 U-Net 的 CNN 架构,因为他们擅长重建图像。
在解释什么是 U-Net 之前,请注意上面代码中使用的指标。什么是acc_camvid
?
图像分割问题中的精度与任何分类问题中的精度相同。
Accuracy = no. of correctly classified pixels / total no. of pixels
然而,在这种情况下,一些像素被标记为Void
(该标签也存在于codes.txt
),在计算精度时不应予以考虑。因此,我们创建了一个新的函数来计算精度,在这里我们避开了这些标签。
CNN 的工作方式是将一幅图像分解成越来越小的部分,直到只剩下一件事可以预测(下图 U-Net 架构的左边部分)。一个 U-Net 然后把它变得越来越大,它为 CNN 的每个阶段都这样做。然而,从一个小矢量构建一幅图像是一项困难的工作。因此,我们有从原始卷积层到反卷积网络的连接。
像往常一样,我们找到学习率并训练我们的模型。即使只有一半的数据集,我们也能达到 92%的准确率。
检查一些结果。
事实是真正的目标,而预测是我们的模型所标注的。
我们现在可以在完整的数据集上进行训练。
结论
这就是本文的全部内容。在本文中,我们看到了如何使用 U-net 对图像的每个像素进行颜色编码。U-nets 越来越受欢迎,因为它们在从模糊图像生成高分辨率图像等应用上的表现优于 GANs。因此,知道它们是什么以及如何使用它们是非常有用的。
如果你想了解更多关于深度学习的知识,可以看看我在这方面的系列文章:
我所有关于深度学习的文章的系统列表
medium.com](https://medium.com/@dipam44/deep-learning-series-30ad108fbe2b)
~快乐学习
Tensorflow 2.0 中的图像相似性检测
准备好为您的 web 应用程序使用管道了吗
Photo by Sharon McCutcheon on Unsplash
在这篇文章中,我将向您展示我是如何在我的’时尚价格比较’ web 应用程序中实现’图像相似性检测任务的。我将使用图像相似性,根据用户搜索的内容向他们推荐视觉上相似的产品。
实现的完整源代码可以在我的 GitHub 库的中找到。
在整篇文章中,将有专门的部分来讨论以下每一个主题:
- 如何使用 Tensorflow 2.0 和 Tensorflow Hub 生成产品图像的**【图像特征向量】**。
- 如何使用Spotify/airy库和图像特征向量计算图像相似度得分。
- 在一个 JSON 文件中存储相似性分数和相关的产品标识号,以便在我们的 web 应用程序中进行可视化搜索。
什么是“图像相似性检测”,为什么它很重要?
图像相似性检测用于量化图像的视觉和语义相似程度。
在现代应用中,重复产品检测、图像聚类、视觉搜索和推荐任务都是使用这种技术来执行的。
“搜索的未来将是图片而不是关键词。”——本·希伯尔曼,Pinterest CEO
视觉搜索的一个优点是它完全依赖于商品的外观。不需要条形码、二维码、产品名称或其他产品元数据等其他数据。”——布伦特·拉博斯基、 亚马逊网络服务
“顾客越来越多地使用社交媒体平台,如 Instagram 和 Pinterest,作为灵感的来源,因此视觉搜索有可能改变我们为家庭购物的方式。” —马克钢,数字导演,Argos
Photo by Vidar Nordli-Mathisen on Unsplash
如何使用 Tensorflow 2.0 和 Tensorflow Hub 生成“图像特征向量”
- Tensorflow 2.0 和 Tensorflow Hub
Tensorflow 是 Google 开发的用于机器学习的端到端开源平台。它拥有工具、库和社区资源,让开发人员可以轻松构建和部署机器学习应用程序。
TensorFlow Hub 提供了许多可重用的机器学习模型。它使迁移学习变得非常容易,因为它为不同的问题领域和不同的任务(如图像分类、图像分割、姿态检测、文本嵌入、文本分类、视频生成等)提供了预训练的模型。
关于迁移学习的更多信息,你可以查看我以前的文章。
[## 浏览器中的机器学习:为自定义图像分类训练和服务 Mobilenet 模型
用 Tensorflow.js 和 Angular 在浏览器上训练基于 Mobilenet 的自定义图像分类模型
towardsdatascience.com](/training-custom-image-classification-model-on-the-browser-with-tensorflow-js-and-angular-f1796ed24934)
- 什么是图像特征向量?
一个图像特征向量是代表整个**图像的数字列表,**通常用于图像相似性计算或图像分类任务。
一般来说,低级图像特征是图像的次要细节,例如线、边缘、角或点。高级特征建立在低级特征之上,以检测图像中的对象和较大的形状。
我们可以使用卷积神经网络提取这两种类型的特征:第一对卷积层将学习用于找到低级特征的过滤器,而后面的层将学习识别常见的形状和对象。
在我们的例子中,我们将使用存储在 Tensorflow Hub 中的mobilenet _ v2 _ 140 _ 224预训练的卷积神经网络来提取产品图像的高级特征。
MobilenetV2 是一个简单的神经网络架构,适用于移动和资源受限的应用程序。点击此链接可获得关于 MobilenetV2 的更多信息。
Photo by Quaid Lagan on Unsplash
在开始编码之前,需要在我们的本地计算机上安装 Tensorflow 2.0、Tensorflow Hub 和 Spotify/airy 库。
$ virtualenv --system-site-packages -p python3 ./TFvenv
$ source ./TFvenv/bin/activate$ pip install tensorflow
$ pip install tensorflow-hub
$ pip install annoy
我们来生成图像特征向量:get_image_feature_vectors.py
这个脚本的主要目的是通过读取位于本地文件夹中的图像文件来生成图像特征向量。
它有两个功能: load_img() 和get _ image _ feature _ vectors()。
load_img(path) 获取文件名,作为函数的参数。然后加载并预处理图像,以便我们可以在我们的 MobilenetV2 CNN 模型中使用它们。
预处理步骤如下:
- 将图像解码为 W x H x 3 形状张量,数据类型为整数。
- 将图像大小调整为 224 x 224 x 3 形状张量,因为我们使用的 MobilenetV2 模型版本需要特定的图像大小。
- 将张量的数据类型转换为 float 并添加一个新轴,使张量形状为 1 x 224 x 224 x 3。这正是模型所期望的输入形状。
**get _ image _ feature _ vectors()**函数是我提取图像特征向量的地方。你可以在下面看到,这个函数的一步一步的定义;
- 使用 Tensorflow Hub 加载 MobilenetV2 模型
- 遍历本地文件夹中的所有图像,并将它们传递给 load_img(path) 函数
- 推断图像特征向量
- 将每个特征向量保存到单独的文件中以备后用
get_image_feature_vectors.py in action
**# get_image_feature_vectors.py****#################################################
# Imports and function definitions
#################################################**
**# For running inference on the TF-Hub module with Tensorflow**
import tensorflow as tf
import tensorflow_hub as hub**# For saving 'feature vectors' into a txt file**
import numpy as np**# Glob for reading file names in a folder**
import glob
import os.path
**#################################################****#################################################
# This function:
# Loads the JPEG image at the given path
# Decodes the JPEG image to a uint8 W X H X 3 tensor
# Resizes the image to 224 x 224 x 3 tensor
# Returns the pre processed image as 224 x 224 x 3 tensor
#################################################**
def load_img(path):**# Reads the image file and returns data type of string**
img = tf.io.read_file(path)**# Decodes the image to W x H x 3 shape tensor with type of uint8**
img = tf.io.decode_jpeg(img, channels=3)**# Resizes the image to 224 x 224 x 3 shape tensor**
img = tf.image.resize_with_pad(img, 224, 224)**# Converts the data type of uint8 to float32 by adding a new axis
# img becomes 1 x 224 x 224 x 3 tensor with data type of float32
# This is required for the mobilenet model we are using**
img = tf.image.convert_image_dtype(img,tf.float32)[tf.newaxis, ...]
return img**#################################################
# This function:
# Loads the mobilenet model in TF.HUB
# Makes an inference for all images stored in a local folder
# Saves each of the feature vectors in a file
#################################################**
def get_image_feature_vectors():
**# Definition of module with using tfhub.dev**
module_handle = "https://tfhub.dev/google/imagenet/
mobilenet_v2_140_224/feature_vector/4"
**# Loads the module**
module = hub.load(module_handle)**# Loops through all images in a local folder**
for filename in glob.glob('/Users/erdemisbilen/Angular/
fashionWebScraping/images_scraped/full/*.jpg'):
print(filename)**# Loads and pre-process the image**
img = load_img(filename)**# Calculate the image feature vector of the img**
features = module(img)**# Remove single-dimensional entries from the 'features' array **
feature_set = np.squeeze(features)
**# Saves the image feature vectors into a file for later use**
outfile_name = os.path.basename(filename) + ".npz"
out_path = os.path.join('/Users/erdemisbilen/Angular/
fashionWebScraping/images_scraped/feature-vectors/',
outfile_name)**# Saves the 'feature_set' to a text file**
np.savetxt(out_path, feature_set, delimiter=',')get_image_feature_vectors()
如何使用 Spotify/airy 库计算相似度得分
- 什么是 Spotify/骚扰库?
**(AapproximateNearestNeighborOhYeah)**,是一个用于近似最近邻实现的开源库。
我将使用它来查找给定集合中与给定特征向量最接近(或最相似)的图像特征向量。
调优 aroy 只需要两个主要参数:树的数量
*n_trees*
和搜索过程中要检查的节点数量*search_k*
。
*n_trees*
在构建期间提供,影响构建时间和索引大小。较大的值会给出更准确的结果,但索引也较大。
*search_k*
在运行时提供,影响搜索性能。较大的值会给出更准确的结果,但需要更长的时间返回。
让我们来计算相似度得分:cluster _ image _ feature _ vectors . py
这个脚本的主要目的是使用我们在前一章刚刚生成的图像特征向量来计算图像相似性得分。
****它有两个功能: match_id(文件名)和 cluster() 。
cluster() 函数按照以下流程进行图像相似度计算:
- 通过追加存储在本地文件夹中的所有图像特征向量来建立恼人的索引
- 计算最近邻和相似性得分
- 将信息保存和存储在 JSON 文件中,以备后用。
match_id(filename) 是一个帮助函数,因为我需要将图像与产品 id 进行匹配,以便在我的 web 应用程序中实现可视化产品搜索。有一个 JSON 文件,其中包含与产品图像名称匹配的所有产品 id 信息。该函数使用 JSON 文件检索给定图像文件名的产品 id 信息。
cluster_image_feature_vectors.py in action
****# cluster_image_feature_vectors.py****#################################################
# Imports and function definitions
#################################################
# Numpy for loading image feature vectors from file**
import numpy as np**# Time for measuring the process time**
import time**# Glob for reading file names in a folder**
import glob
import os.path**# json for storing data in json file**
import json**# Annoy and Scipy for similarity calculation**
from annoy import AnnoyIndex
from scipy import spatial
**#################################################****#################################################
# This function reads from 'image_data.json' file
# Looks for a specific 'filename' value
# Returns the product id when product image names are matched
# So it is used to find product id based on the product image name
#################################################**
def match_id(filename):
with open('/Users/erdemisbilen/Angular/fashionWebScraping
/jsonFiles/image_data.json') as json_file:for file in json_file:
seen = json.loads(file)for line in seen:
if filename==line['imageName']:
print(line)
return line['productId']
break
**#################################################****#################################################
# This function:
# Reads all image feature vectores stored in /feature-vectors/*.npz
# Adds them all in Annoy Index
# Builds ANNOY index
# Calculates the nearest neighbors and image similarity metrics
# Stores image similarity scores with productID in a json file
#################################################** def cluster():
start_time = time.time()
print("---------------------------------")
print ("Step.1 - ANNOY index generation - Started at %s"
%time.ctime())
print("---------------------------------")**# Defining data structures as empty dict**
file_index_to_file_name = {}
file_index_to_file_vector = {}
file_index_to_product_id = {}**# Configuring annoy parameters**
dims = 1792
n_nearest_neighbors = 20
trees = 10000**# Reads all file names which stores feature vectors**
allfiles = glob.glob('/Users/erdemisbilen/Angular
/fashionWebScraping/images_scraped/feature-vectors/*.npz')
t = AnnoyIndex(dims, metric='angular')for file_index, i in enumerate(allfiles):**# Reads feature vectors and assigns them into the file_vector**
file_vector = np.loadtxt(i)**# Assigns file_name, feature_vectors and corresponding product_id**
file_name = os.path.basename(i).split('.')[0]
file_index_to_file_name[file_index] = file_name
file_index_to_file_vector[file_index] = file_vector
file_index_to_product_id[file_index] = match_id(file_name)**# Adds image feature vectors into annoy index**
t.add_item(file_index, file_vector)print("---------------------------------")
print("Annoy index : %s" %file_index)
print("Image file name : %s" %file_name)
print("Product id : %s"
%file_index_to_product_id[file_index])
print("--- %.2f minutes passed ---------" % ((time.time() -
start_time)/60))**# Builds annoy index**
t.build(trees)print ("Step.1 - ANNOY index generation - Finished")
print ("Step.2 - Similarity score calculation - Started ")named_nearest_neighbors = []**# Loops through all indexed items**
for i in file_index_to_file_name.keys():
**# Assigns master file_name, image feature vectors
# and product id values**
master_file_name = file_index_to_file_name[i]
master_vector = file_index_to_file_vector[i]
master_product_id = file_index_to_product_id[i]**# Calculates the nearest neighbors of the master item**
nearest_neighbors = t.get_nns_by_item(i, n_nearest_neighbors)**# Loops through the nearest neighbors of the master item**
for j in nearest_neighbors:
print(j)**# Assigns file_name, image feature vectors and
# product id values of the similar item** neighbor_file_name = file_index_to_file_name[j]
neighbor_file_vector = file_index_to_file_vector[j]
neighbor_product_id = file_index_to_product_id[j]**# Calculates the similarity score of the similar item**
similarity = 1 - spatial.distance.cosine(master_vector,
neighbor_file_vector)rounded_similarity = int((similarity * 10000)) / 10000.0**# Appends master product id with the similarity score
# and the product id of the similar items** named_nearest_neighbors.append({
'similarity': rounded_similarity,
'master_pi': master_product_id,
'similar_pi': neighbor_product_id})print("---------------------------------")
print("Similarity index : %s" %i)
print("Master Image file name : %s" %file_index_to_file_name[i])
print("Nearest Neighbors. : %s" %nearest_neighbors)
print("--- %.2f minutes passed ---------" % ((time.time() -
start_time)/60))print ("Step.2 - Similarity score calculation - Finished ")**# Writes the 'named_nearest_neighbors' to a json file**
with open('nearest_neighbors.json', 'w') as out:
json.dump(named_nearest_neighbors, out)print ("Step.3 - Data stored in 'nearest_neighbors.json' file ")
print("--- Prosess completed in %.2f minutes ---------" %
((time.time() - start_time)/60))cluster()**
如您所见,我将每个产品图片的最高 20 个相似性分数保存在一个 JSON 文件中,并带有匹配的产品 id 信息。这是因为我不知道如何在客户端进行相似性计算来消除所需的工作量。
有了存储在 JSON 文件中的相似性得分,我可以轻松地填充 Elasticsearch 集群,或者填充数据库,以便在我的价格比较 web 应用程序的浏览器上实现近乎实时的可视化搜索体验。
为了开发这样的应用程序,web 抓取在开发和维护日常产品数据集方面起着重要的作用。如果你对这个主题感兴趣,可以看看我下面的相关文章。
**** [## 使用 Python 和 Scrapy 在 30 分钟内抓取 10 家在线商店的网页
获取启动应用程序项目所需的源数据
towardsdatascience.com](/web-scraping-of-10-online-shops-in-30-minutes-with-python-and-scrapy-a7f66e42446d)****
结论
Fashion Search Web Application by Erdem Isbilen — Visual Search Results
正如你在上面所看到的,MobileNetV2 和 Annoy 在寻找视觉上相似的产品方面做得非常好。
这种实现方式的一个缺点是它只能在整个图像级别工作。如果图像的背景不同,即使物体相似,它也不会提供好的结果。
该应用程序可以进一步改进,以实现类似于 Pinterest 或 Houzz 上的对象级相似性搜索。
使用三重损失的图像相似性
你训练过机器学习模型解决分类问题吗?如果是,班级数量是多少?也许 10 到 200?还是 1000?班级数量在百万量级的情况下,同样的模式是否行得通?如果答案是否定的,这篇文章是给你的。
行业中的几个真实世界的应用,从人脸识别到对象检测,从 POS 标记到 NLP 中的文档排序,都被公式化为多类分类问题。由于网络的稀疏性,当输出层中的类别数量过多时,典型的基于 softmax 的深度网络将不会有所帮助。相反,这种问题可以用不同的方式来表述。想法是以这样的方式学习数据点的分布式嵌入表示,即在高维向量空间中,上下文相似的数据点被投影在附近的区域中,而不相似的数据点被投影在彼此远离的地方。
三重损失结构通过相似性和相异性的概念帮助我们学习分布式嵌入。这是一种神经网络架构,其中多个并行网络被训练,彼此共享权重。在预测期间,输入数据通过一个网络来计算输入数据的分布式嵌入表示。
在本文中,我们将讨论如何训练三重态损失以及如何在预测过程中使用训练好的模型。
培训数据准备:
对于三元组丢失,目标是构建三元组,由锚图像、正图像(与锚图像相似)和负图像(与锚图像不同)组成。有不同的方法来定义相似和不相似的图像。如果您的数据集具有多个作为目标类的标注,则同一类的图像可被视为相似,而不同类的图像可被视为不相似。
我有一个包含 6 个不同类别的地质图像数据集。为了生成三元组,首先,随机选择 2 个类。然后,从一个类中选择两个图像,从另一个类中选择一个图像。现在,相同类别的图像被认为是相似的,所以它们中的一个被用作锚,而另一个是正面的,而来自另一个类别的图像被认为是负面的图像。
同样,对于每一批,选择一组 n 个三联体。
损失函数:
三重态损失的成本函数如下:
L(a,p,n) = max(0,D(a,p) — D(a,n) + margin)
其中 **D(x,y)😗*x 和 y 的学习向量表示之间的距离。可以使用 L2 距离或(1 -余弦相似度)作为距离度量。该功能的目的是保持锚点和正极之间的距离小于锚点和负极之间的距离。
模型架构:
想法是有 3 个相同的网络具有相同的神经网络架构,它们应该共享权重。我重复一遍,所有的网络应该共享潜在的权重向量。【请参考 github 知识库,了解 tensorflow 实现中如何在网络间共享权重】。深度网络的最后一层具有 D 个神经元,用于学习 D 维向量表示。
锚、正和负图像通过它们各自的网络传递,并且在反向传播期间,使用共享架构更新权重向量。在预测期间,任何一个网络都用于计算输入数据的矢量表示。
Triplet Loss architecture
下面是实现的张量板可视化。
tensorboard visualization of the computation graph
模型学习:
该模型不仅学会了同时为不同的类制定聚类,而且还成功地将相似的图像投影到它们的邻域中。在分类架构的情况下,该模型试图学习一对类之间的决策边界,但是该模型不考虑类内相似和不相似图像之间的完整性。
为了了解训练模型的学习有多好,我随机选择了 20%的图像,并在对高维向量空间表示进行降维后,在 2D 空间中绘制这些图像。
Plot of data points in 2D space
成绩:
以下是模型执行情况的快照。我已经从测试图像的语料库中随机选择了 20 个查询图像。并且对于每个查询图像,绘制出在高维向量空间表示上在余弦相似性方面相似的前 10 个最可能图像。
结论:
三重损耗架构帮助我们解决了几个类数量非常多的问题。假设你想建立一个人脸识别系统,你有一个 100 万张人脸的数据库,预先计算每张人脸的三维向量。现在,给定一张人脸图像(作为测试图像),使用所有一百万个预先计算的向量计算余弦相似度,无论哪张图像具有最高的相似度,都将是被选中的候选图像。如果测试图像只是噪声,则最高相似度将非常低,并且将低于阈值参数。
计算语料库中每个图像的余弦相似度在计算上是非常低效的。同时,它可能需要大量的物理内存来存储语料库图像的矢量表示。
faiss 是 facebook-research 开发的一个开源框架,它帮助建立了一个基于可用内存的语料库索引,并提供了几种方法来相对更快地找到相似的图像。
Git 存储库:
克隆 git 库:https://github.com/sanku-lib/triplet_loss.git
延伸阅读:
参考文献:
[1]https://www.cs.cmu.edu/~rsalakhu/papers/oneshot1.pdf
https://arxiv.org/pdf/1503.03832.pdf
[3]https://arxiv.org/pdf/1702.08734.pdf
图像到图像的翻译
图像到图像的翻译是一类视觉和图形问题,其目标是学习输入图像和输出图像之间的映射。它的应用范围很广,如收藏风格转移、物体变形、季节转移、照片增强等。
CycleGAN
Unpaired Image-to-Image Translation Using Cycle-Consistent Adversarial Networks(ICCV 2017)
作者提出了一种在缺少成对例子的情况下学习将图像从源域 X 翻译到目标域 Y 的方法。目标是学习映射 G : X → Y,使得来自 G(X)的图像分布与使用对抗损失的分布 Y 不可区分。因为这种映射是高度欠约束的,所以我们将其与逆映射 F : Y → X 耦合,并引入循环一致性损失来强制 F(G(X)) ≈ X(反之亦然)。
成对的训练数据(左)由一一对应的训练实例组成。不成对的训练集没有这样的对应关系(图取自论文)
图取自报纸。
该模型包含两个映射函数 G : X → Y 和 F : Y → X,以及相关的对抗性鉴别器 DY 和 DX。DY 鼓励 G 将 X 转换为与域 Y 不可区分的输出,反之亦然。为了进一步规范映射,他们引入了两个“循环一致性损失”,这两个损失捕捉了这样的直觉:如果我们从一个域转换到另一个域,然后再转换回来,我们应该到达我们开始的地方。
斯塔根
Unified Generative Adversarial Networks for Multi-Domain Image-to-Image Translation(CVPR 2018)
现有的图像到图像的转换方法在处理多于两个域时具有有限的可扩展性和鲁棒性,因为对于每对图像域应该独立地建立不同的模型。StarGAN 是一种新颖且可扩展的方法,仅使用一个模型就可以为多个领域执行图像到图像的翻译。
跨域模型和我们提出的模型 StarGAN 之间的比较。(a)为了处理多个域,应该为每对图像域建立跨域模型。(b) StarGAN 能够使用单个生成器学习多个域之间的映射。图中显示了连接多个域的星型拓扑。(图取自论文)
StarGAN 概述,由两个模块组成,一个鉴别器 D 和一个生成器 G. (a) D 学习区分真假图像,并将真实图像分类到其对应的域。(b) G 接受图像和目标域标签作为输入,并生成假图像。目标域标签被空间复制并与输入图像连接。g 尝试从给定原始域标签的伪图像重建原始图像。(d) G 试图生成与真实图像无法区分的图像,并被 d 归类为目标域
基于 CycleGAN 模型的图像到图像翻译
一种用于图像到图像翻译的无监督方法。
Photo by Tim Mossholder on Unsplash
如果你对 GANs 领域完全陌生,我建议你在开始写这篇文章之前先看看我以前的文章。
概观
towardsdatascience.com](/fake-face-generator-using-dcgan-model-ae9322ccfd65)
即使你不熟悉 GANs,我仍然建议你通读这篇文章,因为我们需要所有我们在上一篇文章中学到的基本概念来理解这篇文章。也就是说,让我们从这篇文章开始。
循环生成对抗网络 (CycleGAN) ,是一种训练深度卷积网络用于图像到图像翻译任务的方法。与其他用于图像转换任务的 GAN s 模型不同,C ycleGAN 使用无监督方法学习一个图像域和另一个图像域之间的映射。例如,如果我们对将马的图像转换成斑马的图像感兴趣,我们不需要将马物理转换成斑马的训练数据集。 CycleGAN 做到这一点的方法是通过训练生成器网络来学习从域 X 到看起来像是来自域 Y 的图像的映射*(反之亦然)*。
当你阅读这篇文章时,你会对它是如何做到的有更深的理解。所以让我们开始吧…
CycleGAN
CycleGAN architecture. Image from https://modelzoo.co/model/mnist-svhn-transfer
出于直觉,我们会参考上面的图像。
对于成对的图像集,我们可以直接创建一个 GAN 来借助 Pix2Pix 学习从 x 到 y 的映射。你可以在这里阅读更多关于 Pix2Pix Networks 的内容。
但是准备成对的数据集既费时又困难。我的意思是,我们需要一幅斑马的图像,它的位置和马的位置一样,或者有着相同的背景,这样我们才能学会绘制地图。
为了解决这个问题,CycleGAN 架构应运而生。CycleGAN 能够学习从一个域 X 到另一个域 Y 的映射,而不必寻找完全匹配的训练对!我们来看看 CycleGAN 是怎么做到的。
假设我们有一组来自域 X 的图像和一组来自域 y 的不成对的图像,我们希望能够将一组图像转换成另一组图像。为此,我们定义了一个映射 G (G: X- > Y) ,它尽最大努力将 X 映射到 Y。但是对于不成对的数据,我们不再能够查看真实和虚假的数据对。但是我们知道我们可以改变我们的模型来产生一个属于目标领域的输出。
因此,当你推送一匹马的图像时,我们可以训练一个生成器生成逼真的斑马图像。但问题是,我们不能强迫生成器的输出对应于它的输入*(在上面的图像中,第一个转换是正确的图像到图像的转换)。这导致了一个被称为模式崩溃的问题,其中一个模型可能将来自域 X 的多个输入映射到来自域 Y 的同一个输出。在这种情况下,给定一匹输入马(域 X)* ,我们所知道的就是输出应该看起来像斑马*(域 Y)* 。但是为了在相应的目标域中获得输入的正确映射,我们引入了一个额外的映射作为逆映射 G’ (G’: Y- > X) ,它试图将 Y 映射到 X。这被称为循环一致性约束。
可以这样想,如果我们将一幅马的图像*(域 X)* 翻译成一幅斑马的图像*(域 Y)* ,然后我们再从斑马(域 Y)翻译回一匹马(域 X),我们应该回到开始时的马的图像。
一个完整的翻译周期应该会让你回到开始时的图像。在从域 X 到 Y 的图像变换的情况下,如果满足以下条件,我们说图像从域 X 到域 Y 的变换是正确的。
condition -1
在循环一致性约束的帮助下,CycleGAN 确保模型学习到从域 X 到域 y 的正确映射。
图像到图像翻译任务
下面的任务被分解成一系列小任务,从加载和可视化数据到训练模型。
可视化数据集
具体来说,我们将看一组在夏季或冬季拍摄的约塞米蒂国家公园的照片。季节是我们的两个领域!
Images from the summer season domain.
Images from the winter season domain.
一般来说,你可以看到夏天的图像比冬天的图像更亮更绿。冬天包含了像雪和多云图像这样的东西。在这个数据集中,我们的主要目标是训练一个生成器,学习将图像从夏天转换到冬天,反之亦然。这些图像不包含标签,被称为不成对的训练数据。但是通过使用 CycleGAN,我们可以使用无监督的方法学习从一个图像域到另一个图像域的映射。
你可以点击这里下载以下数据。
定义模型
CycleGAN 包括两个鉴别器( D_x 和 D_y )和两个发生器( G_xtoy 和 G_ytox )。
- D_x —将来自域 X 的训练图像识别为真实图像,将从域 Y 到域 X 的翻译图像识别为伪造图像。
- D_y —将来自域 X 的训练图像识别为真实图像,将从域 Y 到域 X 的翻译图像识别为伪造图像。
- G_xtoy —将图像从域 X 转换到域 y。
- G_ytox —将图像从域 Y 转换到域 x。
鉴别器
在这个 CycleGAN 中,鉴别器 D_x 和 D_y 是卷积神经网络,它们看到一幅图像并试图将其分类为真实或伪造。在这种情况下,real 由接近 1 的输出表示,而 fake 由接近 0 的输出表示。鉴别器具有以下架构:
# helper conv function
def conv(in_channels, out_channels, kernel_size, stride=2, padding=1, batch_norm=True):
"""Creates a convolutional layer, with optional batch normalization.
"""
layers = []
conv_layer = nn.Conv2d(in_channels=in_channels, out_channels=out_channels,
kernel_size=kernel_size, stride=stride, padding=padding, bias=False)
layers.append(conv_layer)
if batch_norm:
layers.append(nn.BatchNorm2d(out_channels))
return nn.Sequential(*layers)
class Discriminator(nn.Module):
def __init__(self, conv_dim=64):
super(Discriminator, self).__init__()
# Define all convolutional layers
# Should accept an RGB image as input and output a single value
self.layer_1 = conv(3,conv_dim,4,batch_norm = False)
self.layer_2 = conv(conv_dim,conv_dim*2,4)
self.layer_3 = conv(conv_dim*2,conv_dim*4,4)
self.layer_4 = conv(conv_dim*4,conv_dim*8,4)
self.layer_5 = conv(conv_dim*8,1,4,1,batch_norm = False)
def forward(self, x):
# define feedforward behavior
x = F.relu(self.layer_1(x))
x = F.relu(self.layer_2(x))
x = F.relu(self.layer_3(x))
x = F.relu(self.layer_4(x))
x = self.layer_5(x)
return x
解释
- 以下架构由输出单个 logit 的五个卷积层组成。这个逻辑定义了图像是否真实。这种体系结构中没有完全连接的层。
- 除了第一层和最后一层,所有卷积层之后都是批量归一化(在 conv 帮助函数中定义)。
- 对于隐藏单元,使用 ReLU 激活功能。
- 每次卷积后特征图的数量基于参数conv _ 尺寸(在我的实现中 conv _ 尺寸= 64) 。
D_x 和 D_y 的架构都是一样的,所以我们只需要定义一个类,后面实例化两个鉴别器。
剩余块和剩余函数
在定义生成器架构时,我们将在我们的架构中使用称为 Resnet 块和剩余函数的东西。使用 Resnet 块和剩余函数的思想如下:
残留块
残差块连接编码器和解码器。这种架构背后的动机如下:深度神经网络可能非常难以训练,因为它们更可能具有爆炸或消失的梯度,因此难以达到收敛;批处理规范化对此有所帮助。
这个问题的一个解决方案是使用 Resnet 块,允许我们学习所谓的剩余函数,因为它们被应用于层输入。
剩余功能
当我们创建深度学习模型时,该模型(应用了激活的若干层)负责学习从输入 x 到输出 y 的映射 M。
M(x) = y
我们可以定义一个剩余函数,而不是学习从 x 到 y 的直接映射。
F(x) = M(x)-x
这着眼于应用于 x 的映射与原始输入 x 之间的差异。F(x)通常是两个卷积层+归一化层以及其间的 ReLU。这些卷积层应该具有与输出相同数量的输入。这种映射可以写成如下形式:剩余函数和输入 x 的函数。
M(x) = F(x) + x
你可以在这里阅读更多关于深度剩余学习。下面是实现 Residual Block 的代码片段。
class ResidualBlock(nn.Module):
"""Defines a residual block.
This adds an input x to a convolutional layer (applied to x) with the same size input and output.
These blocks allow a model to learn an effective transformation from one domain to another.
"""
def __init__(self, conv_dim):
super(ResidualBlock, self).__init__()
# conv_dim = number of inputs
# define two convolutional layers + batch normalization that will act as our residual function, F(x)
# layers should have the same shape input as output; I suggest a kernel_size of 3
self.layer_1 = conv(conv_dim,conv_dim,3,1,1,batch_norm = True)
self.layer_2 = conv(conv_dim,conv_dim,3,1,1,batch_norm = True)
def forward(self, x):
# apply a ReLu activation the outputs of the first layer
# return a summed output, x + resnet_block(x)
out_1 = F.relu(self.layer_1(x))
out_2 = x + self.layer_2(out_1)
return out_2
发电机
生成器 G_xtoy 和 G_ytox 由编码器、将图像转换成小特征表示的 conv 网和解码器、负责将特征表示转换成变换图像的转置 conv 网组成。下面是实现生成器的代码片段。
def deconv(in_channels, out_channels, kernel_size, stride=2, padding=1, batch_norm=True):
"""Creates a transpose convolutional layer, with optional batch normalization.
"""
layers = []
# append transpose conv layer
layers.append(nn.ConvTranspose2d(in_channels, out_channels, kernel_size, stride, padding, bias=False))
# optional batch norm layer
if batch_norm:
layers.append(nn.BatchNorm2d(out_channels))
return nn.Sequential(*layers)
class CycleGenerator(nn.Module):
def __init__(self, conv_dim=64, n_res_blocks=6):
super(CycleGenerator, self).__init__()
# 1\. Define the encoder part of the generator
self.layer_1 = conv(3,conv_dim,4)
self.layer_2 = conv(conv_dim,conv_dim*2,4)
self.layer_3 = conv(conv_dim*2,conv_dim*4,4)
# 2\. Define the resnet part of the generator
layers = []
for n in range(n_res_blocks):
layers.append(ResidualBlock(conv_dim*4))
self.res_blocks = nn.Sequential(*layers)
# 3\. Define the decoder part of the generator
self.layer_4 = deconv(conv_dim*4,conv_dim*2,4)
self.layer_5 = deconv(conv_dim*2,conv_dim,4)
self.layer_6 = deconv(conv_dim,3,4,batch_norm = False)
def forward(self, x):
"""Given an image x, returns a transformed image."""
# define feedforward behavior, applying activations as necessary
out = F.relu(self.layer_1(x))
out = F.relu(self.layer_2(out))
out = F.relu(self.layer_3(out))
out = self.res_blocks(out)
out = F.relu(self.layer_4(out))
out = F.relu(self.layer_5(out))
out = F.tanh(self.layer_6(out))
return out
解说
- 下面的架构由编码器的三个卷积层和解码器的三个转置卷积层组成,它们都使用一系列残差块*(在我们的例子中为 6)连接。*
- 所有卷积层之后是批归一化。
- 除了最后一层,所有转置卷积层之后都是批量归一化。
- 对于隐藏单元,使用 ReLU 激活函数,除了最后一层,我们使用 tanh 激活函数,这是基于我们在之前的文章 *(训练 DCGAN 的技巧)*中的讨论。
- 编码器和解码器中每次卷积后的特征图数量基于参数 conv_dim 。
G_xtoy 和 G_ytox 的架构都是一样的,所以我们只需要定义一个类,后面实例化两个生成器。
培训过程
训练过程包括定义损失函数、选择优化器以及最后训练模型。
鉴频器和发电机损耗
我们已经看到,常规 GANs 将鉴别器视为具有 sigmoid 交叉熵损失函数的分类器。然而,这种损失函数可能导致学习过程中的消失梯度问题。为了解决这个问题,我们将对鉴别器使用最小二乘损失函数。这种结构通常被称为最小二乘 GANs,你可以从 LSGANs 的原始论文中读到更多关于它们的内容。
鉴频器损耗
鉴别器损耗将是鉴别器的输出(给定图像)和目标值(0 或 1)之间的均方误差,这取决于它应该将该图像分类为假还是真。例如,对于一个真实的图像 x,我们可以通过观察它与使用均方误差将 x 识别为真实图像的接近程度来训练 D_x:
out = D_x(x)
real_error = torch.mean((out-1))(用于 Pytorch)
发电机损耗
在这种情况下,我们将生成看起来像是属于域 X 但却基于域 Y 的图像的假图像,反之亦然。我们将通过观察应用于这些伪图像的鉴频器的输出来计算这些生成图像的真实损失。
除了对抗性损失,发电机损失项将包括循环一致性损失。这种损失是重建图像与原始图像相比有多好的量度。例如,我们有一个假的生成图像 x^和一个真实的图像 y,在 g _ xtoy*(g_xtoy(x^)= y^】*的帮助下,我们可以从 x^生成 y^。这里,周期一致性损失将是原始图像和重建图像之间的绝对差异。
Cycle consistency loss. Image from https://ssnl.github.io/better_cycles/report.pdf
下面是定义损失的代码片段。
def real_mse_loss(D_out):
# how close is the produced output from being "real"?
return torch.mean((D_out - 1)**2)
def fake_mse_loss(D_out):
# how close is the produced output from being "fake"?
return torch.mean(D_out**2)
def cycle_consistency_loss(real_im, reconstructed_im, lambda_weight):
# calculate reconstruction loss
# return weighted loss
loss = torch.mean(torch.abs(real_im - reconstructed_im))
return loss*lambda_weight
在周期一致性损失中,lambda 项是一个权重参数,它将对批次中的平均绝对误差进行加权。建议大家看一下 原文,CycleGAN 论文 得到一个 lambda_weight 的起始值。
【计算机】优化程序
对于 CycleGAN,我们为生成器 (G_xtoy 和 G_ytox) 和 D_x 和 D_y 定义了三个优化器。所有数值超参数均选自原始 CycleGAN 文件。
# hyperparams for Adam optimizers
lr= 0.0002
beta1= 0.5
beta2= 0.999
g_params = list(G_XtoY.parameters()) + list(G_YtoX.parameters()) # Get generator parameters
# Create optimizers for the generators and discriminators
g_optimizer = optim.Adam(g_params, lr, [beta1, beta2])
d_x_optimizer = optim.Adam(D_X.parameters(), lr, [beta1, beta2])
d_y_optimizer = optim.Adam(D_Y.parameters(), lr, [beta1, beta2])
培训
当 CycleGAN 进行训练并看到来自集合 X 和 Y 的一批真实图像时,它通过执行以下步骤进行训练:
对于鉴别器:
- 计算实际图像上的鉴别器 D_x 损失。
- 在 G_ytox 的帮助下,使用集合 Y 中的图像生成伪图像,然后计算 D_x 的伪损失。
- 计算总损失并进行反向传播和优化。对 D_y 做同样的事情,你的域就转换了。
对于发电机:
- 根据域 Y 中的实像生成看起来像域 X 的假像,然后根据 D_x 如何响应假像 X 计算发电机损耗。
- 基于步骤 1 中的伪 x 射线图像生成重建图像 Y^图像。
- 计算重建和真实 Y 图像的周期一致性损失。
- 重复步骤 1 至 4,仅交换域,添加所有发电机损耗,并执行反向传播和优化。
下面是这样做的代码片段。
def training_loop(dataloader_X, dataloader_Y, test_dataloader_X, test_dataloader_Y,
n_epochs=1000):
print_every=10
# keep track of losses over time
losses = []
test_iter_X = iter(test_dataloader_X)
test_iter_Y = iter(test_dataloader_Y)
# Get some fixed data from domains X and Y for sampling. These are images that are held
# constant throughout training, that allow us to inspect the model's performance.
fixed_X = test_iter_X.next()[0]
fixed_Y = test_iter_Y.next()[0]
fixed_X = scale(fixed_X) # make sure to scale to a range -1 to 1
fixed_Y = scale(fixed_Y)
# batches per epoch
iter_X = iter(dataloader_X)
iter_Y = iter(dataloader_Y)
batches_per_epoch = min(len(iter_X), len(iter_Y))
for epoch in range(1, n_epochs+1):
# Reset iterators for each epoch
if epoch % batches_per_epoch == 0:
iter_X = iter(dataloader_X)
iter_Y = iter(dataloader_Y)
images_X, _ = iter_X.next()
images_X = scale(images_X) # make sure to scale to a range -1 to 1
images_Y, _ = iter_Y.next()
images_Y = scale(images_Y)
# move images to GPU if available (otherwise stay on CPU)
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
images_X = images_X.to(device)
images_Y = images_Y.to(device)
# ============================================
# TRAIN THE DISCRIMINATORS
# ============================================
## First: D_X, real and fake loss components ##
# 1\. Compute the discriminator losses on real images
d_x_optimizer.zero_grad()
real_D_loss = real_mse_loss(D_X(images_X))
# 3\. Compute the fake loss for D_X
fake_D_loss = fake_mse_loss(D_X(G_YtoX(images_Y)))
# 4\. Compute the total loss and perform backprop
d_x_loss = real_D_loss + fake_D_loss
d_x_loss.backward()
d_x_optimizer.step()
## Second: D_Y, real and fake loss components ##
d_y_optimizer.zero_grad()
real_D_y_loss = real_mse_loss(D_Y(images_Y))
fake_D_y_loss = fake_mse_loss(D_Y(G_XtoY(images_X)))
d_y_loss = real_D_y_loss + fake_D_y_loss
d_y_loss.backward()
d_y_optimizer.step()
# =========================================
# TRAIN THE GENERATORS
# =========================================
## First: generate fake X images and reconstructed Y images ##
g_optimizer.zero_grad()
# 1\. Generate fake images that look like domain X based on real images in domain Y
out_1 = G_YtoX(images_Y)
# 2\. Compute the generator loss based on domain X
loss_1 = real_mse_loss(D_X(out_1))
# 3\. Create a reconstructed y
out_2 = G_XtoY(out_1)
# 4\. Compute the cycle consistency loss (the reconstruction loss)
loss_2 = cycle_consistency_loss(real_im = images_Y, reconstructed_im = out_2, lambda_weight=10)
## Second: generate fake Y images and reconstructed X images ##
out_3 = G_XtoY(images_X)
# 5\. Add up all generator and reconstructed losses and perform backprop
loss_3 = real_mse_loss(D_Y(out_3))
out_4 = G_YtoX(out_3)
loss_4 = cycle_consistency_loss(real_im = images_X, reconstructed_im = out_4, lambda_weight=10)
g_total_loss = loss_1 + loss_2 + loss_3 + loss_4
g_total_loss.backward()
g_optimizer.step()
# Print the log info
if epoch % print_every == 0:
# append real and fake discriminator losses and the generator loss
losses.append((d_x_loss.item(), d_y_loss.item(), g_total_loss.item()))
print('Epoch [{:5d}/{:5d}] | d_X_loss: {:6.4f} | d_Y_loss: {:6.4f} | g_total_loss: {:6.4f}'.format(
epoch, n_epochs, d_x_loss.item(), d_y_loss.item(), g_total_loss.item()))
sample_every=100
# Save the generated samples
if epoch % sample_every == 0:
G_YtoX.eval() # set generators to eval mode for sample generation
G_XtoY.eval()
save_samples(epoch, fixed_Y, fixed_X, G_YtoX, G_XtoY, batch_size=16)
G_YtoX.train()
G_XtoY.train()
# uncomment these lines, if you want to save your model
# checkpoint_every=1000
# # Save the model parameters
# if epoch % checkpoint_every == 0:
# checkpoint(epoch, G_XtoY, G_YtoX, D_X, D_Y)
return losses
使用 GPU 进行了超过 5000 个时期的训练,这就是为什么我必须将我的模型和输入从 CPU 移动到 GPU。
结果
- 以下是在每个时期之后记录的发生器和鉴别器的训练损失的曲线图。
我们可以观察到,发生器开始时有很高的误差,但随着时间的推移,它开始产生像样的图像转换,从而有助于降低误差。
两种鉴频器误差都显示出非常小的误差波动。但是到 5000 个纪元结束时,我们可以看到两个鉴别器误差都减少了,从而迫使生成器进行更真实的图像转换。
- 可视化样品。
经过 100 次迭代之后—
Translation from X to Y after 100 iterations
Translation from Y to X after 100 iterations
经过 5000 次迭代后—
Translation from X to Y after 5000 iterations
Translation from Y to X after 5000 iterations
我们可以观察到 CycleGAN 模型产生低分辨率图像,这是一个正在进行的研究领域,您可以通过点击此处了解更多关于使用多个生成器的高分辨率公式。
这种型号很难精确匹配颜色。这是因为,如果 G_xtoy 和 G_ytox 可能会改变图像的色调;周期一致性损失可能不受影响,并且仍然很小。你可以选择引入一个新的、基于颜色的损失项来比较 G_ytox(y)和 y,以及 G_xtoy(x)和 x,但这变成了一种监督学习方法。也就是说,CycleGAN 能够做出令人满意的翻译。
如果你想保持联系,你可以在 LinkedIn 上找到我。
参考
查看我关于这篇文章的 Github 报告。
使用 Tesseract.js 进行图像到文本的 OCR
使用 javascript 从图像中提取文本
Photo by Franck V. on Unsplash
您是否希望从图像、照片中提取文本?是不是刚拍了一张讲义的照片,想转换成文字?然后你需要一个可以通过 OCR(光学字符识别)识别文本的应用程序。
今天,我将实现你期待已久的愿望,用强大的 JavaScript 库 Tesseract.js 构建一个图像到文本的转换器
点击下面的链接亲自尝试一下:
[## 使用 Tesseract.js - Benson 技术的图像到文本 OCR
您是否希望从图像、照片中提取文本?今天,我要满足你的愿望,建立一个图像文本…
bensonruan.com](https://bensonruan.com/image-to-text-ocr-with-tesseract-js/)
履行
你是不是觉得自己发现了宝藏?我们可以得到一本书的扫描图像,使用 OCR 技术读取图像,并以我们可以在机器上使用的格式输出文本。这可以极大地提高我们的生产率,并避免重复的手工输入。
在本教程中,我将向您展示如何使用 Tesseract.js 构建 OCR web 应用程序。让我们直接进入代码。
#步骤 1:包含 tesseract.js
首先,我们需要包含 JavaScript 库 tesseract.js,在你的 HTML5 页面中包含 Tesseract.js 最简单的方法就是使用 CDN。因此,将以下内容添加到您的网页的<head>
中。
<html>
<head>
<script src='[https://unpkg.com/tesseract.js@v2.0.0-alpha.13/dist/tesseract.min.js](https://unpkg.com/tesseract.js@v2.0.0-alpha.13/dist/tesseract.min.js)'></script>
</head>
如果您使用的是 npm,也可以通过运行下面的命令来安装它
npm install tesseract.js@next
在的末尾,包含主 javascript 文件 tesseract-ocr.js
<script src="js/tesseract-ocr.js"></script>
</body>
</html>
#步骤 2:设置 html 元素
接下来我们需要做的是添加下面的 html 元素
- 语言选择器
- 图像文件选择器
- 所选图像的缩略图预览
- 处理后结果的占位符
#步骤 3:初始化并运行 Tesseract
此外,我们将初始化一个TesseractWorker
。然后利用recognize
功能。这个函数异步运行并返回一个TesseractJob
对象。
您可以在一个回调函数中获得文本结果,该函数可以使用then()
方法添加。此外,使用progress()
方法添加一个回调来监控 OCR 操作的状态和进度。
#第 4 步:显示进度和结果
最后,让我们探索返回的TesseractJob
对象,并使用它来显示结果。
一旦结果返回,它包含一个置信度,从图像中提取的文本。在单词数组中,还包括单词在图像内部的位置。现在我们使用下面的函数progressUpdate
将它显示给用户。
代码差不多就是这样了!选择你自己的图片和一些文字,看着结果滚滚而来!
GitHub 知识库
您可以通过下面的链接下载上述演示的完整代码:
使用 javascript 库 tesseract.js 在浏览器中从图像中提取文本…
github.com](https://github.com/bensonruan/Tesseract-OCR)
Photo by Temple Cerulean on Unsplash
结论
毕竟,我已经用不同的图像做了一些实验,并且发现了 Tesseract.js 的一些优点和缺点。
优点:
- 它支持多种语言,点击这里查看支持语言的完整列表。
- 在正常字体和清晰背景的情况下,精确度相当高
缺点:
- 它在嘈杂的背景下不太好用
- 它被一些自定义字体弄糊涂了
但是,我仍然认为它是一个伟大的 JavaScript 库。它为浏览器带来了 OCR 的强大功能,并为开发人员打开了一扇机会之门。
ImageAI:动态物体识别
将物体识别快速连接到你的应用程序
如果计算机有眼睛,它能识别什么?
区分猫和狗会很好,但更好的是识别开放图像数据集中的所有 7870 个对象!这里有一个练习。向窗外看,数一数你能认出多少物体。物体识别是将人类的这部分能力引入计算机。它实现了计算机以前很难实现的广泛应用——从无人驾驶汽车到高级安全。你的脸书人脸识别到交通管理。
计算机视觉已经真正从对整个图像进行分类发展到识别图像中的单个物体。这就是“这是一张有车辆的道路照片”和“这张照片上有 12-15 辆汽车和 4-6 辆摩托车”的区别。这是一种算法,它给出了足够的上下文来声明“中等流量”。
听起来不错。我该如何报名?
许多大型云提供商都有现成的 API 来做这件事。有谷歌、微软和亚马逊的实现。如果你有预算,那么只需将它插入你的应用程序,你就有了而不是热狗应用程序。但是我想把重点放在一些你可能会弄脏手的东西上。这些 API 大多由深度学习模型驱动,在对象识别方面,YOLO 是你的人。我推荐看看他们为 YOLO v3 的实现所做的惊人的开场白:
我们向 YOLO 展示一些更新!我们做了一些设计上的小改动,让它变得更好。我们还训练了这个新的网络,它非常棒。比上次大一点,但是更准确。不过还是很快,别担心。
作者有幽默感,但不要让这愚弄了你。这是一个伟大的算法。它被命名为YOOonlyLookO因为该算法的主要贡献是速度非常快。其他模型执行两阶段区域提议和对象分类阶段。YOLO 使用单一模型来输出包围盒和类别概率。它在速度和准确性方面远远超过了其他算法。
这个 YOLO 不错。怎么才能拿到?
有一个开源库可以解决这个问题。ImageAI 是一个 Python 库,它可以轻松地使开发人员利用现成的训练好的模型进行对象识别。你也可以用自己的物品进行定制训练。
我在这个 Kaggle 笔记本上尝试了简单的识别用例。就像下面这个片段一样简单:
from imageai.Detection import ObjectDetection
# load YOLO here
detector = ObjectDetection()
detector.setModelTypeAsYOLOv3()
detector.setModelPath("yolo.h5")
detector.loadModel()
# load your image here "input_img"
# ...
detections = detector.detectObjectsFromImage(
input_img, input_type='array',
minimum_percentage_probability=50, output_type='array')
我在开放的图像数据集上进行测试,挑选出至少有一个“人”对象的图片。YOLO 模型是 ImageAI 的现成产品,它接受了包括人员、车辆和家居用品在内的 80 个类别的培训。我将模型设置为输出成为对象的概率大于 50%的区域。
看看它在我的一些样本输入中的表现
从左到右。(1) 100%一个人,但注意领带。(2)重叠的包围盒被识别出来,太神奇了。(3)失焦的人还是可以识别的。(4 和 5)图纸被认为是人。(6–8)可以检测多个人。(9)模型以为有两只狗。
(1)这是一个算法被难住的例子。(2)感觉良好!(3)404 消息被误标为时钟。(4)多个人被误标为一个人。我真的不知道这里发生了什么——某种聚会?(6)到处都是错误的标签!(7–9)全部正确。
(1)我不认为这个图像包含一个人!(2)发现了一个碗,真酷。(3–9)人!
这个人在废墟中行走的图像提出了一种可能性,即同样的技术可以帮助灾难恢复工作。例如,巡逻森林火灾或洪水的无人机可以识别被困人员。
它以 Python 字典的形式输出对象,所以你应该能够在你的应用程序中轻松操作它。这是一个普通的应用程序。能够分辨照片中的单个物体可以自动分类你越来越多的相机图像。或者至少,点钟,碗和滑板。
原载于 2019 年 8 月 25 日http://itstherealdyl.wordpress.com。
作为数据结构的图像:艺术到 256 整数
让我们以数据结构的形式介绍图像的基本知识,认识一些迷幻的猫,并使用 NumPy“绘制”一个粉红色的棋盘!
形象问题一直让我兴奋。
我非常习惯在日常工作中使用表格数据。我相信大多数数据人都能理解。这篇文章将让你了解如何将图像表示为数据结构的基本知识,这样当奇异的图像识别或对象检测问题出现时,你就从some knowledge
的地方开始,而不是从no knowledge
的地方开始!
说到图像,我们将遵循一个由来已久的互联网传统。我们的一些主题将是猫!认识一下艾尔莎和斯穆希!
现在你注意到我了…我们开始吧。
黑白图像
让我们从这张简陋棋盘的图片开始:
信不信由你,我刚刚用NumPy
做了这个!
让我们假设我们有一个名为chessboard
的8x8
NumPy 数组。让我们看一下它最上面的一行。这是一行,其中最左边的像素是白色的:
chessboard[0]array([255, 0, 255, 0, 255, 0, 255, 0], dtype=uint8)
现在让我们看看第二排。这是一行,其中最左边的像素是黑色:
chessboard[1]array([ 0, 255, 0, 255, 0, 255, 0, 255], dtype=uint8)
每个数字代表从0 to 255
开始的像素强度。也就是说,在这个颜色系统中有256
个可能的值。你可能已经猜到了,这里的0
代表‘黑’,而255
代表‘白’。介于两者之间的一切都是灰色的。
为了制作棋盘,我简单地堆叠了这些黑白行,直到得到一个维度数组8x8
(即 8 行 8 列):
import matplotlib.pyplot as plt
import numpy as npwhite_row = np.zeros(8).astype(np.uint8)
black_row = np.zeros(8).astype(np.uint8)
black_row[[1, 3, 5, 7]] = 255
white_row[[0, 2, 4, 6]] = 255chessboard = np.array([
white_row,
black_row,
white_row,
black_row,
white_row,
black_row,
white_row,
black_row,
])print(chessboard)[[255 0 255 0 255 0 255 0]
[ 0 255 0 255 0 255 0 255]
[255 0 255 0 255 0 255 0]
[ 0 255 0 255 0 255 0 255]
[255 0 255 0 255 0 255 0]
[ 0 255 0 255 0 255 0 255]
[255 0 255 0 255 0 255 0]
[ 0 255 0 255 0 255 0 255]]
随机边注: 如果说
*0*
*255*
之间的一切在我们的色彩体系中都是一种深浅灰,那么五十度灰是怎么回事?让我们忽略灰色恰好是一个人的名字。为什么不:**
总之,这 254 种灰色看起来像什么?让我们创建一个
*16x16*
NumPy 数组,因为*16x16 = 256*
而这恰好是我们要绘制的像素强度的个数。
*all_shades = np.reshape(np.arange(256), (16,16))*
让我们画出那些可怜的、被忽视的灰色阴影:
很美,不是吗?
够了!“随机附言”完毕。
彩色图像呢?让我们增加一些音量!
我相信你们大多数人都听说过 RGB 颜色系统,其中:
R == 'red'
,G == 'green'
,以及B == 'blue'
在 RGB 彩色图像中,我们现在有三个矩阵,而不是一个8x8
:
- 专用于
red
像素强度的矩阵, - 专用于
green
像素亮度的矩阵,以及 - 专用于
blue
像素亮度的矩阵。
这些被称为我们的 RGB 图像的红色、绿色和蓝色通道。
每个矩阵包含一个介于0
和255
之间的整数,其中0
表示该通道中的颜色已经被有效地“关闭”,而255
表示该通道中的颜色已经被“打开到最大”。
当我们将这些矩阵堆叠在一起时,我们得到了一幅彩色图像!RGB
系统是additive
颜色系统的一个例子,其中颜色是通过将三个矩阵中的每一个矩阵的像素强度相加形成的。
我们现在知道彩色图像是三维的。彩色图像具有:
- 以像素为单位的
height
, - 以像素为单位的
width
,以及 - 3 个通道的一个
depth
。
让我们制作红色、绿色和蓝色的图像!
首先,请注意通道-第一个和通道-最后一个约定。我们的 NumPy 数组是一维的3x8x8
。也就是说,当描述数组的维度时,我们的“RGB 通道优先”。另一个约定是 channels-last,其中通道在数组的维度中列在最后(即8x8x3
)。
我们将使用Matplotlib's imshow
功能来绘制图像。检查函数的 docstring,我们会看到:
(M,N,3):具有 RGB 值(0–1 float 或 0–255 int)的图像…前两个维度(M,N)定义图像的行和列。
换句话说,imshow
需要一个“channels-last”NumPy 数组。放心吧!我们将通过使用np.moveaxis
将频道轴移动到末端,将“频道优先”图像转换为“频道最后”图像:
*channels_first = np.zeros((3, 8, 8)).astype(np.uint8)channels_first.shape(3, 8, 8)channels_last = np.moveaxis(channels_first, 0, 2)channels_last.shape(8, 8, 3)*
很简单,对吧?我们继续。
让我们turn on
所有的red
像素。假设 RGB 排序,我们将把第一个矩阵中的所有像素亮度设置为255
的最大值。
我们将首先创建一个3x8x8
阵列,所有像素亮度“关闭”(即设置为零):
*all_pixels_off = np.zeros((3, 8, 8)).astype(np.uint8)print(all_pixels_off)[[[0 0 0 0 0 0 0 0]
[0 0 0 0 0 0 0 0]
[0 0 0 0 0 0 0 0]
[0 0 0 0 0 0 0 0]
[0 0 0 0 0 0 0 0]
[0 0 0 0 0 0 0 0]
[0 0 0 0 0 0 0 0]
[0 0 0 0 0 0 0 0]]
[[0 0 0 0 0 0 0 0]
[0 0 0 0 0 0 0 0]
[0 0 0 0 0 0 0 0]
[0 0 0 0 0 0 0 0]
[0 0 0 0 0 0 0 0]
[0 0 0 0 0 0 0 0]
[0 0 0 0 0 0 0 0]
[0 0 0 0 0 0 0 0]]
[[0 0 0 0 0 0 0 0]
[0 0 0 0 0 0 0 0]
[0 0 0 0 0 0 0 0]
[0 0 0 0 0 0 0 0]
[0 0 0 0 0 0 0 0]
[0 0 0 0 0 0 0 0]
[0 0 0 0 0 0 0 0]
[0 0 0 0 0 0 0 0]]]*
所有通道像素都已关闭。所以我们得到一个全黑的图像是有道理的:
*_ = plt.imshow(np.moveaxis(all_pixels_off, 0, 2))*
让我们把红色像素调到最大:
*from copy import deepcopy
red_image = deepcopy(all_pixels_off)
red_image[0, :] = 255*
检查矩阵,我们看到这个:
*print(red_image)[[[255 255 255 255 255 255 255 255]
[255 255 255 255 255 255 255 255]
[255 255 255 255 255 255 255 255]
[255 255 255 255 255 255 255 255]
[255 255 255 255 255 255 255 255]
[255 255 255 255 255 255 255 255]
[255 255 255 255 255 255 255 255]
[255 255 255 255 255 255 255 255]][[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]][[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]]]*
绘制它,我们得到这个:
*_ = plt.imshow(np.moveaxis(red_image, 0, 2))*
对绿色像素重复:
*green_image = deepcopy(all_pixels_off)
green_image[1, :] = 255
print(green_image)[[[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]][[255 255 255 255 255 255 255 255]
[255 255 255 255 255 255 255 255]
[255 255 255 255 255 255 255 255]
[255 255 255 255 255 255 255 255]
[255 255 255 255 255 255 255 255]
[255 255 255 255 255 255 255 255]
[255 255 255 255 255 255 255 255]
[255 255 255 255 255 255 255 255]][[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]]]_ = plt.imshow(np.moveaxis(green_image, 0, 2))*
并且再次针对蓝色像素:
*blue_image = deepcopy(all_pixels_off)
blue_image[2, :] = 255
print(blue_image)[[[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]][[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]
[ 0 0 0 0 0 0 0 0]][[255 255 255 255 255 255 255 255]
[255 255 255 255 255 255 255 255]
[255 255 255 255 255 255 255 255]
[255 255 255 255 255 255 255 255]
[255 255 255 255 255 255 255 255]
[255 255 255 255 255 255 255 255]
[255 255 255 255 255 255 255 255]
[255 255 255 255 255 255 255 255]]]_ = plt.imshow(np.moveaxis(blue_image, 0, 2))*
如果我们把所有的像素亮度都调到最大会怎么样?
让我们将红色、绿色和蓝色通道中的所有像素强度设置为它们的最大值255
:
*all_channels_to_the_max = deepcopy(all_pixels_off)
all_channels_to_the_max[:, :] = 255
print(all_channels_to_the_max)[[[255 255 255 255 255 255 255 255]
[255 255 255 255 255 255 255 255]
[255 255 255 255 255 255 255 255]
[255 255 255 255 255 255 255 255]
[255 255 255 255 255 255 255 255]
[255 255 255 255 255 255 255 255]
[255 255 255 255 255 255 255 255]
[255 255 255 255 255 255 255 255]][[255 255 255 255 255 255 255 255]
[255 255 255 255 255 255 255 255]
[255 255 255 255 255 255 255 255]
[255 255 255 255 255 255 255 255]
[255 255 255 255 255 255 255 255]
[255 255 255 255 255 255 255 255]
[255 255 255 255 255 255 255 255]
[255 255 255 255 255 255 255 255]][[255 255 255 255 255 255 255 255]
[255 255 255 255 255 255 255 255]
[255 255 255 255 255 255 255 255]
[255 255 255 255 255 255 255 255]
[255 255 255 255 255 255 255 255]
[255 255 255 255 255 255 255 255]
[255 255 255 255 255 255 255 255]
[255 255 255 255 255 255 255 255]]]_ = plt.imshow(np.moveaxis(all_channels_to_the_max, 0, 2))*
我们得到一个完全白色的图像!
给定一个 RGB 图像数据结构,我们如何获得灰度?
你大概能猜到我们需要做什么。让我们将所有像素的亮度设置为0
和255
之间的某个值。我们将在所有三个矩阵中应用相同的值:
*greyscale = deepcopy(all_pixels_off)
greyscale[:, :] = 150
print(greyscale)[[[150 150 150 150 150 150 150 150]
[150 150 150 150 150 150 150 150]
[150 150 150 150 150 150 150 150]
[150 150 150 150 150 150 150 150]
[150 150 150 150 150 150 150 150]
[150 150 150 150 150 150 150 150]
[150 150 150 150 150 150 150 150]
[150 150 150 150 150 150 150 150]][[150 150 150 150 150 150 150 150]
[150 150 150 150 150 150 150 150]
[150 150 150 150 150 150 150 150]
[150 150 150 150 150 150 150 150]
[150 150 150 150 150 150 150 150]
[150 150 150 150 150 150 150 150]
[150 150 150 150 150 150 150 150]
[150 150 150 150 150 150 150 150]][[150 150 150 150 150 150 150 150]
[150 150 150 150 150 150 150 150]
[150 150 150 150 150 150 150 150]
[150 150 150 150 150 150 150 150]
[150 150 150 150 150 150 150 150]
[150 150 150 150 150 150 150 150]
[150 150 150 150 150 150 150 150]
[150 150 150 150 150 150 150 150]]]*
绘制它,我们得到:
嘣!我们自己有一个灰色的形象。
NumPy art:我们来做一个火辣的粉色棋盘吧!
快速搜索一下,我就知道*【粉红佳人】*的 RBG 值是这样的:
*red = 255
green = 105
blue = 180*
这是我们将要做的:
- 我们将复制三份我们原来的
8x8
棋盘阵列。一个代表红色通道。另一个代表绿色通道。最后一个代表蓝色通道。 - 然后,我们将改变每个通道的像素强度,以匹配上面的粉红色 RBG 值。我们将改变原始黑色像素的值(即零)。
- 一旦我们完成了这些,我们将创建一个
3x8x8
数组,它将代表我们在 RGB 颜色空间中的棋盘。
当我们绘制图像时,我们将有希望看到一个神话般的,粉红色的棋盘。我们开始吧!
首先,副本:
*red_channel = deepcopy(chessboard)
green_channel = deepcopy(chessboard)
blue_channel = deepcopy(chessboard)*
接下来,像素值:
*red_channel[np.where(red_channel == 0)] = 255
green_channel[np.where(green_channel == 0)] = 105
blue_channel[np.where(blue_channel == 0)] = 180*
让我们创建我们的3x8x8
数组,其中3
代表我们的通道:
*hot_pink_chessboard = np.array([red_channel, green_channel, blue_channel]).astype(np.uint8)*
现在开始策划!请击鼓…
万岁!那确实是一个漂亮的棋盘。
迷幻猫
让我们用我们对图像的了解来搞乱我的猫 Elsa 和 Smooshie 的图像。
我们将使用cv2
包将我们的猫图像读入 Python。cv2
可以通过发布pip install opencv-python
来安装。
*import cv2
elsa = cv2.imread('./elsa_original.jpg')*
恼人的是,cv2
以不同的通道顺序存储图像。代替 RGB,我们有一个 BGR 通道排序。所以我们现在有一个 BGR 的图像。我们可以看到颜色有点偏离:
*_ = plt.imshow(elsa)*
我们现在将重新排列我们的频道,看看我们的图像是否看起来更好:
*elsa = cv2.cvtColor(elsa, cv2.COLOR_BGR2RGB)
_ = plt.imshow(elsa)*
那就好多了!
让我们增加红色通道的像素强度。假设我们想要将50
添加到红色通道的像素亮度中。当使用numpy.uint8
数据类型时,如果current pixel intensity + 50 > 255
,我们的像素强度环绕并从零开始计数。这是一个整数溢出的例子。
为了避免这种情况,我们将使用一个次优但快速的解决方案。我们将把 NumPy 数组转换为numpy.uint16
数据类型,它的上限是65,535
。我们会给每个像素强度加上100
。imshow
自动裁剪数组,使其最大值为255
,这样我们就可以直接绘制图像了。
*elsa_int16 = elsa.astype(np.int16)
elsa_int16 = np.moveaxis(elsa_int16, 2, 0)elsa_red = deepcopy(elsa_int16)
elsa_red[0, :] += 100_ = plt.imshow(np.moveaxis(elsa_red, 0, 2))*
艾尔莎绝对有红色调!
让我们用绿色通道重复:
*elsa_green = deepcopy(elsa_int16)
elsa_green[1, :] += 100
_ = plt.imshow(np.moveaxis(elsa_green, 0, 2))*
是的,绝对更环保!最后,蓝色通道:
*elsa_blue = deepcopy(elsa_int16)
elsa_blue[2, :] += 100
_ = plt.imshow(np.moveaxis(elsa_blue, 0, 2))*
是的,绝对是蓝色的。
让我们将任意数字添加到每个通道,看看我们会得到什么:
*elsa_psychedelic = deepcopy(elsa)
elsa_psychedelic = np.moveaxis(elsa_psychedelic, 2, 0)elsa_psychedelic[0, :] += 150
elsa_psychedelic[1, :] += 5
elsa_psychedelic[2, :] += 50_ = plt.imshow(np.moveaxis(elsa_psychedelic, 0, 2))*
看起来很酷,艾尔莎!斯穆希呢?
*smooshie = cv2.imread('./smooshie_original.jpg')
smooshie = cv2.cvtColor(smooshie, cv2.COLOR_BGR2RGB)
_ = plt.imshow(smooshie)*
让我们给 Smooshie 的照片添加一些不同的数字:
*smooshie_psychedelic = deepcopy(smooshie)
smooshie_psychedelic = np.moveaxis(smooshie_psychedelic, 2, 0)smooshie_psychedelic[0, :] += 85
smooshie_psychedelic[1, :] += 10
smooshie_psychedelic[2, :] += 175_ = plt.imshow(np.moveaxis(smooshie_psychedelic, 0, 2))*
我们有两只迷幻猫!
( 排队《你爱的阳光》)
结论
我们已经学会了如何将图像表达为可以在 Python 中操作的数据。我们一路上做了一个热粉色的棋盘和一些迷幻的猫照!
我希望这篇文章已经给了你处理图像核等主题所需的基础,这些主题对于理解卷积神经网络如何工作很重要。
下次见。
贾斯廷
原载于 2019 年 12 月 14 日https://embracingtherandom.com*。*
幻想工程和复活
Protohouse (2012) by Gilles Retsin and Softkill Design. A house structurally optimized for minimal volume and uniform stress. Image: ArchDaily
生成式设计描绘了想象的新领域,同时重组了过去的元素。这与我们现有系统的运作方式不协调。
新一代设计技术赋予设计师超能力。他们指的是为设计者设计的一套基于算法的技术。他们不仅是制造的工具,更是思考 的工具。
创成式设计技术通过让设计者指定设计问题的参数来工作。使用聪明的算法和大量的处理能力,生成式设计软件遵循一个进化过程,快速循环通过数千个——如果不是数百万个——设计选择,测试配置并从每次迭代中学习什么可行什么不可行。然后,设计师从无数生成的选项中进行策划和选择。
这些工具非常有用。他们可以优化材料的使用,加快设计过程,创造艺术,想象全新的产品形式。他们是从大规模生产过渡到 大规模定制 *的推动者之一。*生成式设计的未来看起来非常光明。
Tensegrity nodes. Image: engineering.com courtesy of Arup
然而,生成式设计技术的一个关键问题是,它们所实现的输出并不完全符合我们现有系统的规则。虽然我们的法律是围绕静态、类别和稀缺建立的,但生成式设计系统会产生流动、梯度和大量的解决方案。
例如,专利法依赖于根据独创性和实用性对技术进行分类。生成式设计产生了无数模糊了这两种分类的设计。他们质疑谁制造了什么,谁拥有什么,以及谁应该拥有什么。
我们的社会系统和生成设计系统之间的不一致表明,如果生成设计的用途扩大,将不得不做出一些让步。这篇文章没有提供这样的解决方案。相反,它概述了生成式设计技术与我们的规则和法律之间的四个摩擦领域。如果这些摩擦被认为会愈演愈烈,那么我们就有责任以相应的紧迫感做出回应。
I .(去)个性化媒体
对于它们的许多用例,创成式设计技术不依赖于先前的数据来生成解决方案。例如,可以通过处理一组参数来设计桥梁,而不需要参考预先存在的结构。
但是数据可以作为有用的参数来指导解决方案的创建。这在 2016 年谷歌泄露的内部视频中得到了说明,该视频强调了一种推测性的用例,即用户的详细数据被用于设计“个性化”产品。在这种情况下,我们未来的自己不仅会收到定制的媒体信息,还会收到定制的物理媒体。
The Selfish Ledger (2016) by Google. Source: The Verge
收到认识到我们独特需求的定制产品的前景令人兴奋,但也提出了这样做的合适范例的问题。
哈佛大学社会学家 Shoshana Zuboff 提出了当前数字范式的一个特征。她称之为监督资本主义,这是一种市场逻辑,公司从人类经验中积累大量数据,将这些数据组织成未来行为的预测模型,并拍卖外部实体修改这种行为的能力。在这个体系中,“给予”给我们的东西是达到他人目的的手段。
由于我们生活中的个人物品可能会受到生活数据的影响,我们需要谨慎对待这些数据收集和使用背后的商业模式。如果一个数字系统的可用性取决于它改变我们行为以消费更多的能力,我们希望我们最亲密的物品也这样做吗?
决定数字服务的规则和边界的问题还没有解决。随着数字领域与物理领域的融合,它们将会复合。
二。重构的真实性
创成式设计技术可以用来模拟和重新配置已经存在的事物。已经有很多关于“deepfakes”的影响的讨论,deep fakes 是深度学习的一种应用,它基于真实的副本创建假的照片、视频和文字。但是很少有人讨论整部作品如何被拆开、合成和复活。
艺术界现在正在见证“已故艺术家”的“新作品”。一个著名的例子是下一个伦勃朗,这是一幅模仿伦勃朗风格的 3D 打印画,伦勃朗是一位去世已久的 17 世纪荷兰画家。该项目由荷兰国际集团发起,荷兰国际集团是一家荷兰跨国银行集团“,寻求一种创新的方式,并在竞争对手中脱颖而出——并利用了微软的技术诀窍。这幅名为“T4”的肖像画由 1.48 亿像素组成,取材于伦勃朗作品中的 168,263 个片段。
The Next Rembrandt (2016). It’s not a Rembrandt, but it looks like one. Source: The Next Rembrandt
“我们观察了许多伦勃朗的画作,扫描了它们的表面纹理、元素构成以及使用的颜料种类。如果你想虚拟生成一幅伦勃朗的画,这就是你需要的信息。”— Joris Dik,代尔夫特技术大学
最近,伦敦大学学院的研究人员使用了一种神经转移风格技术来“恢复”毕加索著名的“蓝色吉他手”下面的一幅被涂掉的画。他们的过程使用了隐藏画作的红外和 x 射线图像,以及毕加索其他作品的“风格”图像,来重现这幅画。基本的算法是通用的,可以将任何图像转换成另一个艺术家的风格。
伦勃朗和毕加索会因为复制他们作品的算法而在坟墓里打滚吗,或者他们会赞同这些(再)创造的叛逆行为吗?活着的艺术家呢?
像前面提到的专利法一样,我们的系统是围绕类别设计的。他们裁定事物是真是假,原件还是复印件,专利还是专利侵权,等等。
生成设计引入了真实、现实、信任和价值的粒度。新的规则可能需要被书写——或者甚至可能产生。
三。一个分布式网络
除了 deepfakes 的利用性质之外,生成式设计技术有可能被用于明显的恶意目的。例如,生成对抗网络(GANs),一种算法,已经被麻省理工学院技术评论认定为网络威胁,因为它们能够入侵系统。一个尚未看到的风险是,生殖设计可能有助于创造以前无法想象的武器。
The Shuty (2015) by Defence Distributed. A semi-automatic gun made of metal and plastic parts. The plastic parts are 3D printed, while the metal parts can be purchased from typical hardware stores. Source: YouTube
在线网络通过扩散和普及对抗性算法和设计,加剧了这种潜在的危害。
一个这样的例子是 3D 打印枪的蓝图和 CAD 模型是如何在发明后不久就在网上传播的。虽然一名美国联邦法官确实批准了一项针对 Defense Distributed(发布该设计的组织)的全国性禁令,但一旦设计被共享,它们很少会被取消共享。
限制生成设计技术的对抗性使用及其设计的传播没有简单的答案。开发人员很难将约束编码到他们的工具中。设计师可以被要求选择加入行为准则——但这些可以被忽略。政府或平台可以尝试监控内容,但过去的努力基本无效。
斯图尔特·布兰德曾打趣道,“信息想要自由。“互联网和生成式设计技术的融合表明在这个方向上又前进了一步——无论是字面上还是象征性的——这将是危险的。
四。像国家一样产生
生成式设计技术非常适合优化,但它们只能优化可测量的内容。这可能会引入许多设计问题,这些问题源于以下前提:
- 测量只能得到现实的一部分。
- 测量可能会被扭曲,尤其是当受试者知道他们正在被测量的时候。
- 没有或不能衡量的东西可能会被忽略。
这种对可测量性的偏好在智能城市计划中是有先见之明的。亚当·格林菲尔德写道支撑这些倡议的是一种隐含的世界观,即“世界在原则上是完全可知的,其内容是可计数的,它们之间的关系能够在技术系统的状态下被有意义地编码,没有偏见或扭曲[……]那么,如果对制定合理的公民政策至关重要的信息在某种程度上从它们的探测中缺失,存在于它们之间的空间中,或者来自于我们着手测量的世界的任何质量和我们对它的物质经验之间的相互作用,那又会怎么样呢?”
在智能城市出现之前,詹姆斯·c·克拉克将这种心态称为高级现代主义。在他的书《看起来像一个国家》中,他认为善意的大规模项目之所以失败,是因为没有——也不可能——充分理解复杂的相互依存关系,有利于使事物清晰易读。它将我们引向巴西利亚这样的城市:设计合理,却缺乏内在生活。
创成式设计面临同样的问题。乔尔·史密斯的实验研究项目“进化平面图”强调了这一点。该项目探索了一所小学的推测性、优化的平面布局——考虑了交通流量、材料使用和消防通道等因素。这个项目很有创意,但是我们应该对这种想法如何应用于现实世界持谨慎态度。像儿童之间的社会互动这样的事情,是无法通过量化来完全理解的。
Evolving Floorplans (2018) by Joel Smith. An elementary school. Left: Optimized for minimizing traffic flow between classes and material usage. Right: Also optimized for minimizing fire escape paths. Source: Joelsmith.net
对于这个问题,似乎确实有一个明确的解决方案:一个深思熟虑的设计过程。生成式设计软件的创造者之一 Autodesk 就用它的软件设计了它的多伦多办公室。通过提出一个框架良好的问题,让员工参与进来,并仔细策划解决方案,他们找到了一个原始设计,这是任何人和计算机都无法独立构思的。
Toronto Office (2018) by Autodesk. Source: YouTube
然而,如果我们生活中的许多事情都是例证的话,深思熟虑的设计远非无处不在。生成式设计技术只会基于可以测量的东西进行设计,而忽略不可测量的东西太容易了。在生成性设计的所有产品和服务中培养对这些考虑的警觉可能是一个持续的挑战。
综上
生成式设计技术是超级工具,它引入了一种围绕框架问题、观察难以理解的操作和管理结果的新设计实践。他们通常像外星人一样的输出反映了他们外星人的内部运作,莫名其妙地将过去和假设的碎片编织在一起。它们非常有用。
对我们当前社会系统中的生成设计产品的简要审视表明,工具、规则或法律可能需要改变或再生,以激发未来的利益并减轻伤害。
**喜欢你读的书吗?**关注我中、 LinkedIn ,或者 Twitter 。
不平衡的类:第 1 部分
避免分类中的不平衡分类陷阱
在最近的一个数据科学项目中,我开发了一个监督学习模型,对度假屋网站 Airbnb 的首次用户的预订位置进行分类。作为 2015 年 Kaggle 竞赛的一部分,该数据集可在 Kaggle 上获得。
在我的项目中,我决定将用户分为两组:一组是在美国和加拿大境内预订首次旅行的用户,另一组是在国际其他地方预订首次旅行的用户,这实质上是将问题转化为一个二元分类问题。听起来很简单,对吧?
问题是分类目标(预订位置)非常不平衡。近 75%的首次用户预订了美国和加拿大境内的旅行。在我的初始模型显示了有希望的结果之后,对模型性能度量的进一步检查突出了一个关键问题,即当试图用不平衡的类大小执行二进制分类时。这篇文章旨在强调在构建具有不平衡类的分类模型时要注意的一些陷阱,并强调一些处理这些问题的方法。
数据
这张图显示了我的目标群体中固有的严重不平衡:
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
import pickle
import seaborn as snsdf = pd.read_pickle('data_for_regression_20k.pkl')sns.set_style("white")
dests2 = df.groupby('country_USA_World_bi').agg({'country_USA_World_bi':['count']}).reset_index()
dests2.columns = ['dest', 'count']
dests2['pct'] = dests2['count']*100/(sum(dests2['count']))x = dests2['dest']
y = dests2['pct']
palette = ['olive','mediumvioletred']fig, ax = plt.subplots(figsize = (8,4))
fig = sns.barplot(y, x, estimator = sum, ci = None, orient='h', palette=palette)
y_lab = ['USA/Canada', 'International']
ax.set_yticklabels(labels=y_lab, ha='right')for i, v in enumerate(y):
ax.text(v - 15, i + .05, str(int(v)+.5)+'%', color='white', fontweight='bold')plt.title('Country Destinations as Percent of Total Bookings',size = 16, weight = 'bold')
plt.ylabel('Country')
plt.xlabel('Percent of total');
Close to 75% of users booked vacation rental in the U.S.A. and Canada
在将逻辑回归分类器应用于我的数据之前,我将数据分为训练集(80%)和测试集(20%)。由于国际旅行者的代表性不足,我使用了分层参数来确保两个目标类都在测试集中得到了体现。然后,我使用一个标准标量对训练和测试特性集进行了标准化。
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import precision_score, recall_score, precision_recall_curve,f1_score, fbeta_score, make_scorery = df['country_USA_World_bi']
X = df.drop(['country_dest_id','country_USA_World_bi','month_created', 'day_created', 'month_active', 'day_active'], axis = 1)Xtrain, Xtest, ytrain, ytest = train_test_split(X, y, test_size=0.2, stratify=y,random_state = 88)std_scale = StandardScaler()
X_train_scaled = std_scale.fit_transform(Xtrain)
X_test_scaled = std_scale.transform(Xtest)
我运行的第一个模型是逻辑回归,因为逻辑回归包括特征系数(这有助于可解释性)。我用默认的 hypterparameters 拟合了一个初始模型,并惊喜地发现,在任何 hypter parameter 调整之前,该模型有 75%的准确性。很自然地,我发现自己在想这是不是好得不像真的。回想 75%的用户在美国/加拿大境内旅行,难道我每次只需猜测目的地就能有 75%的准确率吗?
事实上,模型精度(预测的国际预订实际上是国际预订的比例)、模型召回率(模型正确识别的国际预订的比例)和 f1 分数(两者的平衡)都非常差。
出于这个项目的目的,我对回忆分数最感兴趣,因为我认为模型能够准确预测将进行国际旅行的用户是最有用的(因为那些进行国际旅行的用户更有可能是具有较大旅行预算的狂热旅行者)。然而,鉴于最初的回忆分数只有 0.01,我还有很长的路要走,以改善这个模型!
from sklearn.linear_model import LogisticRegressiondef fit_logistic_regression_classifier(X_training_set, y_training_set):
logreg = LogisticRegression(random_state=88)
model = logreg.fit(X_training_set, y_training_set)
y_pred = model.predict(X_test_scaled)
print('accuracy = ',model.score(X_test_scaled, ytest).round(2),
'precision = ',precision_score(ytest, y_pred).round(2),
'recall = ',recall_score(ytest, y_pred).round(2),
'f1_score = ',f1_score(ytest, y_pred).round(2)
)
return(y_pred)
y_pred = fit_logistic_regression_classifier(X_train_scaled, ytrain)
混淆矩阵是一个很好的工具,可以形象化模型被混淆的程度。在 sklearn 中的混淆矩阵给出了根据实际类别预测的每个类别中观察值数量的原始值计数。plot_confusion_matrix()函数给出了每个实际类和预测类中值的百分比的可视化表示。
import itertools
from sklearn.metrics import confusion_matrix
def make_confusion_matrix(cm, classes,title='Confusion matrix',cmap=plt.cm.Blues):
print(cm)
# Normalize values
cm = cm.astype('float')*100 / cm.sum(axis=1)[:, np.newaxis]
plt.imshow(cm, interpolation='nearest', cmap=cmap)
plt.title(title)
plt.colorbar()
tick_marks = np.arange(len(classes))
plt.xticks(tick_marks, classes, rotation=45)
plt.yticks(tick_marks, classes)fmt = '.2f'
thresh = cm.max() / 2.
for i, j in itertools.product(range(cm.shape[0]), range(cm.shape[1])):
plt.text(j, i, format(cm[i, j], fmt),
horizontalalignment="center",
color="white" if cm[i, j] > 50 else "black")
plt.ylabel('True label')
plt.xlabel('Predicted label')
plt.tight_layout()
def plot_confusion_matrix(y_test_set, y_pred):
class_names = ['Domestic','International']
cnf_matrix = confusion_matrix(y_test_set, y_pred)
np.set_printoptions(precision=2)
plt.figure()
make_confusion_matrix(cnf_matrix, classes=class_names, title='Confusion matrix with normalization');
该图表明我的直觉是正确的——该模型将几乎 100%的观察结果分类为国内旅行者,因此有 75%的时间达到了目标!
plot_confusion_matrix(ytest, y_pred)
1。随机过采样
不平衡学习库包括多种方法来重新平衡类别,以获得更准确的预测能力。我尝试的方法叫做随机过采样。根据文档,“随机过采样可用于重复一些样本,并平衡数据集之间的样本数量。”基本上,这种重新平衡方法使用目标类的随机抽样和替换来获得训练集中每个类的平衡表示。事实上,在将随机抽样器应用于我的训练集之后,我在每个目标类中都有 12,743 个观察样本,而我的基线场景是 3,186 个国内预订和 1,088 个国际预订。
from imblearn.over_sampling import RandomOverSamplerros = RandomOverSampler(random_state=88)
X_resampled, y_resampled = ros.fit_sample(X_train_scaled, ytrain)yvals, counts = np.unique(ytest, return_counts=True)
yvals_ros, counts_ros = np.unique(y_resampled, return_counts=True)
print('Classes in test set:',dict(zip(yvals, counts)),'\n',
'Classes in rebalanced test set:',dict(zip(yvals_ros, counts_ros)))
和以前一样,我用默认参数拟合了一个逻辑回归分类器,并观察了模型的性能指标和混淆矩阵。由于该模型不再能在 75%的时间内正确猜测国内位置,其性能显著下降:准确率降至 54%。
然而,回想一下,我的兴趣指标增加了——从 0.01 增加到 0.58。在测试集中提供平衡的班级规模显著提高了模型预测少数民族班级的能力(在我的例子中,Airbnb 在国际地点的预订)。
y_pred_ros = fit_logistic_regression_classifier(X_resampled, y_resampled)
plot_confusion_matrix(ytest, y_pred_ros)
2。SMOTE 和 ADASYN
合成少数过采样技术(SMOTE)和自适应合成(ADASYN)是对少数类进行过采样的另外两种方法。与对现有观测值进行过采样的随机过采样不同,SMOTE 和 ADASYN 使用插值在少数类的现有观测值附近创建新观测值。
对于 SMOTE 重新平衡,我使用了 SMOTENC 对象,因为我的大多数特征(除了六个之外)都是非连续的(即分类的)特征。就像以前一样,我最终得到了一套平衡的训练。
ADASYN 给了我一个新的培训集,其中约 49%的旅行者去了美国/加拿大,51%的旅行者去了国外。这种(微不足道的)不平衡是由于 ADASYN 根据难度的加权分布在困难点周围创建新数据点的方式造成的(见 he 等人,2008 )。
就像随机过采样一样,模型对所有目的地进行分类的能力(准确性)会随着过采样而下降。另一方面,SMOTE 和 ADASYN 都提高了模型对少数类的分类能力(回忆)。
from imblearn.over_sampling import SMOTENC, ADASYN
smote_nc = SMOTENC(categorical_features=list(np.arange(7,80)), random_state=88)
X_smoted, y_smoted = smote_nc.fit_resample(X_train_scaled, ytrain)adasyn = ADASYN(random_state=88)
X_adasyn, y_adasyn = adasyn.fit_resample(X_train_scaled, ytrain)yvals, counts = np.unique(ytest, return_counts=True)
yvals_smt, counts_smt = np.unique(y_smoted, return_counts=True)
yvals_ads, counts_ads = np.unique(y_adasyn, return_counts=True)print('Classes in test set:',dict(zip(yvals, counts)),'\n',
'Classes in rebalanced test set with SMOTENC:',dict(zip(yvals_smt, counts_smt)),'\n',
'Classes in rebalanced test set with ADASYN:',dict(zip(yvals_ads, counts_ads)))
y_pred_smt = fit_logistic_regression_classifier(X_smoted, y_smoted)
plot_confusion_matrix(ytest, y_pred_smt)
y_pred_ads = fit_logistic_regression_classifier(X_adasyn, y_adasyn)
plot_confusion_matrix(ytest, y_pred_ads)
3。平衡类网格搜索
由于采用 ADASYN 过采样的基线模型在召回率方面表现最佳,因此我对这个测试集进行了网格搜索,以找到进一步优化模型性能的参数。
from sklearn.model_selection import GridSearchCV
grid = {"C":np.logspace(-3,3,7), "penalty":["l1","l2"]}# l1 lasso l2 ridge
logreg = LogisticRegression(random_state=88)
logreg_cv = GridSearchCV(logreg,grid,cv=5,scoring='recall')
logreg_cv.fit(X_adasyn, y_adasyn)
print("tuned hpyerparameters :(best parameters) ", logreg_cv.best_params_)
具有 0.001 的 C 参数和 L2 正则化惩罚的逻辑回归模型具有 0.65 的改进的回忆分数。这意味着该模型能够有效地抓住 65%的将在国际上预订 Airbnbs 的新用户。
y_pred_cv = logreg_cv.predict(X_test_scaled)
print('accuracy = ',logreg_cv.score(X_test_scaled, ytest).round(2),
'precision = ',precision_score(ytest, y_pred_cv).round(2),
'recall = ',recall_score(ytest, y_pred_cv).round(2),
'f1_score = ',f1_score(ytest, y_pred_cv).round(2)
)
plot_confusion_matrix(ytest, y_pred_cv)
虽然平衡类和超参数调整显著提高了模型的召回分数,但模型精度仍然很低,只有 0.3。这意味着,只有 30%被归类为国际旅行者的用户实际上在国际上预订 Airbnbs。在商业环境中,像这样的模型可以用于根据预测的预订目的地通知度假屋的定向广告。这意味着 70%收到建议的用户,比如说,可以俯瞰埃菲尔铁塔的房子,实际上会考虑在国内旅行。这种错误定位不仅与该集团无关,而且未能向美国/加拿大集团传播相关广告可能意味着随着时间的推移会损失收入。
现在,我已经通过对少数类进行过采样解决了模型性能的高估问题,接下来的步骤可能包括额外的特征工程,以梳理出更多的信号并拟合替代分类算法(如 K-最近邻或随机森林分类器)。
结论
在这个例子中,一旦我重新平衡了目标类的大小,模型的准确性就会显著下降。即使在使用 gridsearch 交叉验证进行超参数调整后,逻辑回归模型的准确性也比具有不平衡类别的基线模型低 10 个百分点。
这个例子说明了考虑类别不平衡的重要性,以避免高估分类模型的准确性。我还用工作代码概述了通过过采样(随机过采样、SMOTE 和 ADASYN)重新平衡类的三种技术。关于每种技术的更多信息可以在不平衡学习文档中找到。
不平衡的班级:第 2 部分
避免分类中的不平衡分类陷阱
R 最近,我写了这篇文章关于分类模型中不平衡的班级规模可能会导致分类模型的性能被高估。这篇文章讨论了我正在使用来自 Kaggle 的 Airbnb 首次用户预订数据开发的一个分类项目。该项目的目标是预测首次使用 Airbnb 的用户是否会在美国/加拿大或国际上的某个地方预订度假屋。然而,美国/加拿大境内的预订量约占数据的 75%,因此很难准确估计国际预订量。
考虑到几乎 100%的观察值被预测为主导类(在我的例子中,是去美国/加拿大的旅行者),我使用自适应合成(ADASYN)对测试集中的少数类进行过采样。由于对使用默认参数的开箱即用逻辑回归的结果不满意,我决定使用 scikit-learn 中的 GridSearchCV 进行一些模型选择和强力参数调整。我的特性是工程化的,我的类是平衡的,我闪亮的新 AWS 实例是计算优化的。什么会出错?
数据预处理
我急切地想拟合一些模型,看看我能根据已获得的特征对用户位置进行多好的分类。我已经将数据框架分成了目标变量(y)和特征矩阵(X)。我对数据集执行了训练-测试-分割(80%训练,20%测试),并进行分层,以确保少数类在测试集中得到代表。我在特征集(X_train 和 X_test)上使用了标准的标量转换。最后,我使用了自适应合成(ADASYN)方法对训练数据中的少数类进行过采样(详见不平衡类第一部分)。
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from imblearn.over_sampling import ADASYNX_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2,
stratify=y,random_state = 88)
std_scale = StandardScaler()
X_train_scaled = std_scale.fit_transform(X_train)
X_test_scaled = std_scale.transform(X_test)adasyn = ADASYN(random_state=88)
X_adasyn, y_adasyn = adasyn.fit_resample(X_train_scaled, y_train)
GridSearchCV
我循环了五个分类器:逻辑回归、K 近邻、决策树、随机森林和支持向量分类器。我将“模型”定义为带有分类器对象的每个分类器的字典列表(为了再现性,随机状态总是设置为 88,你能猜出我最喜欢的数字吗?),以及要调整的特定于模型的超参数网格。
from sklearn.linear_model import LogisticRegression
from sklearn.neighbors import KNeighborsClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.svm import SVCmodels = [{'name': 'logreg','label': 'Logistic Regression',
'classifier': LogisticRegression(random_state=88),
'grid': {"C":np.logspace(-3,3,7), "penalty":["l1","l2"]}},
{'name': 'knn','label':'K Nearest Neighbors',
'classifier':KNeighborsClassifier(),
'grid': {"n_neighbors":np.arange(8)+1}},
{'name': 'dsc','label': 'Descision Tree',
'classifier': DecisionTreeClassifier(random_state=88),
'grid': {"max_depth":np.arange(8)+1}},
{'name': 'rf', 'label': 'Random Forest',
'classifier': RandomForestClassifier(random_state=88),
'grid': {'n_estimators': [200, 500],'max_features': ['auto', 'sqrt', 'log2'],
'max_depth' : [4,5,6,7,8],'criterion' :['gini', 'entropy']}},
{'name': 'svm_rbf', 'label': 'SVC (RBF)',
'classifier':SVC(random_state=88),
'grid': {'C': [1, 10, 100, 1000], 'gamma': [0.001, 0.0001], 'kernel': ['rbf']}}]
我在下面定义了 model_selection 函数来执行网格搜索,以优化给定模型的五个交叉验证集中的 hypterparameters。我决定使用受试者操作特征曲线(ROC AUC)分数下的计算面积来确定模型性能,以便尝试最大化真阳性,同时最小化模型预测中的假阳性。该函数返回一个字典,其中包括分类器、GridSearch 中的最佳参数以及验证集中的最佳平均 ROC_AUC 分数。
from sklearn.metrics import roc_auc_score
def model_selection(classifier, name, grid, X_train, y_train, scoring):
gridsearch_cv=GridSearchCV(classifier,
grid,
cv=5,
scoring = scoring)
gridsearch_cv.fit(X_adasyn, y_adasyn)
results_dict = {}
results_dict['classifier_name'] = name
results_dict['classifier'] = gridsearch_cv.best_estimator_
results_dict['best_params'] = gridsearch_cv.best_params_
results_dict['ROC_AUC'] = gridsearch_cv.best_score_
return(results_dict)results = []
for m in models:
print(m['name'])
results.append(fit_first_model(m['classifier'],
m['name'],
m['grid'],
X_adasyn,
y_adasyn,
'roc_auc'))
print('completed')
最后,我将每个分类器的结果进行了比较!这个过程花费的时间并不多,所以要准备好消磨一些时间(这个过程结束时,我的厨房一尘不染!).
我将结果放入数据框中,以便更好地查看:
results_df = pd.DataFrame(results).sort_values(by='ROC_AUC', ascending = False)
GridSearch ross-validation results across several classifiers with optimized parameters
嗯,我对第一行非常满意,随机森林分类器获得了 0.85 的 ROC_AUC 分数。这让我想到了 XGBoost 树分类器的一些宏伟计划。我将得到一个预测模型来与 Kaggle 竞赛的获胜者竞争…或者我是吗?
为了确认随机森林分类器的性能,我在测试集上对模型的预测性能进行了评分。当我看到测试集的 ROC_AUC 分数只有 0.525 时,我惊呆了。根据我的经验,测试分数通常低于交叉验证分数,我知道随机森林可能容易过度拟合,但这是 38%的性能下降!
为了更好地衡量,我对测试集上其余四个分类器的性能进行了评分。果然,ROC_AUC 测试分数明显低于交叉验证 ROC_AUC 平均值。逻辑回归是个例外,在验证集和测试集上的表现都比随机猜测略好。
Average cross-validation ROC_AUC scores are well above test scores
订单事项
这里发生了什么事?嗯,请记住,在分割我的训练数据进行交叉验证之前,我用 ADASYN 进行了过采样。所以,我的五重验证集并不代表现实世界中的分布。相反,它们包含代表少数类中难以分类的观察的“合成”数据点。因此,当我在测试集上对模型性能进行评分时(目标类比例代表真实世界),分数显著下降。
幸运的是,不平衡学习有一个管道类,它将只在分类器拟合期间应用 ADASYN 重采样,从而允许我避免一些笨拙的 for 循环和手动 GridSearchCV。
下面是在交叉验证拟合期间使用过采样在随机森林分类器上构建 GridSearchCV 超参数调整管道的代码。(注意网格字典里的 class__ 前缀!)
from imblearn.pipeline import make_pipeline, Pipelinerf = RandomForestClassifier(random_state=88)
adasyn = ADASYN(random_state=88)grid = {'class__n_estimators': [200, 500],
'class__max_features': ['auto', 'sqrt', 'log2'],
'class__max_depth' : [4,5,6,7,8],
'class__criterion' :['gini', 'entropy']}pipeline = Pipeline([('sampling', adasyn), ('class', rf)])grid_cv = GridSearchCV(pipeline, grid, scoring = 'roc_auc', cv = 5)
grid_cv.fit(X_train_scaled, y_train)grid_cv.best_score_
你瞧,交叉验证的 ROC_AUC 只有 0.578,更能说明模型在应用于测试集时的实际表现。我又一次遍历了所有模型,保存了最佳参数、平均验证分数和测试分数,以确保完整性。
Average cross-validation roc_auc scores with pipeline oversampling are far more similar to test set scores
关键要点
在验证集中使用合成数据的分类算法的 ROC-AUC 曲线(左)表明,与在具有代表性不平衡类别的验证集中评分的 ROC-AUC 曲线(右)相比,明显高估了拟合度。(注意:这些算法是在单个验证集上评分的,而不是三次的平均值,因此得分与上表不同)。
ROC-AUC Curves
虽然我无法自信地预测 Airbnb 用户的度假目的地,但这个项目说明了关注高级机器学习算法“幕后”发生的事情的重要性。特别是,我在之前关于不平衡的类规模如何导致分类器性能被高估的文章的基础上,讨论了为什么在使用交叉验证和超参数调整对重新采样的训练数据拟合分类器时,顺序很重要。
我希望这个系列对您有用。请随时在下面的评论中提供任何意见或额外的见解!
公共/私人区块链中的不变性—第 2 部分
Photo by marcos mayer on Unsplash
在第 1 部分中,我们讨论了什么是不变性,在什么情况下它是重要的,以及所有这些与区块链的关系。
关于区块链不变性的进一步讨论涉及到具体的实现。在这一部分,我要谈一谈公共的和私人的区块链;具体来说,比特币和 Hyperledger Fabric。公共与私人的区别更丰富了讨论。
比特币不变性
根据我们设定的起点,分析比特币不变性时会涉及很多内容。试图从零开始涵盖这个主题将导致一篇文章的一个真正情有可原的解释。
关于比特币不变性,最常见的困惑之一是,既然区块被链接在一起,那么区块链就是不可变的。这不是真正正确的;真正让比特币不可改变的是工作证明。
工作证明
工作证明(PoW),你可能知道,不是为比特币发明的东西。顾名思义,工作证明是工作被执行的证明。一个非常抽象的定义将是:一个工具,证明足够的努力致力于某事。特别是,防止拒绝服务攻击很有意义。
所以有三个有趣的问题:
- 为什么比特币需要能量?
- 谁在工作?
- 他们为什么这么做?
要理解这些问题,我们应该记住,比特币是一个公共区块链,因此任何人都可以在网络中扮演任何角色。正如在任何一种公共系统中一样,参与者之间的信任程度是有限的。在比特币中,执行 PoW 算法的节点被称为矿工,他们的角色是为链提出新的区块。
因为任何人都可以成为矿工、在任何时间点都有多个竞争区块被开采。由于每个挖掘器可以决定在块中包含哪些未确认的事务,因此我们将有许多可能的块,每个块都有不同的事务。那么,如果网络有多种可能的选择,它如何决定哪一个是下一个块呢?换句话说,网络如何达成共识?一个解决办法是让创造过程足够艰难。
如果生成一个有效的块足够困难,以至于所有的矿工作为一个整体只能生成一个,比如说,在几分钟的时间里,那么我们就不会有从许多中选择一个的问题。这就像一个硬彩票,检查 102 个中奖者比检查 3000 个中奖者更容易。此外,这不仅是一个难题,寻找解决方案也很昂贵。既然也贵,矿工在向网络广播无效块之前会三思。此外,矿工 T21 可以证明工作已经完成,这是无可争议的。
这并没有以确定性的方式解决一致性问题,但足以在网络中提供可靠的一致性保证。选择正确的硬度是分叉概率、网络负载和确认速度之间的权衡。
但是对于一个矿工来说,权力到底意味着什么呢?在比特币方面,矿工应该找到通常被称为密码难题的解决方案。这个难题包括找到一个数,使得新的块散列值低于某个阈值。这个号是的一个证明矿工按预期执行了 PoW。当验证一个新的块时,这个号不满足这个条件的事实足以认为它是无效的。
为什么是这个谜题而不是另一个?因为我们很确定唯一的解决方法就是暴力。它没有捷径;是骗不了的。此外,网络将自动调整其难度,即前面提到的阈值,以响应网络散列能力的增加或减少,从而保持块创建的定义速度。
我们可以想象,也许矿工会为了在网络中达成共识而解决这个昂贵的问题,但这太天真了,不是因为矿工邪恶,而是因为他们理性。激励矿工的真正原因是激励。
当采矿者成功开采一个区块时,它将获得奖励以及该区块内包含的交易的交易费用的总和;都在比特币里。这些激励在供应链的生命周期内是动态的。
奖励是确定性的,每 210.000 个块减半,交易费用可以根据受未决交易池的大小或某种外源交易紧急程度影响的需求而变化。
关于激励和权力是如何交织在一起使系统变得安全和可持续的,有很多话要说。这部分制度设计更多的是心理学,经济学,博弈论。在一个不可信的网络中,你不能假设网络参与者总是按照预期行事,或者有不同于他们自己的其他兴趣。
PoW 作为一种共识机制,但是它和不可变性有什么关系呢?为了理解这一点,我们应该考虑比特币中的节点应该遵循的一个重要规则:最长的链是当前链。当前链是什么概念?。
假设链中的最后一个块是 100 号。由于在矿工之间没有任何协调来生成块号 101,事实上,他们在比赛/抽奖成为奖励的所有者,所以有可能他们中的两个大约同时生成有效的 101 块(比如 101a 和 101b)。我所说的“同时”是指两个块都被广播到网络,并且对于附加到其当前链的其他节点来说是完全有效的选项。
在这种情况下,网络中的节点可以接收这两个有效块,在链中有一个分叉。挡块 101a 是指 a 叉中的挡块编号 101, 101b 是指 b 叉中的挡块编号 101。
起初,这个节点持有两条链并等待一段时间。如果几分钟后生成了一个 102a 块(带有父块 101a ,则发生以下情况:
- 包含块 102a 的链被认为是当前链,因为它是已知的最长的链。(链条长度也称为高度
- 具有 101b 的前叉被分开放置,不被视为当前链条。
这对矿工来说有着重要的意义,因为他们可以选择在现有的链条上采矿,也可以尝试在叉子上继续挖掘。因为最长的链是当前的链,所以在另一个分支上提交工作是有风险的,因为采矿的任何回报在现实中没有任何价值。
最明显的选择是在最长的链上挖掘,因为这个链更有可能继续是最长的。如果矿工坚持在滞后的叉/链上采矿,则该叉有可能永远赶不上当前的链长度。因此,为获得奖励和收取费用而参与发电区块发电的所有工作都完全浪费了。
此外,由于每个理性的矿工会选择这个选项,有更多的矿工开采最长的链,因此新块的当前链速度比任何其他链快几个数量级的可能性更高。你可以计算一下,发现追上最长链的概率随着链间长度的不同而呈指数下降。
决定在哪里挖掘未来的块与不变性无关,因为我们考虑的是追加,而不是修改历史。但事实证明,试图修改历史是在有意构建当前链条的分叉。
同样,如果当前区块是 100 号,并且我们试图生成另一个有效区块 90,我们应该与具有 11 个区块滞后的最长链竞争,并且让每个矿工联合起来反对我们在最长链上采矿。所以这是一个巨大的挑战。
我们应该对 11 个街区(从 90 号街区到 100 号街区)供电。这意味着我们自己解决电力问题,导致大量的电力成本,并在这个过程中,在一个仍然不被认为是当前的链上产生回报,因此在现实中仍然没有价值。
当我们再次生成第 100 个块时,我们意识到在这段时间内,其他矿工在开采原始链,现在链长度差异很可能比我们开始时更大。
我们应该有超过 50%的网络散列能力,以真正最终赶上一个分叉链,成为当前链。这取决于我们的散列能力超过网络的散列能力多少,以及你想要修改的块有多旧。这叫做 51%攻击。
如果我们只有不到 50%的网络散列能力,那么我们很可能永远也赶不上,因此所有投入到分叉点的电力(因此,成本)都将被完全浪费。
当然,我这里是简化;在中本聪撰写的原创比特币论文第 11 章中有更详细的计算。个人推荐这篇优秀的论文,更详细的了解中本聪的计算和验证。
从整体上考虑网络的哈希能力,任何种类的实体都有能力攻击网络的不变性,这种可能性微乎其微。这就是为什么有时你可能会听,这是一个很好的做法,等待 6 块,大约 1 小时,在您的交易确认后,以确保它是不可变的。你想更安全吗?等 12 个,或者更多。如果你对确切的概率感兴趣,这个计算器很有用。
超分类帐结构不变性
在织物的情况下,不变性分析要简单得多,因为它是一个许可的区块链。由于在网络中有更多的信任,解决这个问题需要更少的工件。
In 结构块由订购服务生成。它的职责是建立网络中交易的总秩序*。与比特币一样,块包含交易,每个块包含前一个块的哈希值。*
一个通用的生产就绪订购服务由一个 Apache Kafka 集群和一个或多个订购者组成。每个想要发送交易提议的参与者通过 broadcast 接口将其发送给任何一个order*,然后order将其作为 Kafka 中的一个消息推送到一个映射到交易的通道的分区主题中。*
Kafka 完成了为每个通道建立事务总排序的繁重工作,因为每个通道都有一个到主题的一对一映射,而主题只有一个分区。在 Kafka 中,每个分区每个主题的消息保证有一个定义良好的顺序。
在事务在主题上之后,排序器读取分区以生成下一个块。块生成在通道配置中定义了一些确定性规则,它基于块的总大小或基于时间的标准。在后者中,使用来自 Kafka 的元数据,因此不同的排序者将选择包含在块中的相同的有序交易集。
你可能认为这是一个事件源系统,其中任何规则都是基于确定性数据和函数的。事实上,如果所有节点从 Fabric 中消失,但 Kafka 集群仍然存在,我们可以从头开始重建所有块。你的 Kafka 集群是网络安全最重要的组成部分。
然后一个排序器创建一个包含一定数量事务的块,但是这里没有 PoW。为了使该块有效,它应该由一个订购者签名。如果块由来自订购服务的任何订购者签名,则块被视为有效。
对等点是网络中通过交付接口与排序器接收块的节点。他们验证事务并将其标记为有效或无效(考虑到读写集,以及一系列其他标准),然后修改 CouchDB 或 LevelDB 中的世界状态,并明显地将块附加到分类帐。
通过通道配置,维护通道分类帐副本的对等方知道哪个是订购服务的 CAs ,因此它可以检查新块是否来自可信来源。如您所见,块生成依赖于对来自订购服务的 CA 的信任。
很明显,如果任何一个订购者或 CA 的私钥被泄露,或者不知何故凭空产生了一个新的订购者,那么我们就有大麻烦了。
首先,我们可以想象恶意方可能会生成新的块,并开始混淆网络中的不同对等点,使它们的分类帐出现分歧。此外,如果恶意方作为对等节点参与,那么它可以修改自己的分类帐,因为制作新块就像制作签名一样容易。这包括修改以前存在的任何块。它有改写账本历史的力量。可以进行的修改是块内交易的审查或重组,因为每笔交易都是由客户签名的,不能被篡改。
这听起来很严重…但是如果我们有足够的时间和大量的现金来支付电费,我们也可以改写历史。那有什么区别呢?。在织物中,如果我能以某种方式破解任何订购者的私钥,那么与比特币中永恒而昂贵的程序相比,重写历史是非常便宜和快速的;但是如何让其他同事相信我修改过的账本是真正的账本呢?如果我不能说服别人,我在现实世界中也不能产生太大的影响。在织物如果你是唯一一个有一个有效的,但不同的分类帐相比,世界上其他地方,有些事情听起来很不对劲。
在比特币中,这涉及到以某种方式拥有比当前更长的链,如果我们没有接近网络散列能力的 50%,这将是非常困难的。在 Fabric 的情况下,这实际上是不可能的,原因很简单:分类帐中的分叉被认为是关键情况,节点不会接受它们。这种情况被认为是恶意攻击或系统中的错误。
您可能会想:既然这与块篡改的难度无关,那么在 Fabric 中链接块的目的是什么?在比特币中,区块链是必须的,因为它迫使恶意节点不仅挖掘修改过的区块,还挖掘所有后续的区块。
事实证明,在 Fabric 中,块链接对于保持分类帐的不变性并不重要。该实用程序使分类帐变得显窃启*,这意味着很容易检查分类帐是否被修改。如果一个诚实的节点从另一个节点接收到一个块,而它的 hash 与另一个块不匹配(在同一高度),那么我们可以确定它对应的是一个不同的分类帐,出现了危急情况。我们可以进一步调查哪个块是真正的差异,但这个问题可以通过简单快速的检查来检测。*
结论
比特币实现不变性的方式与 Hyperledger Fabric 完全不同。当我们谈到区块链时,我们看到一个重要的特性,那就是数据块是用哈希链接起来的。
在比特币中,它对账本不变性的重要性是显而易见的,因为它与不变性的硬度直接相关。我想要修改的块越老,就越难生成足够高的链来说服网络的其余部分。
在 Fabric 中,块链接与篡改分类帐的难度无关;考虑到一个订购者的私钥被泄露,这可以在几秒钟内完成。尽管如此,说服另一个节点改变它的分类帐是不被设计所接受的;叉子是不被接受的,应该需要人工干预,因为这是出了问题的征兆。
本系列涵盖了许多值得更详细讨论的主题,但我希望您会发现它很有用。
Python 中的不可变数据类型与可变数据类型
了解可变数据类型和不可变数据类型之间的区别,以及如何找出哪个是哪个!
到现在为止,你可能听说过“Python 中的一切都是对象”这句话。对象是数据的抽象,Python 有多种多样的数据结构,你可以用它们来表示数据,或者组合它们来创建你自己的定制数据。
Python 对数据的第一个基本区别是对象的值是否会改变。如果值可以改变,对象称为可变,如果值不能改变,对象称为不可变。
在这个速成课程中,我们将探索:
- 可变类型和不可变类型的区别
- 不同的数据类型以及如何确定它们是可变的还是不可变的
理解可变和不可变之间的区别非常重要,因为它会影响您编写的代码。
我们开始吧!
这个速成课程改编自 Next Tech 的学习 Python 编程课程,该课程使用理论和实践的混合来探索 Python 及其特性,并从 Python 初学者进步到熟练。它包括一个浏览器内沙盒环境,预装了所有必要的软件和库。这里可以免费上手!
可变与不可变
首先,理解 Python 中的每个对象都有一个 ID(或标识)、一个类型和值是很重要的,如下面的代码片段所示:
age = 42
print(id(age)) # id
print(type(age)) # type
print(age) # value [Out:]
10966208
<class ‘int’>
42
一旦创建,对象的 ID 永远不会改变。这是它的唯一标识符,当我们想要使用它时,Python 在幕后使用它来检索对象。
类型也永远不会改变。类型告诉对象支持哪些操作,以及可以分配给它的可能值。
该值可以更改,也可以不更改。如果可以,则称该对象是可变的,如果不能,则称该对象是不可变的。
让我们来看一个例子:
age = 42
print(id(age))
print(type(age))
print(age)age = 43
print(age)
print(id(age)) [Out:]
10966208
<class ‘int’>
42
43
10966240
age
的值变了吗?嗯,【T1 号】是一个整数,类型为int
,是不可变的。所以,实际上在第一行,age
是一个名字,它指向一个int
对象,它的值是42
。
当我们键入age = 43
时,所发生的是另一个对象被创建,类型为int
,值为43
(同样,id
也会不同),名字age
被设置为指向它。所以,我们没有把那个42
改成43
。我们实际上只是将age
指向了一个不同的位置。
从第二个名为age
的对象创建前后的打印id(age)
可以看出,它们是不同的。
现在,让我们看看使用可变对象的同一个例子。
x = [1, 2, 3]
print(x)
print(id(x))x.pop()
print(x)
print(id(x)) [Out:]
[1, 2, 3]
139912816421064
[1, 2]
139912816421064
对于这个例子,我们创建了一个名为m
的列表,其中包含 3 个整数1
、2
和3
。在我们通过“弹出”最后一个值3
来改变m
之后,m
的 ID 保持不变!
因此,int
类型的对象是不可变的,而list
类型的对象是可变的。现在让我们讨论其他不可变和可变的数据类型!
可变数据类型
可变序列在创建后可以改变。Python 的一些可变数据类型有:列表、字节数组、集合和字典。
列表
如前所述,列表是可变的。这里是另一个使用append()
方法的例子:
a = list(('apple', 'banana', 'clementine'))
print(id(a))a.append('dates')
print(id(a)) [Out:]
140372445629448
140372445629448
字节数组
字节数组代表了bytes
对象的可变版本。它们公开了大多数可变序列的常用方法以及大多数bytes
类型的方法。项目是范围[0,256]内的整数。
让我们看一个简单的例子,用bytearray
类型来说明它是可变的:
b = bytearray(b'python')
print(id(b))b.replace(b'p', b'P')
print(id(b)) [Out:]
139963525979808
139963525979808
设置
Python 提供了两种集合类型,set
和frozenset
。它们是不可变对象的无序集合。
c = set((‘San Francisco’, ‘Sydney’, ‘Sapporo’))
print(id(c))
c.pop()
print(id(c)) [Out:]
140494031990344
140494031990344
如你所见,set
s 确实是可变的。稍后,在不可变数据类型部分,我们将看到frozenset
是不可变的。
字典
d = {
'a': 'alpha',
'b': 'bravo',
'c': 'charlie',
'd': 'delta',
'e': 'echo'
}
print(id(d))d.update({
'f': 'foxtrot'
})
print(id(d)) [Out:]
14007111431940
14007111431940
不可变数据类型
不可变数据类型与可变数据类型的不同之处在于,它们在创建后不能更改。一些不可变类型包括数字数据类型、字符串、字节、冻结集和元组。
数字数据类型
你已经看到整数是不可变的;同样,Python 的其他内置数值数据类型如布尔、浮点、复数、分数和小数也是不可变的!
字符串和字节
Python 中的文本数据是用str
对象处理的,通常称为字符串。它们是不可变的 Unicode 码位序列。Unicode 码位可以表示一个字符。
当涉及到存储文本数据或在网络上发送文本数据时,您可能希望对其进行编码,使用适合您所使用的介质的编码。编码的结果产生一个bytes
对象,其语法和行为类似于字符串。
字符串和字节都是不可变的,如下面的代码片段所示:
# string
e = 'Hello, World!'
print(id(e))e = 'Hello, Mars!'
print(id(e)) [Out:]
140595675113648
140595675113776# bytes
unicode = 'This is üŋíc0de' # unicode string: code points
print(type(unicode))
f = unicode.encode('utf-8') # utf-8 encoded version
print(type(f))
print(id(f))f = b'A bytes object' # a bytes object
print(id(f)) [Out:]
<class 'str'>
<class 'bytes'>
140595675068152
140595675461360
在字节部分,我们首先将f
定义为我们的unicode
字符串的编码版本。正如你从print(type(f))
中看到的,这是一款bytes
型。然后我们创建另一个名为f
的bytes
对象,其值为b'A bytes object'
。两个f
对象有不同的 id,这表明字节是不可变的。
冻结集
正如上一节所讨论的,frozenset
与set
相似。但是,frozenset
对象在可变对应物方面非常有限,因为它们不能被改变。尽管如此,对于成员测试、并集、交集和差运算以及性能原因,它们仍然被证明是非常有效的。
元组
我们将要看到的最后一个不可变序列类型是元组。元组是任意 Python 对象的序列。在元组中,项目由逗号分隔。这些也是不可变的,如下例所示:
g = (1, 3, 5)
print(id(g))g = (42, )
print(id(g)) [Out:]
139952252343784
139952253457184
我希望你喜欢这个速成班,它讲述了不可变对象和可变对象之间的区别,以及如何找出哪一个是对象!既然您已经理解了 Python 编程的这个基本概念,那么现在您可以探索可以用于每种数据类型的方法。
如果你想学习这方面的知识并继续提高你的 Python 技能,Next Tech 有一个完整的 学习 Python 编程 课程,该课程涵盖:
- 功能
- 条件编程
- 理解和生成器
- 装饰器、面向对象编程和迭代器
- 文件数据持久化
- 测试,包括对测试驱动开发的简要介绍
- 异常处理
- 剖析和性能
这里 可以免费上手 !