CNN 的数据预处理和网络构建
学习建立一个典型的端到端管道来训练 CNN
艾莉娜·格鲁布尼亚克在 Unsplash 上的照片
在本文中,我们将经历训练卷积神经网络的端到端管道,即组织数据到目录、预处理、数据扩充、模型建立等。
我们将在图像处理中常用的数据预处理技术上花费大量时间。这是因为在大多数深度学习项目中,预处理大约需要你 50–80%的时间,知道一些有用的技巧会对你的项目有很大帮助。我们将使用 Kaggle 的 flowers 数据集 来演示关键概念。为了直接进入代码,Kaggle 上发布了一个附带的笔记本(请使用 CPU 运行代码的初始部分,使用 GPU 进行模型训练)。
使用 Kaggle 笔记本探索和运行机器学习代码|使用来自多个数据源的数据
www.kaggle.com](https://www.kaggle.com/tanyadayanand/end-to-end-pipeline-for-training-cnns-resnet)
导入数据集
让我们从导入必要的库和加载数据集开始。这是每个数据分析过程中必不可少的一步。
***# Importing necessary libraries*** import keras
import tensorflow
from skimage import io
import os
import glob
import numpy as np
import random
import matplotlib.pyplot as plt
%matplotlib inline***# Importing and Loading the data into data frame
#class 1 - Rose, class 0- Daisy*** DATASET_PATH = '../input/flowers-recognition/flowers/'
flowers_cls = ['daisy', 'rose']
***# glob through the directory (returns a list of all file paths)*** flower_path = os.path.join(DATASET_PATH, flowers_cls[1], '*')
flower_path = glob.glob(flower_path)***# access some element (a file) from the list***
image = io.imread(flower_path[251])
数据预处理
图像—通道和尺寸
图像有不同的形状和大小**。他们也来自不同的来源**。例如,一些图像是我们所说的“自然图像”,这意味着它们是在真实世界中以颜色拍摄的。例如:
- 花的图片是自然的图像。
- x 光图像是而不是自然图像。
考虑到所有这些变化,我们需要对任何图像数据进行一些预处理。RGB 是最流行的编码格式,我们遇到的大多数“自然图像”都是 RGB 格式的。此外,数据预处理的第一步是使图像尺寸相同。让我们继续讨论如何改变图像的形状和形式。
***# plotting the original image and the RGB channels***
f, (ax1, ax2, ax3, ax4) = plt.subplots(1, 4, sharey=True)
f.set_figwidth(15)
ax1.imshow(image)
***# RGB channels*
*# CHANNELID : 0 for Red, 1 for Green, 2 for Blue.***
ax2.imshow(image[:, : , 0]) *#Red*
ax3.imshow(image[:, : , 1]) *#Green*
ax4.imshow(image[:, : , 2]) *#Blue*
f.suptitle('Different Channels of Image')
形态变换
术语形态变换是指涉及图像的形状和形式的任何修改。这些非常常用于图像分析任务。虽然它们适用于所有类型的图像,但对于非自然的图像(来自真实世界的图片以外的来源)来说,它们尤其强大。典型的变换是侵蚀、扩张、张开和闭合。现在让我们看看实现这些形态变换的一些代码。
1。阈值处理
一种更简单的操作,我们把亮度高于某个阈值的所有像素转换成 1;具有小于阈值的值的像素被转换为零。这产生了一个二进制图像*。*
***# bin_image will be a (240, 320) True/False array*
*#The range of pixel varies between 0 to 255*
*#The pixel having black is more close to 0 and pixel which is white is more close to 255*
*# 125 is Arbitrary heuristic measure halfway between 1 and 255 (the range of image pixel)***
bin_image = image[:, :, 0] > 125
plot_image([image, bin_image], cmap='gray')
2。侵蚀、扩张、打开&关闭
侵蚀缩小亮区,放大暗区。另一方面,膨胀正好相反——它缩小黑暗区域,扩大明亮区域。
开口是侵蚀后的扩张。开口可以去除小亮点(即“盐”),连接小暗裂纹。这往往会“打开”(亮)特征之间的(暗)间隙。
关闭是扩张后侵蚀。闭合可以去除小暗斑(即“辣椒”),连接小亮裂。这有助于“封闭”(亮)特征之间的(暗)间隙。
所有这些都可以使用skimage.morphology
模块来完成。基本的想法是让一个特定大小的圆盘(下面的 3)在图像周围移动,并使用它应用这些变换。
from skimage.morphology import binary_closing, binary_dilation, binary_erosion, binary_opening
from skimage.morphology import selem
***# use a disk of radius 3***
selem = selem.disk(3)
***# oprning and closing***
open_img = binary_opening(bin_image, selem)
close_img = binary_closing(bin_image, selem)
***# erosion and dilation***
eroded_img = binary_erosion(bin_image, selem)
dilated_img = binary_dilation(bin_image, selem)
plot_image([bin_image, open_img, close_img, eroded_img, dilated_img], cmap='gray')
正常化
归一化是预处理部分中最关键的步骤。这是指重新调整像素值,使它们位于一个限定的范围内。这样做的原因之一是有助于传播梯度的问题。我们将讨论多种归一化图像的方法。
***#way1-this is common technique followed in case of RGB images***
norm1_image = image/255
***#way2-in case of medical Images/non natural images***
norm2_image = image - np.min(image)/np.max(image) - np.min(image)
***#way3-in case of medical Images/non natural images***
norm3_image = image - np.percentile(image,5)/ np.percentile(image,95) - np.percentile(image,5)
plot_image([image, norm1_image, norm2_image, norm3_image], cmap='gray')
增强
这就把我们带到了数据预处理的下一个方面——数据扩充。很多时候,我们拥有的数据量不足以很好地完成分类任务。在这种情况下,我们执行数据扩充。例如,如果我们正在处理将宝石分为不同类型的数据集,我们可能没有足够数量的图像(因为高质量的图像很难获得)。在这种情况下,我们可以执行扩充来增加数据集的大小。增强通常用于基于图像的深度学习任务,以增加训练数据的数量和方差。增强只能在训练集上进行,而不能在验证集上进行。
如你所知,汇集增加了**不变性。**如果一张狗的照片在图像的左上角,通过池化,您将能够识别出狗是否在左上角的左/右/上/下。但是训练数据由像 翻转、旋转、裁剪、平移、照明、缩放、添加噪声、 等数据增强组成。模型学习所有这些变化。这大大提高了模型的准确性。因此,即使狗出现在图像的任何角落,模型也能够以很高的准确度识别它。
可能有多种类型的增强。基本类型使用以下变换类型之一来变换原始图像:
- 线性变换
- 仿射变换
from skimage import transform as tf
***# flip left-right, up-down***
image_flipr = np.fliplr(image)
image_flipud = np.flipud(image)
plot_image([image, image_flipr, image_flipud])
***# specify x and y coordinates to be used for shifting (mid points)***
shift_x, shift_y = image.shape[0]/2, image.shape[1]/2
***# translation by certain units***
matrix_to_topleft = tf.SimilarityTransform(translation=[-shift_x, -shift_y])
matrix_to_center = tf.SimilarityTransform(translation=[shift_x, shift_y])
***# rotation***
rot_transforms = tf.AffineTransform(rotation=np.deg2rad(45))
rot_matrix = matrix_to_topleft + rot_transforms + matrix_to_center
rot_image = tf.warp(image, rot_matrix)
***# scaling***
scale_transforms = tf.AffineTransform(scale=(2, 2))
scale_matrix = matrix_to_topleft + scale_transforms + matrix_to_center
scale_image_zoom_out = tf.warp(image, scale_matrix)
scale_transforms = tf.AffineTransform(scale=(0.5, 0.5))
scale_matrix = matrix_to_topleft + scale_transforms + matrix_to_center
scale_image_zoom_in = tf.warp(image, scale_matrix)
***# translation***
transaltion_transforms = tf.AffineTransform(translation=(50, 50))
translated_image = tf.warp(image, transaltion_transforms)
plot_image([image, rot_image, scale_image_zoom_out, scale_image_zoom_in, translated_image])
***# shear transforms***
shear_transforms = tf.AffineTransform(shear=np.deg2rad(45))
shear_matrix = matrix_to_topleft + shear_transforms + matrix_to_center
shear_image = tf.warp(image, shear_matrix)
bright_jitter = image*0.999 + np.zeros_like(image)*0.001
plot_image([image, shear_image, bright_jitter])
网络建设
现在让我们构建和训练模型。
选择架构
在本节中,我们将使用’ ResNet '架构。由于 ResNets 在行业中已经变得相当普遍,所以有必要花一些时间来理解其架构的重要元素。先说这里提出的 原架构 。还有,在 2016 年,ResNet 团队已经在原架构 这里 提出了一些改进。利用这些修改,他们已经训练了超过 1000 层的网络(例如 ResNet-1001 )。
这里使用的“ResNet builder”模块基本上是一个 Python 模块,包含了 ResNet 的所有构建模块。我们将使用该模块导入 ResNet 的变体(ResNet-18、ResNet-34 等。).resnet.py 模块取自 此处 。它最大的好处是“跳过连接”机制允许非常深的网络。
运行数据生成器
数据生成器支持预处理—它将图像归一化(除以 255)并裁剪图像的中心部分 (100 x 100) 。
没有特别的理由将 100 作为尺寸,但是已经选择了它,以便我们可以处理所有大于 100*100 尺寸的图像。如果图像的任何尺寸(高度或宽度)小于 100 像素,则该图像将被自动删除。你可以根据需要把它改成 150 或者 200。
现在让我们设置数据发生器。下面的代码设置了一个定制的数据生成器,与 keras API 自带的略有不同。使用定制生成器的原因是为了能够根据手头的问题修改它(可定制性)。
import numpy as np
import keras
class **DataGenerator**(keras.utils.Sequence):
** 'Generates data for Keras'**
def __init__(self, mode='train', ablation=None, flowers_cls=['daisy', 'rose'],
batch_size=32, dim=(100, 100), n_channels=3, shuffle=True):
** *"""*
*Initialise the data generator*
*"""***
self.dim = dim
self.batch_size = batch_size
self.labels = {}
self.list_IDs = []
***# glob through directory of each class***
for i, cls **in** enumerate(flowers_cls):
paths = glob.glob(os.path.join(DATASET_PATH, cls, '*'))
brk_point = int(len(paths)*0.8)
if mode == 'train':
paths = paths[:brk_point]
else:
paths = paths[brk_point:]
if ablation **is** **not** None:
paths = paths[:ablation]
self.list_IDs += paths
self.labels.update({p:i for p **in** paths})
self.n_channels = n_channels
self.n_classes = len(flowers_cls)
self.shuffle = shuffle
self.on_epoch_end()
def __len__(self):
**'Denotes the number of batches per epoch'**
return int(np.floor(len(self.list_IDs) / self.batch_size))
def __getitem__(self, index):
**'Generate one batch of data'**
***# Generate indexes of the batch***
indexes = self.indexes[index*self.batch_size:(index+1)*self.batch_size]
***# Find list of IDs***
list_IDs_temp = [self.list_IDs[k] for k **in** indexes]
***# Generate data***
X, y = self.__data_generation(list_IDs_temp)
return X, y
def on_epoch_end(self):
**'Updates indexes after each epoch'**
self.indexes = np.arange(len(self.list_IDs))
if self.shuffle == True:
np.random.shuffle(self.indexes)
def __data_generation(self, list_IDs_temp):
**'Generates data containing batch_size samples' *# X : (n_samples, *dim, n_channels)***
** *# Initialization***
X = np.empty((self.batch_size, *self.dim, self.n_channels))
y = np.empty((self.batch_size), dtype=int)
delete_rows = []
** *# Generate data***
for i, ID **in** enumerate(list_IDs_temp):
***# Store sample***
img = io.imread(ID)
img = img/255
if img.shape[0] > 100 **and** img.shape[1] > 100:
h, w, _ = img.shape
img = img[int(h/2)-50:int(h/2)+50, int(w/2)-50:int(w/2)+50, : ]
else:
delete_rows.append(i)
continue
X[i,] = img
***# Store class***
y[i] = self.labels[ID]
X = np.delete(X, delete_rows, axis=0)
y = np.delete(y, delete_rows, axis=0)
return X, keras.utils.to_categorical(y, num_classes=self.n_classes)
首先,我们将训练数据存储在 nn 目录中(如果有 nn 类的话)。对于给定的批量大小,我们希望生成批量数据点,并将它们提供给模型。
第一个for
循环“遍历”每个类(目录)。对于每个类,它将每个图像的路径存储在列表paths
中。在训练模式中,它将paths
子集化,以包含前 80%的图像;在验证模式下,它对最后 20%进行子集划分。在消融实验的特殊情况下,它只是对每一类的第一个ablation
图像进行子集划分。
我们将所有图像(所有类别)的路径存储在一个组合列表self.list_IDs
中。字典self.labels
包含标签(作为path: class_number (0/1)
的键:值对)。
在循环之后,我们调用方法on_epoch_end()
,该方法创建一个长度为self.list_IDs
的数组self.indexes
,并对它们进行混洗(在每个时期的末尾混洗所有的数据点)。
_getitem_
方法使用(混洗)数组self.indexes
从路径列表self.list_IDs
中选择batch_size
个条目(路径)。
最后,方法__data_generation
返回一批图像作为对 X,y,其中 X 的形状为(batch_size, height, width, channels)
,y 的形状为(batch size, )
。注意__data_generation
也做一些预处理——它标准化图像(除以 255)并裁剪图像中心 100 x 100 的部分。因此,每个图像具有形状(100, 100, num_channels)
。如果图像的任何尺寸(高度或宽度)小于 100 像素,该图像将被删除。
消融实验
这些指的是获取一小块数据并在其上运行您的模型——这有助于判断模型是否正在运行。这被称为消融实验。
构建网络的第一步是让网络在数据集上运行。让我们试着仅在几幅图像和一个时期上拟合网络。注意,因为指定了ablation=100
,所以使用每类 100 个图像,所以总批次数为np.floor(200/32)
= 6。
注意,DataGenerator
类“继承”自keras.utils.Sequence
类,因此它拥有基础keras.utils.Sequence
类的所有功能(比如model.fit_generator
方法)。
***# using resnet 18***
model = resnet.ResnetBuilder.build_resnet_18((img_channels, img_rows, img_cols), nb_classes)
model.compile(loss='categorical_crossentropy', optimizer='SGD',
metrics=['accuracy'])
***# create data generator objects in train and val mode*
*# specify ablation=number of data points to train on***
training_generator = DataGenerator('train', ablation=100)
validation_generator = DataGenerator('val', ablation=100)
***# fit: this will fit the net on 'ablation' samples, only 1 epoch***
model.fit_generator(generator=training_generator,
validation_data=validation_generator,
epochs=1,)
过拟合训练数据
下一步是尝试在训练数据上过度拟合模型。为什么我们要故意夸大我们的数据呢?简单地说,这将告诉我们网络是否能够学习训练集中的模式。这将告诉您模型的行为是否符合预期。
我们将使用 ablation=100(即在每类的 100 个图像上训练),因此它仍然是一个非常小的数据集,并且我们将使用 20 个时期。在每个时期,将使用 200/32=6 批。
***# resnet 18***
model = resnet.ResnetBuilder.build_resnet_18((img_channels, img_rows, img_cols), nb_classes)
model.compile(loss='categorical_crossentropy',optimizer='SGD',
metrics=['accuracy'])
***# generators***
training_generator = DataGenerator('train', ablation=100)
validation_generator = DataGenerator('val', ablation=100)
***# fit***
model.fit_generator(generator=training_generator,
validation_data=validation_generator,
epochs=20)
训练精度随着每个历元不断增加
结果表明,训练精度随着每个历元不断提高。验证准确性也会增加,然后趋于平稳,这是“良好拟合”的标志,即我们知道该模型至少能够从一个小数据集学习,因此我们可以希望它也能够从整个数据集学习。
总而言之,对任何模型的一个好的测试是检查它是否能够过度适应训练数据(即训练损失随着时期持续减少)。这种技术在深度学习中特别有用,因为大多数深度学习模型都是在大型数据集上训练的,如果它们无法过度适应小版本,那么它们就不太可能从大版本中学习。
超参数调谐
我们在数据集的一小块上训练模型,并确认模型可以从数据集学习(通过过度拟合来指示)。在修复了模型和数据扩充之后,我们现在需要找到优化器的学习率(这里是 SGD)。首先,让我们列出想要优化的超参数:
- 学习速度和变化+优化器
- 增强技术
基本思想是随着超参数的各种值的增加,跟踪验证损失。
Keras 回调
在你继续之前,让我们讨论一下回调。回调基本上是您希望在培训的特定情况下执行的操作。例如,我们希望在每个时期结束时执行存储丢失的操作(这里的实例是一个时期的结束)。
形式上,回调只是一个函数(如果您想要执行单个操作),或者一个函数列表(如果您想要执行多个操作),它们将在特定事件(一个时期的结束、每个批次的开始、准确性达到稳定状态时等)时执行。).Keras 通过类keras.callbacks.Callback
提供了一些非常有用的回调功能。
Keras 有很多内置的回调函数(这里列出的)。在 keras 中创建自定义回调的一般方法是:
from keras import optimizers
from keras.callbacks import *
***# range of learning rates to tune***
hyper_parameters_for_lr = [0.1, 0.01, 0.001]
***# callback to append loss***
class **LossHistory**(keras.callbacks.Callback):
def on_train_begin(self, logs={}):
self.losses = []
def on_epoch_end(self, epoch, logs={}):
self.losses.append(logs.get('loss'))
***# instantiate a LossHistory() object to store histories***
history = LossHistory()
plot_data = {}
***# for each hyperparam: train the model and plot loss history***
for lr **in** hyper_parameters_for_lr:
print ('**\n\n**'+'=='*20 + ' Checking for LR=**{}** '.format(lr) + '=='*20 )
sgd = optimizers.SGD(lr=lr, clipnorm=1.)
***# model and generators***
model = resnet.ResnetBuilder.build_resnet_18((img_channels, img_rows, img_cols), nb_classes)
model.compile(loss='categorical_crossentropy',optimizer= sgd,
metrics=['accuracy'])
training_generator = DataGenerator('train', ablation=100)
validation_generator = DataGenerator('val', ablation=100)
model.fit_generator(generator=training_generator,
validation_data=validation_generator,
epochs=3, callbacks=[history])
***# plot loss history***
plot_data[lr] = history.losses
在上面的代码中,我们创建了一个自定义回调,在每个时期结束时将丢失的数据追加到一个列表中。注意,logs
是keras.callbacks.Callback
的一个属性(一个字典),我们用它来获得键‘loss’的值。这个字典的其他一些关键字是acc
、val_loss
等。
为了告诉模型我们想要使用回调,我们创建了一个名为history
的LossHistory
对象,并使用callbacks=[history]
将其传递给model.fit_generator
。在这种情况下,我们只有一个回调history
,尽管您可以通过这个列表传递多个回调对象(多个回调的示例在下一节中——参见DecayLR()
的代码块)。
这里,我们调整了学习率超参数,并观察到与 0.01 和 0.001 相比,0.1 是最佳的学习率。然而,在整个训练过程中使用如此高的学习率并不是一个好主意,因为损失可能在稍后开始在最小值附近振荡。因此,在训练开始时,我们为模型使用高学习率来快速学习,但是随着我们进一步训练并向最小值前进,我们逐渐降低学习率。
***# plot loss history for each value of hyperparameter***
f, axes = plt.subplots(1, 3, sharey=True)
f.set_figwidth(15)
plt.setp(axes, xticks=np.arange(0, len(plot_data[0.01]), 1)+1)
for i, lr **in** enumerate(plot_data.keys()):
axes[i].plot(np.arange(len(plot_data[lr]))+1, plot_data[lr])
上面的结果表明 0.1 的学习率是最好的,尽管在整个训练中使用如此高的学习率通常不是一个好主意。因此,我们应该使用学习率衰减——从高学习率开始,并随着每个时期衰减。
我们使用另一个自定义回调 ( DecayLR
)来衰减每个时期结束时的学习率。衰减率被指定为 0.5 ^历元。另外,注意这次我们告诉模型使用两个回调(作为列表callbacks=[history, decay]
传递给model.fit_generator
)。
尽管我们在这里使用了自己的自定义衰减实现,但是您也可以使用内置在 keras 优化器中的实现(使用decay
参数)。
***# learning rate decay***
class **DecayLR**(keras.callbacks.Callback):
def __init__(self, base_lr=0.001, decay_epoch=1):
super(DecayLR, self).__init__()
self.base_lr = base_lr
self.decay_epoch = decay_epoch
self.lr_history = []
***# set lr on_train_begin***
def on_train_begin(self, logs={}):
K.set_value(self.model.optimizer.lr, self.base_lr)
***# change learning rate at the end of epoch***
def on_epoch_end(self, epoch, logs={}):
new_lr = self.base_lr * (0.5 ** (epoch // self.decay_epoch))
self.lr_history.append(K.get_value(self.model.optimizer.lr))
K.set_value(self.model.optimizer.lr, new_lr)
***# to store loss history***
history = LossHistory()
plot_data = {}
***# start with lr=0.1***
decay = DecayLR(base_lr=0.1)
***# model***
sgd = optimizers.SGD()
model = resnet.ResnetBuilder.build_resnet_18((img_channels, img_rows, img_cols), nb_classes)
model.compile(loss='categorical_crossentropy',optimizer= sgd,
metrics=['accuracy'])
training_generator = DataGenerator('train', ablation=100)
validation_generator = DataGenerator('val', ablation=100)
model.fit_generator(generator=training_generator,
validation_data=validation_generator,
epochs=3, callbacks=[history, decay])
plot_data[lr] = decay.lr_history
plt.plot(np.arange(len(decay.lr_history)), decay.lr_history)
增强技术
现在让我们编写一些代码来实现数据扩充。扩充通常是通过数据生成器完成的,即扩充的数据是动态地批量生成的。你可以使用内置的 keras ImageDataGenerator
或者编写你自己的数据生成器(如果你想的话,可以定制一些特性等)。下面的代码显示了如何实现这些。
import numpy as np
import keras
***# data generator with augmentation***
class **AugmentedDataGenerator**(keras.utils.Sequence):
**'Generates data for Keras'**
def __init__(self, mode='train', ablation=None, flowers_cls=['daisy', 'rose'],
batch_size=32, dim=(100, 100), n_channels=3, shuffle=True):
**'Initialization'**
self.dim = dim
self.batch_size = batch_size
self.labels = {}
self.list_IDs = []
self.mode = mode
for i, cls **in** enumerate(flowers_cls):
paths = glob.glob(os.path.join(DATASET_PATH, cls, '*'))
brk_point = int(len(paths)*0.8)
if self.mode == 'train':
paths = paths[:brk_point]
else:
paths = paths[brk_point:]
if ablation **is** **not** None:
paths = paths[:ablation]
self.list_IDs += paths
self.labels.update({p:i for p **in** paths})
self.n_channels = n_channels
self.n_classes = len(flowers_cls)
self.shuffle = shuffle
self.on_epoch_end()
def __len__(self):
**'Denotes the number of batches per epoch'**
return int(np.floor(len(self.list_IDs) / self.batch_size))
def __getitem__(self, index):
** 'Generate one batch of data'**
***# Generate indexes of the batch***
indexes = self.indexes[index*self.batch_size:(index+1)*self.batch_size]
***# Find list of IDs***
list_IDs_temp = [self.list_IDs[k] for k **in** indexes]
** *# Generate data***
X, y = self.__data_generation(list_IDs_temp)
return X, y
def on_epoch_end(self):
** 'Updates indexes after each epoch'**
self.indexes = np.arange(len(self.list_IDs))
if self.shuffle == True:
np.random.shuffle(self.indexes)
def __data_generation(self, list_IDs_temp):
**'Generates data containing batch_size samples' *# X : (n_samples, *dim, n_channels)***
***# Initialization***
X = np.empty((self.batch_size, *self.dim, self.n_channels))
y = np.empty((self.batch_size), dtype=int)
delete_rows = []
** *# Generate data***
for i, ID **in** enumerate(list_IDs_temp):
***# Store sample***
img = io.imread(ID)
img = img/255
if img.shape[0] > 100 **and** img.shape[1] > 100:
h, w, _ = img.shape
img = img[int(h/2)-50:int(h/2)+50, int(w/2)-50:int(w/2)+50, : ]
else:
delete_rows.append(i)
continue
X[i,] = img
** *# Store class***
y[i] = self.labels[ID]
X = np.delete(X, delete_rows, axis=0)
y = np.delete(y, delete_rows, axis=0)
***# data augmentation***
if self.mode == 'train':
aug_x = np.stack([datagen.random_transform(img) for img **in** X])
X = np.concatenate([X, aug_x])
y = np.concatenate([y, y])
return X, keras.utils.to_categorical(y, num_classes=self.n_classes)
要优化的指标
根据具体情况,我们选择合适的指标。对于二元分类问题,AUC 通常是最好的度量。
AUC 通常是比准确性更好的指标。因此,让我们监控 AUC,并根据验证数据选择基于 AUC 的最佳模型,而不是针对准确性进行优化。我们将使用回调on_train_begin
和on_epoch_end
来初始化(在每个时期的开始)和存储 AUC(在时期的结束)。
from sklearn.metrics import roc_auc_score
class **roc_callback**(Callback):
def on_train_begin(self, logs={}):
logs['val_auc'] = 0
def on_epoch_end(self, epoch, logs={}):
y_p = []
y_v = []
for i **in** range(len(validation_generator)):
x_val, y_val = validation_generator[i]
y_pred = self.model.predict(x_val)
y_p.append(y_pred)
y_v.append(y_val)
y_p = np.concatenate(y_p)
y_v = np.concatenate(y_v)
roc_auc = roc_auc_score(y_v, y_p)
print ('**\n**Val AUC for epoch**{}**: **{}**'.format(epoch, roc_auc))
logs['val_auc'] = roc_auc
最终运行
现在让我们来训练最终的模型。请注意,我们将继续在models/best_models.hdf5
保存最佳模型的权重,因此您需要创建一个目录models
。请注意,模型权重通常保存在 hdf5 文件中。
保存最佳模型是使用ModelCheckpoint
附带的回调功能完成的。我们基本上指定了保存模型权重的filepath
,monitor='val_auc'
指定了您正在基于验证准确性选择最佳模型,save_best_only=True
仅保存最佳权重,mode='max'
指定了验证准确性将被最大化。
***# model***
model = resnet.ResnetBuilder.build_resnet_18((img_channels, img_rows, img_cols), nb_classes)
model.compile(loss='categorical_crossentropy',optimizer= sgd,
metrics=['accuracy'])
training_generator = AugmentedDataGenerator('train', ablation=32)
validation_generator = AugmentedDataGenerator('val', ablation=32)
***# checkpoint***
filepath = 'models/best_model.hdf5'
checkpoint = ModelCheckpoint(filepath, monitor='val_auc', verbose=1, save_best_only=True, mode='max')
auc_logger = roc_callback()
***# fit***
model.fit_generator(generator=training_generator,
validation_data=validation_generator,
epochs=3, callbacks=[auc_logger, history, decay, checkpoint])
plt.imshow(image)
***#*standardizing *image
#moved the origin to the centre of the image***
h, w, _ = image.shape
img = image[int(h/2)-50:int(h/2)+50, int(w/2)-50:int(w/2)+50, : ]
model.predict(img[np.newaxis,: ])
嘿,我们有一个非常高的概率为 1 类,即玫瑰。如果你还记得的话,0 班是黛西,1 班是罗斯(在博客上面)。因此,模型已经学习得很好了。我们已经建立了一个在 3 个时期结束时具有良好 AUC 的模型。如果您使用更多的纪元来训练,您应该能够达到更好的 AUC 值。
如果你有任何问题、建议或批评,可以通过 LinkedIn 或评论区联系我。
C++中的数据预处理和可视化
关于如何使用 C++实现机器学习的基本功能的工作代码示例
“我们相信上帝。所有其他人都必须带数据。”— W. Edwards Deming,统计学家、教授、作家、讲师和顾问。
卢克·切瑟在 Unsplash 上的照片
D 数据预处理是将原始数据转换成计算机可理解格式的过程,它是任何机器学习操作的第一步。数据收集通常控制松散,可能会导致超出范围的值。数据准备和过滤步骤可能需要相当长的处理时间。
数据预处理包括:
- 从文件中读取数据。
- 数据清理。
- 实例选择。
- 数据标准化。
- 数据转换。
- 特征提取和选择。
要了解更多详情,您可以查看以下视频
数据预处理的产物就是最终的训练集。在本文中,我将介绍使用 C++时的一些数据预处理步骤,以及使用 Matplotlib-Cpp 库的数据可视化。
这篇文章是解决机器学习算法在 C++中实现的系列文章的一部分,在整个系列中,我们将使用 Iris 数据集这里。
- 什么时候应该学习使用 C++的机器学习?
- 每个 C++开发人员必读的 8 本书。
- c++中的数据预处理和可视化。
- 使用 C++进行机器学习数据操作。
- 使用 C++ 从零开始的朴素贝叶斯。
- 线性回归在 C++中的实现。
请注意,已经有一些库可以轻松完成这项工作,但是本系列的目的是学习如何从头开始开发这些算法。如果您有兴趣了解更多关于 c++ ML 库的信息,您可以阅读这篇文章:
我喜欢使用 C++,甚至在我发现了用于机器学习的 Python 编程语言之后。C++…
www.analyticsvidhya.com](https://www.analyticsvidhya.com/blog/2020/05/introduction-machine-learning-libraries-c/)
在本文中,我将使用 i ris 数据集作为我们可以对其执行每个操作的数据示例,同时请注意,我将在本教程中使用 C++11。
从文件中读取数据:
从这里下载 iris.data 文件后。让我们用简单的读文件指令从文件中读取数据,并在单独的向量中解析每种类型的数据。
从文件中读取数据
在这段代码中,我们使用 ifstream 从一个文件创建了一个简单的输入流。
我们还使用多个向量来读取数据集中的每种信息,然后将所有数据添加到一个二维向量中。
在 iris 数据集中,除了 iris 类数据是字符串类型之外,所有数据都来自相同的数据类型,因此我必须将其转换为 enum 类型,并将其作为 float 进行处理,以匹配 Iris_Dataset 向量中的其余数据。
但是,您总是可以使用其他方式加载不同类型的数据,例如,您可以创建一个结构并在其上加载数据,或者为 iris 数据集创建一个类并在该类上加载数据。
现在,我决定用这种简单的方法来处理相同数据类型的数据。
数据可视化:
图像胜于文字,直观地表示数据对于理解数据、收集数据信息和识别异常值非常重要。
虽然这在使用 C++开发机器学习算法时似乎不太重要,因为大多数情况下,您将使用 python 等其他语言的数据来测试和实现算法,然后可以将算法转换为 C++,但我相信这对于在实现过程中可视化数据(例如用于调试目的)非常重要。
在本文中,我将使用 Matplotlib-CPP ,它是 Matplotlib 的 python APIs 的简单包装器。请查看文档以了解更多关于该库的信息。
使用 Matplotlib-CPP 很简单,您只需要包含头文件“matplotlibcpp.h”并将其与 python 库链接。下面是来自 GitHub 库的一个最小的例子:
来自 matplotlib-cpp 的最小示例
现在,我将只使用标准绘图 API 表示四个 iris 属性的数据:
用绘图法绘制虹膜数据集
使用 bar API 表示数据的另一种方式是:
利用条形法绘制虹膜数据集
数据清理:
从数据集中检测并纠正(或删除)损坏或不准确数据的过程,例如,在数据收集阶段可能会引入一些缺失、不一致的值或异常值。
在前面的函数中,您可能注意到我使用了 replace std::replace 函数来替换一些值。这一步可用于在将数据读入向量之前删除或替换任何值。例如,我怀疑“,”或“-”可能会混淆从文件中加载的值,所以我决定使用统一的读取方式,用其他值替换它们。
另一种方法是使用[迭代器](http://Iterators in C++ STL)和λ表达式移除或替换添加到向量中的数据。
这是一个去除萼片长度大于 5.8 的值的例子。
移除大于 5.8 的值
这可以通过添加一行代码来完成
数据标准化:
数据标准化是机器学习模型中提高模型精度的重要步骤。要了解更多关于数据标准化的重要性,您可以阅读以下文章:
要素缩放简介我最近在处理一个数据集,该数据集包含多个要素,跨越不同的…
www.analyticsvidhya.com](https://www.analyticsvidhya.com/blog/2020/04/feature-scaling-machine-learning-normalization-standardization/) [## 为什么数据规范化对于机器学习模型是必要的
标准化是一种经常作为机器学习的数据准备的一部分而应用的技术。正常化的目标…
medium.com](https://medium.com/@urvashilluniya/why-data-normalization-is-necessary-for-machine-learning-models-681b65a05029)
标准化数据集包括重新调整值的分布,使观察值的平均值为 0,标准偏差为 1。这将需要减去平均值并除以标准偏差。
首先,我们实现了一个计算平均值的 API:
使用模板计算平均值
这个 API 将获取任何标准类型的向量并计算平均值。可以使用类似的方法来计算标准偏差。
使用模板函数计算标准偏差
计算萼片长度的平均值和标准偏差:
CPP 中的均值和标准差计算
计算萼片长度平均值和标准偏差
我们可以在应用标准化之前将数据可视化:
原始萼片长度直方图
然后我们可以应用这一行代码来计算标准化后的值:
使用 lambda 表达式减去平均值并除以每个向量值的标准偏差
标准化后计算的平均值和标准偏差
标准化萼片长度直方图
在本文中,我们给出了一些数据预处理步骤的实现示例,我们介绍了从文件中读取数据、使用 Matplotlibcpp 进行数据可视化、数据清理,以及在数据标准化过程中对数据执行一些操作,如均值和标准差。
推荐书籍:
- c++编程语言,作者比雅尼·斯特劳斯特鲁普
- 安德烈·布尔科夫所著的百页机器学习书籍
- 完全初学者的机器学习,作者奥利弗·西奥博尔德
或者你可以跟随这些教程
或者访问这篇文章谈 每个机器学习开发者必读的 9 本书&每个 C++编程开发者必读的 8 本书。
这篇文章是解决机器学习算法在 C++中实现的系列文章的一部分,在整个系列中,我们将使用 Iris 数据集这里。
- 什么时候应该学习使用 C++的机器学习?
- 每个 C++开发人员必读的 8 本书。
- c++中的数据预处理和可视化。
- 使用 C++进行机器学习数据操作。
- 使用 C++ 从零开始的朴素贝叶斯。
- 线性回归在 C++中的实现。
也请查看我的最新文章:
[## ML 模型的版本控制:为什么需要它,它是什么,如何实现它— neptune.ai
版本控制在任何软件开发环境中都很重要,在机器学习中更是如此。在 ML 中…
medium.com](https://medium.com/coderbyte/version-control-for-ml-models-why-you-need-it-what-it-is-how-to-implement-it-neptune-ai-497ff85b2b1a) [## 加入我的介绍链接媒体-艾哈迈德哈希什
作为一个媒体会员,你的会员费的一部分会给你阅读的作家,你可以完全接触到每一个故事…
medium.com](https://medium.com/@ahmhashesh/membership)
感谢你到此为止,查看其他已发表的故事:
长时记忆模型和近期工作综述
medium.com](https://medium.com/ml2b/introduction-to-compressive-transform-53acb767361e) [## 变压器综合指南
关注是你所需要的,甚至更多
pub.towardsai.net](https://pub.towardsai.net/comprehensive-guide-to-transformers-6c73a9e6e2df)
了解数据预处理
弗兰基·查马基在 Unsplash 上拍摄的照片
数据预处理是一项重要的任务。它是一种数据挖掘技术,将原始数据转换成更容易理解、有用和有效的格式。
数据有更好的想法。这种思路在进行数据预处理后会更加清晰易懂。
为什么需要数据预处理?
真实世界的数据通常是:
**不完整:**某些属性或值或两者缺失,或者只有汇总数据。
**噪音:**数据包含错误或异常值
**不一致:**数据包含代码或名称等差异。
数据预处理中的任务
- **数据清洗:**又称擦洗。这项任务包括填充缺失值、平滑或去除噪声数据和异常值,以及解决不一致问题。
- **数据集成:**该任务涉及集成来自多个来源的数据,如数据库(关系型和非关系型)、数据立方体、文件等。数据源可以是同类的,也可以是异类的。从源获得的数据在格式上可以是结构化的、非结构化的或半结构化的。
- **数据转换:**这包括根据数据集的需要对数据进行标准化和汇总。
- **数据缩减:**在此步骤中,数据被缩减。可以减少记录的数量或属性或维度的数量。进行缩减时要记住,缩减后的数据应该产生与原始数据相同的结果。
- **数据离散化:**被认为是数据约简的一部分。数值属性被标称属性取代。
数据清理
数据清理过程检测并消除数据中存在的错误和不一致,并提高数据质量。由于数据输入过程中的拼写错误、缺少值或任何其他无效数据,会出现数据质量问题。基本上,“脏”数据被转换成干净的数据。“脏”数据不会产生准确而好的结果。垃圾数据给出了垃圾。因此处理这些数据变得非常重要。专业人士在这一步花了很多时间。
“脏”或“不干净”数据的原因
- 虚拟值
- 数据缺失
- 违反商业规则
- 数据集成问题
- 矛盾的数据
- 地址线使用不当
- 重用的主键
- 非唯一标识符
如何清理数据?
- 处理缺失值
- 处理噪音和异常值
- 删除不需要的数据
处理缺失值
无法在数据集中查看缺失值。他们必须被处理。此外,许多模型不接受缺失值。有几种处理缺失数据的技术,选择正确的技术至关重要。处理缺失数据的技术选择取决于问题域和数据挖掘过程的目标。处理缺失数据的不同方法有:
- **忽略数据行:**对于缺少最大数量数据的记录,建议使用这种方法,这样记录就没有意义了。当只有较少的属性值丢失时,通常避免使用这种方法。如果忽略(即删除)所有缺失值的行,将会导致性能下降。
- **手动填充缺少的值:**这是一个非常耗时的方法,因此对于几乎所有场景都不可行。
- **使用一个全局常量来填充缺失值:**像“NA”或 0 这样的全局常量可以用来填充所有缺失的数据。这种方法用于难以预测缺失值的情况。
- **使用属性均值或中值:**属性的均值或中值用于填充缺失值。
- **使用向前填充或向后填充方法:**在这种情况下,要么使用前一个值,要么使用下一个值来填充缺少的值。也可以使用先前和后续值的平均值。
- 使用数据挖掘算法预测最可能的值
处理噪音和异常值
数据中的噪声可能由于数据收集中的错误、数据输入期间的错误或由于数据传输错误等而被引入。未知的编码(例如:婚姻状况— Q)、超出范围的值(例如:年龄— 10)、不一致的数据(例如:出生日期—1999 年 10 月 4 日,年龄— 50)、不一致的格式(例如:司法部—2000 年 1 月 13 日,出生日期—2016 年 10 月 10 日)等。是不同类型的噪声和异常值。
可以使用**宁滨处理噪音。**在这种技术中,排序后的数据被放入箱或桶中。可以通过等宽(距离)或等深(频率)划分来创建箱。在这些箱上,可以应用平滑。平滑可以通过条均值、条中值或条边界来实现。
使用宁滨平滑异常值,然后对其进行平滑。它们可以通过视觉分析或箱线图检测出来。聚类可用于识别离群数据组。可以平滑或去除检测到的异常值。
删除不需要的数据
不需要的数据是重复的或不相关的数据。如果效率不高,从不同来源抓取数据然后集成可能会导致一些重复数据。应该删除这些冗余数据,因为它们没有任何用处,只会增加数据量和训练模型的时间。此外,由于冗余记录,该模型可能无法提供准确的结果,因为重复数据会干扰分析过程,从而使重复值更加重要。
数据集成
在这个步骤中,准备一个相干数据源。这是通过从多个来源(如数据库、遗留系统、平面文件、数据立方体等)收集和集成数据来实现的。
数据就像垃圾。在你收集它之前,你最好知道你要用它做什么。——马克·吐温
数据集成中的问题
- **模式集成:**来自不同来源的元数据(即模式)可能不兼容。这就导致了实体识别问题 **。**例:考虑两个数据源 R 和 S,R 中的客户 id 表示为 cust_id,S 中表示为 c_id。它们表示相同的东西,代表相同的东西,但是有不同的名称,这导致了集成问题。检测和解决这些问题对于获得一致的数据源非常重要。
- **数据值冲突:**对于不同数据源中的相同真实世界实体,相同数据的值、度量或表示可能不同。这导致相同数据的不同表示、不同的比例等。示例:数据源 R 中的重量以千克表示,而源 S 中的重量以克表示。要解决这个问题,应该使数据表示一致,并相应地执行转换。
- **冗余数据:**在整合不同来源的数据时,可能会出现重复的属性或元组。这也可能导致不一致。通过仔细整合来自多个来源的数据,可以减少这些冗余或不一致。这将有助于提高挖掘速度和质量。此外,可以执行相关分析来检测冗余数据。
数据整理
如果数据非常大,则执行数据缩减。有时,还会从大量属性中找出最合适的属性子集。这就是所谓的降维。数据简化还包括减少属性值的数量和/或元组的数量。各种数据简化技术包括:
- **数据立方体聚集:**在这种技术中,通过应用像切片、切块或卷起这样的 OLAP 操作来减少数据。它使用解决问题所需的最小级别。
- **降维:**对数据属性或维度进行降维。并非所有属性都是数据挖掘所必需的。通过使用如正向选择、反向消除、决策树归纳或正向选择和反向消除的组合的技术来选择最合适的属性子集。
- **数据压缩:**在这种技术中。大量数据被压缩,即用于存储数据的位数减少。这可以通过使用有损或无损压缩来实现。在*有损压缩中,为了进一步压缩,数据质量会受到影响。在无损压缩中,*更高的压缩级别不会影响数据质量。
- **数量减少:**这种技术通过选择更小的数据表示形式来减少数据量。可以使用直方图、聚类或数据采样来减少数量。减少数量是必要的,因为处理整个数据集既昂贵又耗时。
Python 中机器学习的数据预处理
介绍
数据预处理是机器学习中至关重要的一步,对模型的准确性至关重要。数据包含噪音、缺失值、不完整,有时其格式不可用,无法直接用于机器学习模型。但是如果我们使用有问题和肮脏的数据呢?最后的结果会是什么,这个决定是否可信?预处理数据是关键—预处理的目标是获得更有意义的可信数据。这些技术允许我们将原始数据转换成干净和可用的数据集,并通过重新缩放、标准化、二进制化等使数据更有意义。
数据预处理概念
在下面的例子中,我们将使用一个包含汽车的文件,它可以从https://www.kaggle.com/antfarol/car-sale-advertisements下载。我们将使用 Python 进行数据预处理,并使用几个重要的库:
- pandas——一个提供快速、灵活和富有表现力的数据结构的包,旨在使处理“关系”或“带标签”的数据既简单又直观;
- sci kit-learn——一个提供许多非监督和监督学习算法的库,建立在 NumPy、pandas 和 Matplotlib 之上;
- stats model——允许用户探索数据、估计统计模型和执行统计测试的软件包;
- matplotlib——绘图库;
- seaborn——一个用 Python 制作统计图形的库。它构建在 matplotlib 之上,并与 pandas 数据结构紧密集成。
有许多方法和概念可以使用,但在本例中,我们将只看到其中的一部分:
- 处理缺失值
- 处理异常值
- 多重共线性
- 处理分类值
- 标准化
加载数据
第一步是加载数据。我们可以阅读。csv 文件,使用方法 head,我们可以看到数据集中的前 5 行。
发现数据
现在,让我们来发现数据。我们可以使用 describe 方法-如果使用此方法,我们将仅获得数字特征的描述性统计数据。我们可以通过使用 include='all '来包含所有这些内容。
我们可以看到每个列的唯一值和最常见的类别。例如,我们可以看到列 registration 包含 9015 个值 yes,并且它有两个唯一的值。此外,我们还可以看到平均值、标准差、最大值和最小值等等。
处理缺失值
检查缺失值的一种简单方法是使用 isnull 方法。我们将获得一个包含真(1)和假(0)值的数据框,因此我们将对这些值求和,并可以看到哪一列中有缺失值。
我们可以用两种方法处理缺失值:
1.消除缺失值:
a.删除行:我们可以删除缺失值不可接受的行。这可以通过使用 dropna 方法来完成。这样,我们将排除包含缺失值的行。
b.**删除列:**如果该列有 95%或更多的缺失值,则可以且需要将其删除,因为缺失值的估计值不可信,因为它是从 5%(或更少)的数据中计算出来的,因此是不相关的。
2.估计缺失值:如果只有可接受百分比的值缺失,我们可以估计这些值。这可以通过用相应特征的平均值、中间值或最频繁值填充值来完成。
此外,有多种方法可用于输入缺失值。我们可以使用简单估算、迭代估算或分类估算。分类输入器用于输入分类特征。
在这之后,如果我们调用方法 describe 我们可以看到,在 price 列中,我们拥有与 car 和 body 相同的计数。我们缺少的值将替换为此列中所有值的平均值。
处理异常值
如果我们正在处理回归,这是一个非常重要的话题。我们将绘制列价格的分布图。为了获得最佳结果,我们正在寻找正态分布。但是,价格是指数级的。如果我们使用回归,这将是一个问题。我们可以在上表中看到,价格的最小值是 259,最大值是 547,800,平均值是 16,182。另外,我们可以看到,25%的值在 5500 以下,75%的值在 16800 以下。因此,在这种情况下,我们有离群值。异常值是指与数据中的其他观察值相距异常距离的观察值,它们会显著影响回归。因此,回归将尝试使直线更接近这些值。
通常的规则是去除所有远离平均值 3 倍标准差的东西。这是什么意思?对于正态分布的值,有一个已知的规则:68–95–99.7。该规则可以解释为:
- 68%的数据分布在区间[均值—标准差,均值+标准差],
- 95%的数据分布在区间[均值— 2 *标准差,均值+ 2 *标准差],
- 99.7%的数据分布在区间[均值— 3 *标准差,均值+ 3 *标准差]。
基于此,我们可以说,超出区间[mean-3 * STD,mean + 3*std]的值是异常值,这些值可以删除。
最大值离平均值仍然很远,但已经可以接受地接近了。我们现在可以绘制分布图,我们可以看到数据仍然以相同的方式分布,但离群值较少。我们也可以对里程列这样做。
多重共线性
在多元回归方程中,只要一个自变量与一个或多个其他自变量高度相关,就存在多重共线性。多重共线性是一个问题,因为它破坏了独立变量的统计意义。
不幸的是,使用 scikit-learn 我们不能直接确定是否存在多重共线性。我们可以通过使用 statsmodels 来做到这一点。检查多重共线性的最佳方法之一是通过 VIF(方差膨胀因子)。VIF 提出了一种方法,与变量与其他预测因子完全不相关的情况相比,该方法可以估计估计值的标准误差的平方根变大的程度。VIF 的数值告诉我们(以十进制形式)每个系数的方差(即标准误差平方)膨胀的百分比。例如,1.9 的 VIF 告诉我们,如果没有多重共线性(如果与其他预测值没有相关性),特定系数的方差比我们预期的值大 90%。
在我们的例子中,我们可以说:如果里程数越少,价格就越高。还有,车越老,价格就越小。因此,我们可以检查这三列的多重共线性。
当 VIF 值等于 1 时,根本不存在多重共线性。介于 1 和 5 之间的值被认为是完全可以的。但是,对于什么样的值是可接受的,并没有精确的定义。我们可以在不同的资源中发现,低于 5、6 甚至 10 的值都是可以接受的。因此,在我们的示例中,我们可以看到所有的值都低于 5,它们是正常的。我们很难找到所有特性的值都低于 5 的数据。我们可以说我们的数据集没有多重共线性。如果存在多重共线性,我们应该使用 drop 方法删除该列。作为输入,我们将设置想要删除的列,并且 axis = 1 —这意味着我们将删除一列。例如,假设我们想从数据集中删除列 year。
处理分类值
虚拟编码是将分类输入变量转换为连续变量的常用方法。顾名思义,Dummy 是一个重复变量,它代表一个分类变量的一个级别。水平的存在由 1 表示,不存在由 0 表示。对于出现的每个级别,将创建一个虚拟变量。在 Python 中,我们可以使用 get_dummies 方法,该方法返回一个包含一个热编码列的新数据帧。
标准化
处理完分类值后,我们可以对值进行标准化。它们不在一个尺度上;因此,我们的模型将赋予具有较大值的列更大的权重,这不是理想的情况,因为其他列对于构建模型很重要。为了避免这个问题,我们可以执行标准化。
注意:在一些例子中,标准化可能不会给出更好的结果,甚至可能更糟。
我们将使用 scikit-learn 中的 class StandardScaler。StandardScaler 背后的想法是,它将转换数据,使其分布具有平均值 0 和标准差 1。
结论
本文的目的是展示如何使用一些数据预处理技术,并深入了解这些技术的应用场合。还有其他形式的数据清理也是有用的,但是现在,我们在制定任何模型之前覆盖那些重要的。更好更干净的数据胜过最好的算法。如果我们使用一个非常简单的算法对数据进行清理,我们会得到非常令人印象深刻和准确的结果。
如果你对这个话题感兴趣,请随时联系我。
领英简介:【https://www.linkedin.com/in/ceftimoska/
博客原文可从以下链接获得:https://interworks . com . MK/data-pre-processing-for-machine-learning-in-python/
另外,你可以在下面的链接中找到另一篇类似的博文:https://interworks.com.mk/focusareas/data-management/
使用 Python Pandas 进行数据预处理
第 1 部分—缺失数据
本教程解释了如何使用 pandas 库预处理数据。预处理是对数据进行预分析的过程,目的是将数据转换成标准和规范化的格式。
预处理包括以下几个方面:
- 缺少值
- 数据标准化
- 数据标准化
- 宁滨数据
在本教程中,我们只处理缺失值。
你可以从我的 Github 数据科学库下载本教程的源代码作为 Jupyter 笔记本。
输入数据
在本教程中,我们将使用与肝炎相关的数据集,可以从此链接下载。
首先,使用 pandas 库导入数据,并将它们转换成 dataframe。通过head(10)
方法,我们只打印数据集的前 10 行
**import** pandas **as** pd
df **=** pd.read_csv('hepatitis.csv')
df.head(10)
识别缺失值
我们注意到数据集存在一些问题。例如,“电子邮件”列并不适用于所有行。在某些情况下,它会显示NaN
值,这意味着该值缺失。
为了检查我们的数据集是否包含缺失值,我们可以使用函数isna()
,该函数返回数据集的单元格是否为NaN
。然后我们可以计算每一列有多少个缺失值。
df.isna().sum()
它给出了以下输出:
age 0
sex 0
steroid 1
antivirals 0
fatigue 1
malaise 1
anorexia 1
liver_big 10
liver_firm 11
spleen_palpable 5
spiders 5
ascites 5
varices 5
bilirubin 6
alk_phosphate 29
sgot 4
albumin 16
protime 67
histology 0
class 0
dtype: int64
现在我们可以计算每列缺失值的百分比,只需将之前的结果除以数据集的长度(len(df)
)并乘以 100。
df.isna().sum()**/**len(df)*****100
它给出了以下输出:
age 0.000000
sex 0.000000
steroid 0.645161
antivirals 0.000000
fatigue 0.645161
malaise 0.645161
anorexia 0.645161
liver_big 6.451613
liver_firm 7.096774
spleen_palpable 3.225806
spiders 3.225806
ascites 3.225806
varices 3.225806
bilirubin 3.870968
alk_phosphate 18.709677
sgot 2.580645
albumin 10.322581
protime 43.225806
histology 0.000000
class 0.000000
dtype: float64
处理缺失值时,可以采用不同的替代方法:
- 检查数据源,例如通过联系数据源来更正缺少的值
- 删除缺少的值
- 用一个值替换缺少的值
- 保留缺少的值不变。
删除缺少的值
删除缺少的值可以是以下方法之一:
- 删除缺少值的行
- 删除包含缺失值的整列,我们可以通过指定要考虑的
axis
来使用dropna()
。如果我们设置了axis = 0
,我们将删除整个行;如果我们设置了axis = 1
,我们将删除整个列。如果我们应用函数df.dropna(axis=0)
,数据集的 80 行仍然存在。如果我们应用函数df.dropna(axis=1)
,只有年龄、性别、抗病毒药物、组织学和类别这几列保留下来。但是,移除的值不会应用于原始数据帧,而只会应用于结果。我们可以使用参数inplace=True
来存储原始数据帧df
(df.dropna(axis=1,inplace=True)
)中的更改。
df.dropna(axis**=**1)
或者,我们可以只指定必须对其应用删除操作的列。在下面的示例中,只考虑与列liver_big
相关的缺失行。这可以通过subset
参数来实现,该参数允许指定应用删除操作的列子集。
df.dropna(subset**=**['liver_big'],axis**=**0,inplace**=True**)
现在我们可以检查列indirizzo
是否还有缺失的值。
df.isna().sum()/len(df)*100
另一种方法是删除有一定百分比的非空值的列。这可以通过thresh
参数来实现。在下面的例子中,我们只保留至少有 80%的非空值的列。
df.dropna(thresh**=**0.8*****len(df),axis**=**1,inplace**=True**)
替换丢失的值
处理缺失值的一个好策略是用另一个值替换它们。通常采用以下策略:
- 对于数值,用列的平均值替换缺少的值
- 对于类别值,用该列中最常见的值替换缺失的值
- 使用其他功能
为了替换丢失的值,可以使用三个函数:fillna()
、replace()
和interpolate()
。fillna()
函数用作为参数传递的值替换所有 NaN 值。例如,对于数值,数值列中的所有 NaN 值都可以替换为平均值。为了列出列的类型,我们可以使用属性dtypes
如下:
df.dtypes
它给出了以下输出:
age int64
sex object
steroid object
antivirals bool
fatigue object
malaise object
anorexia object
liver_big object
liver_firm object
spleen_palpable object
spiders object
ascites object
varices object
bilirubin float64
alk_phosphate float64
sgot float64
albumin float64
histology bool
class object
dtype: object
数字列
首先,我们选择数字列。
**import** numpy **as** np
numeric **=** df.select_dtypes(include**=**np.number)
numeric_columns **=** numeric.columns
然后,我们用平均值填充数值列的 NaN 值,平均值由df.mean()
函数给出。
df[numeric_columns] **=** df[numeric_columns].fillna(df.mean())
现在,我们可以检查数字列中的 NaN 值是否已被删除。
df.isna().sum()**/**len(df)*****100
它给出了以下输出:
age 0.000000
sex 0.000000
steroid 0.689655
antivirals 0.000000
fatigue 0.000000
malaise 0.000000
anorexia 0.000000
liver_big 0.000000
liver_firm 0.689655
spleen_palpable 0.689655
spiders 0.689655
ascites 0.689655
varices 0.689655
bilirubin 0.000000
alk_phosphate 0.000000
sgot 0.000000
albumin 0.000000
histology 0.000000
class 0.000000
dtype: float64
类别列
我们注意到在dtypes
中,类别列被描述为对象。因此我们可以选择object
列。我们只想考虑布尔列。然而,object
类型也包括列class
,它是一个字符串。我们选择所有的对象列,然后从它们中移除列class
。然后我们可以将结果的类型转换为bool
。
boolean_columns **=** df.select_dtypes(include**=**np.object).columns.tolist()
boolean_columns.remove('class')
df[boolean_columns] **=** df[boolean_columns].astype('bool')
现在,我们可以用最频繁的值替换所有缺少的值。我们可以使用mode()
函数来计算最频繁的值。我们使用fillna()
函数来替换丢失的值,但是我们也可以使用replace(old_value,new_value)
函数。
df[boolean_columns].fillna(df.mode())
现在我们的数据集不包含任何缺失值。
df.isna().sum()**/**len(df)*****100
它给出了以下输出:
age 0.0
sex 0.0
steroid 0.0
antivirals 0.0
fatigue 0.0
malaise 0.0
anorexia 0.0
liver_big 0.0
liver_firm 0.0
spleen_palpable 0.0
spiders 0.0
ascites 0.0
varices 0.0
bilirubin 0.0
alk_phosphate 0.0
sgot 0.0
albumin 0.0
histology 0.0
class 0.0
dtype: float64
插入文字
替换缺失值的另一种解决方案包括使用其他函数,例如线性插值。例如,在这种情况下,我们可以用上一列和下一列之间的插值来替换一个缺失的值。这可以通过使用interpolate()
功能来实现。
因为我们已经管理了所有丢失的值,所以我们重新加载数据集。
df **=** pd.read_csv('hepatitis.csv')
df.isna().sum()**/**len(df)*****100
我们只选择数字列。
numeric **=** df.select_dtypes(include**=**np.number)
numeric_columns **=** numeric.columns
df.head(10)
现在我们可以将interpolate()
函数应用于数字列,方法是将限制方向也设置为forward
。这意味着从第一行开始应用线性插值,直到最后一行。
df[numeric_columns] **=** df[numeric_columns].interpolate(method **=**'linear', limit_direction **=**'forward')
例如,在第 6 行中,在插值之前为 NaN 的列bilirubin
现在假定值为 0.95,这是 0.90(第 4 行)和 1.00(第 6 行)之间的插值。
df.head(10)
摘要
在本教程中,我们已经看到了数据预处理的一个方面,即处理缺失数据。缺失数据会改变数据分析过程,因此必须对其进行管理。
可以使用三种策略来处理缺失数据:
- 丢弃丢失的数据:当数据集有少量丢失的数据时,可以这样做
- 用其他值替换缺失的数据,例如平均值或最频繁出现的值
- 让丢失的数据保持原样。
如果您想了解数据预处理的其他方面,如数据标准化和数据规范化,请继续关注…
如果你想了解我的研究和其他活动的最新情况,你可以在 Twitter 、 Youtube 和 Github 上关注我。
使用 Scikit-Learn 进行数据预处理:标准化和缩放
如何预处理不同取值范围的数值特征
由 Charles Deluvio 在 Unsplash 上拍摄的照片
Scikit-learn 是一个广泛使用的 Python 机器学习库。由于算法的多样性及其易于理解的语法,它在数据科学从业者中获得了极大的欢迎。除了现成的算法,scikit-learn 还提供了有用的数据预处理功能和方法。
数据预处理是机器学习或深度学习中极其重要的一步。我们不能只是将原始数据转储到一个模型中,然后期望它表现良好。即使我们构建了一个复杂的、结构良好的模型,它的性能也会和我们提供给它的数据一样好。因此,我们需要处理原始数据来提高模型的性能。
在本帖中,我们将介绍处理数值范围差异很大的数字特征(列)的方法。我们将应用标准化和缩放。让我们从这些转变背后的动机开始,然后用例子探讨它们之间的区别。
动机
我们适合机器学习模型的数据集通常有许多特征。不同要素的值很可能处于不同的范围内。例如,考虑一个试图预测房价的模型。房子的面积在 200 平方米左右,而房龄通常不到 20 年。大多数情况下,卧室的数量可以是 1、2 或 3 间。所有这些特征在决定房子的价格时都很重要。然而,如果我们在没有任何缩放的情况下使用它们,机器学习模型可能会对具有较高值的特征给予更多的重视。当要素的比例相对相似时,模型往往表现更好,收敛更快。
标准化和标准定标器
这个问题的一个解决方案是标准化。将列视为变量。如果一个列是标准化的,则从每个值中减去该列的平均值,然后用该列的标准偏差除这些值。结果列的标准偏差为 1,平均值非常接近于零。因此,我们最终得到的变量(列)几乎呈正态分布。标准化可以通过StandardScaler.
实现
预处理过程中使用的函数和转换器在sklearn.preprocessing
包中。让我们将这个包与 numpy 和熊猫一起导入。
import numpy as np
import pandas as pdfrom sklearn import preprocessing
我们可以创建一个表示特征的样本矩阵。然后使用 StandardScaler 对象对其进行变换。
a = np.random.randint(10, size=(10,1))
b = np.random.randint(50, 100, size=(10,1))
c = np.random.randint(500, 700, size=(10,1))X = np.concatenate((a,b,c), axis=1)
X
x 表示具有 3 列 10 行的数据帧中的值。列表示特征。每列的平均值和标准偏差:
这些列在平均值和标准偏差方面有很大不同。
我们现在可以创建一个 StandardScaler 对象并使 X 适合它。
sc = preprocessing.StandardScaler().fit(X)
可以通过对 StandardScaler 对象应用 transform 方法来转换 x。
X_standardized = sc.transform(X)
X_standardized
让我们来计算变换后要素的平均值和标准差。
每个特征的平均值非常接近于 0,并且所有特征都具有单位(1)方差。请注意,标准差是方差的平方根。标准差为 1 表示方差为 1。
我想在这里强调非常重要的一点。考虑到我们正在进行监督学习任务,因此我们将数据集分成训练和测试子集。在这种情况下,我们只对标准的 scaler 对象进行fit
训练集,而不是整个数据集。当然,我们需要转换测试集,但这是通过转换方法完成的。
- StandardScaler.fit(X_train)
- standard scaler . transform(X _ train)
- standard scaler . transform(X _ test)
将整个数据集拟合到标准缩放器对象会导致模型了解测试集。然而,模型不应该学习任何关于测试集的东西。它破坏了列车测试分离的目的。一般来说,这个问题被称为数据泄露。
如何检测和避免数据泄露
towardsdatascience.com](/data-leakage-in-machine-learning-6161c167e8ba)
当我们转换测试集时,由于转换中使用的定标器是基于训练集的,因此这些特征将不具有精确的零均值和单位标准差。测试集中的变化量与训练集中的变化量相同。让我们创建一个样本测试集并转换它。
X_test = np.array([[8, 90, 650], [5, 70, 590], [7, 80, 580]])
X_test
X_test_transformed = sc.transform(X_test)
X_test_transformed
测试集各列的平均值和标准偏差:
最小最大缩放器和鲁棒缩放器
将数值范围提高到相似水平的另一种方法是在特定范围内缩放它们。例如,我们可以将每一列压缩到 0 和 1 之间,使得缩放前的最小值和最大值在缩放后变成 0 和 1。这种缩放可以通过 scikit learn 的**MinMaxScaler**
来实现。默认范围是[0,1],但是我们可以使用 feature_range 参数来更改它。
from sklearn.preprocessing import MinMaxScalermm_scaler = MinMaxScaler()
X_scaled = mm_scaler.fit_transform(X)
X_scaled
mm_scaler2 = MinMaxScaler(feature_range=(0,10))
X_scaled2 = mm_scaler2.fit_transform(X)
X_scaled2
StandardScaler
和MinMaxScaler
对异常值不稳健。假设我们有一个值在 100 到 500 之间的特性,其异常值为 15000。如果我们用MinMaxScaler(feature_range=(0,1))
缩放这个特性,15000 被缩放为 1,所有其他值变得非常接近下限,即 0。因此,我们最终得到了一个不成比例的比例,这对模型的性能产生了负面影响。一种解决方案是移除异常值,然后应用缩放。然而,删除异常值并不总是一个好的做法。在这种情况下,我们可以使用 scikit-learn 的**RobustScaler**
。
**RobustScaler**
顾名思义,对离群值具有鲁棒性。它会移除中位数,并根据分位数范围(默认为 IQR:四分位数范围)缩放数据。IQR 是第一个四分位数(第 25 个四分位数)和第三个四分位数(第 75 个四分位数)之间的范围。RobustScaler
不以预定间隔限制缩放范围。因此,我们不需要像对 MinMaxScaler 那样指定一个范围。
我们可以通过在之前的数据集中添加一行异常值来查看MinMaxScaler
和RobustScaler
之间的差异。
X_new = np.append(X, np.array([[50,420,1400]]), axis=0)X_new
我们先用 range [0,1]来套用MinMaxScaler
。
X_new_mm = mm_scaler.fit_transform(X_new)X_new_mm
超出范围上限的异常值。因此,所有其他值都非常接近下限。
RobustScaler
怎么样?
from sklearn.preprocessing import RobustScalerr_scaler = RobustScaler()
X_new_rs = r_scaler.fit_transform(X_new)X_new_rs
什么时候用哪个?
我们已经讨论了标准定标器、最小最大定标器和鲁棒定标器。许多机器学习模型受益于具有相似规模的特征。然而,没有一个严格的规则来定义哪种转换对于特定的算法是最优的。
MinMaxScaler 和 StandardScaler 对异常值都很敏感。因此,在我们无法移除异常值的情况下,RobustScaler 是比其他两个更好的选择。
在没有异常值的情况下,MinMaxScaler 在大多数情况下表现良好。然而,深度学习算法(例如,神经网络)和回归算法支持具有正态分布的特征。对于这种情况,StandardScaler 是更好的选择。
这些是最常用的转换技术,可以满足我们的一般需求。Scikit-learn 还提供了更多具体的转换,在预处理包的文档中有解释。
感谢您的阅读。如果您有任何反馈,请告诉我。
环境科学中的数据隐私和机器学习
苹果 | 谷歌 | SPOTIFY | 其他
马修·斯图尔特在 TDS 播客
编者按:迈向数据科学播客的“攀登数据科学阶梯”系列由 Jeremie Harris 主持。Jeremie 帮助运营一家名为sharpes minds的数据科学导师初创公司。可以听下面的播客:
2015 年的一个周四下午,我在手机上收到了一条自发通知,告诉我在当前的交通状况下,开车去我最喜欢的餐馆需要多长时间。这令人担忧,不仅因为这意味着我的手机在没有明确询问的情况下就已经知道了我最喜欢的餐馆,还因为这表明我的手机对我的饮食习惯了如指掌,知道我特别喜欢在周四出去吃饭。
随着我们的手机、笔记本电脑和亚马逊 Echos 收集越来越多的关于我们的数据,甚至更多,数据隐私正成为研究、政府和行业应用越来越大的担忧。这就是为什么我想与哈佛大学的博士生和数据科学撰稿人 Matthew Stewart 谈谈,以了解数据隐私背后的一些关键原则。Matthew 是一位多产的博客作者,他在哈佛的研究工作专注于机器学习在环境科学中的应用,这也是我们在这一集讨论的话题。
以下是我最大的收获:
- 进入机器学习的博士课程变得越来越难,但如果你对研究生院级别的机器学习或数据科学死心塌地,你可以选择一个你有专业知识的主题(如环境科学、地质学或物理学),并找到一种用数据科学工具解决它的方法。
- 随着机器学习工具变得越来越用户友好,Matthew 的经历真正表明了主题专业知识变得多么重要。他更像是使用数据科学工具的环境科学家,而不是解决环境科学问题的数据科学家。这也是我们在行业中越来越多地看到的趋势,也是建立个人项目以解决你深刻理解的主题的一个很好的理由。
- 像 k-anonymity(我们在播客中定义和讨论的)这样的简单数据隐私方法有严重的缺点,包括它们不允许我们客观地量化它们提供的隐私程度。出于这个原因,替代策略,特别是包括不同的隐私变得越来越重要。
- 随着公司和政府开始更加关注数据隐私问题,我们消费的公开数据被私有化将变得更加普遍。因此,理解不同私有化技术在实践中的工作原理是负责任地使用这些数据的关键。
你可以在推特上关注马修,这里是 T1,你可以在推特上关注我,这里是 T2,这里是 T3。
疫情中的数据隐私
数据隐私
分析新冠肺炎接触追踪的不同方法
世界各地的国家开始逐渐重新开放,政府正在考虑所有的选择,希望控制未来的疫情。
他们的圣杯:联系追踪应用
专家已经对这些应用的安全性和有效性提出了担忧。但在此之前,我们先来讨论一下模拟。
对于我们很多人来说,这可能是第一次听说联系寻人,但这并不是什么新鲜事。公共卫生官员使用接触追踪已经有几个世纪了。最值得注意的是,遏制性病/性传播感染。
首先,他们隔离感染者以防止进一步传播,然后他们追踪与他们接触过的每一个人。他们做所有这些而不透露个人的身份。最后一部分对这一行动至关重要,追踪接触者不需要透露身份,隐私是至关重要的。
那么,政府和私人公司做了什么来把这个过程带到数字世界呢?好吧,让我们讨论一下正在使用的两种方法。
照片由i̇smail·埃内斯·艾汉在 Unsplash 上拍摄
集中方法
在集中式方法中,用户的手机将他们的 GPS 位置以及他们在那里的时间发送到中央服务器。英国国民医疗服务系统和法国都决定走这条路。
集中式方法的最大缺陷是:安全性和隐私,以及电池。
政府知道他们的公民在任何给定时刻的确切位置曾经是老大哥的噩梦,让乔治·奥威尔夜不能寐。一个疫情之后,全球数百万人自愿提供这些信息。作为一个美国人,不信任政府是文化的一部分,但在一些年轻人的反乌托邦小说中,提供近乎实时的位置是毫无疑问的。
英国和法国辩称该应用程序是安全的,数据不会被用于除了追踪联系人之外的任何目的。在一个理想的世界里,通过 GDPR 向欧盟公民提供的保护(甚至不确定这如何与英国退出欧盟合作)将给予他们必要的法律保障。但是,如果政府撒谎并非法使用数据,谁来让他们负责?同一个政府?
除了老大哥,专家们还提出了对恶意行为者获取数据的担忧。英国的联系追踪应用程序目前正在怀特岛进行测试。与此同时,他们还共享了 cybsecurity 专家的信息,以获得反馈。正如任何人可能已经猜到的那样,网络安全专家发现了一些安全漏洞。雪上加霜的是,他们的一个建议是转向分散的方法,这是他们上个月已经拒绝的。
要让任何联系追踪应用程序工作,无论是集中还是分散,手机都必须持续广播信息。加上手机一整天都要这样做,这会对电池造成损害。然而,集中式方法有一个额外的缺点,特别是对于 iOS 设备。苹果不允许应用程序在后台广播蓝牙信息。苹果已经向各公司明确表示,它不会放松这些限制。为了解决这个问题,iOS 上的一些应用需要保持开放才能工作。
新加坡试图在使用集中方法和解决公众关切之间达成平衡。新加坡开源了他们的应用,并声明除非他们转正,否则数据不会共享给中央服务器。
如果这是真的,新加坡可以成为其他仍然坚持使用中央集权方式的国家的榜样。剩下的最后一个问题是选择加入/选择退出的困境。
作为世界上最大的民主国家,印度已经要求公民下载该应用程序,如果他们想继续工作或避免可能的惩罚。即使是新加坡,尽管选择加入,也表示它可能不会永远是可选的。美国阿拉巴马大学系统“鼓励”教职员工和学生使用他们的应用程序,但不确定他们是否会要求在校园内使用。由于世界各地的数据保护法仍然相当有限,个人可能会发现自己被迫下载这些应用程序。如果不是他们的政府,他们的雇主或杂货店或任何其他他们希望合作的企业。
由于这些举动,印度的一名程序员绕过了应用。现在,他们可以带着一个不断显示“安全”的徽章自由行走,而无需广播任何数据。这是最好的情况。专家们都担心个人发出假阳性。这样做,到头来弊大于利。
分散处理方法
苹果和谷歌的合作引领了这种去中心化的方法。两家公司最近都发布了各自手机的更新,增加了这一选择加入 API。
API 的工作方式非常简单,用户的手机会创建经常变化的识别码。如果手机在相当长的时间内检测到附近有另一部手机,这两部手机将交换当前的识别码。如果用户被诊断患有新冠肺炎病毒,该应用程序将通过服务器广播他们之前 2 周的代码。每个人的手机都会定期检查这个服务器,如果发现与被感染代码匹配的,就会收到通知。
通过这种方法,电池仍然是一个问题,因为蓝牙需要一直打开,但在 iOS 设备上,它不会像集中式方法那样必须打开。
到目前为止, Switzerland 是第一批在他们的应用中测试这种 API 的公司之一,澳大利亚政府已经表示他们也将在他们现有的应用中实现这种 API,德国在之前致力于集中式方法之后也采用了这种 API。
所以,分散的方法是完美的,对不对?
嗯,不幸的是,没有。抛开隐私和安全问题,功效仍然是一个问题。
如果新冠肺炎的诊断是由用户输入的,那么肯定会有一些恶意软件发出假阳性,对公众对该应用的信任产生负面影响。
你没带手机的时候呢?或者你的手机没电了?或者穷人和老年人可能根本没有可以安装该应用程序的手机?
我赞扬苹果和谷歌致力于将安全和隐私置于设计中心的 API。我也要赞扬开发联系人追踪应用程序的软件工程师。然而,这是新技术;我们还不确定它到底有多有效。即使它完美地工作,你被通知你可能已经暴露于该病毒,可能是在你已经暴露了其他人之后。
接触追踪应用程序不能替代有效的治疗或疫苗,也不能替代广泛的检测和限制传播的个人防护设备。像其他人一样,我希望这些天的任何事情都会减缓疫情,但重要的是,我们不要从这些应用程序中获得虚假的安全感,最终使病毒传播得更远。
感谢您阅读我的帖子。我很想听听你们的想法,无论是在这里的评论中还是在 Twitter (@SoyCarlosEO) 上。
大数据时代的数据隐私
了解你的隐私是多么的少,以及差别隐私是如何帮助你的。
“争辩说你不在乎隐私权是因为你没什么好隐瞒的,这和说你不在乎言论自由是因为你无话可说没什么区别。”― 爱德华·斯诺登
2017 年 11 月,跑步应用 Strava 发布了一个数据可视化地图,显示了曾经上传到他们系统的每个活动。这相当于超过 3 万亿个 GPS 点,来自从智能手机到 Fitbits 和智能手表等健身追踪器的各种设备。该应用的一个方面是,你可以看到主要城市的热门路线,或者找到偏远地区锻炼模式不同寻常的个人。
2017 年 11 月发布的 Strava 全球热图。来源
与他人分享你的锻炼活动的想法可能看起来相当无害,但这张地图揭示了军事基地和现役人员的位置。其中一个地点是阿富汗赫尔曼德省的一个秘密军事基地。
阿富汗赫尔曼德省的一个军事基地,慢跑者走过的路线由 Strava 标出。照片:Strava 热图
这不是唯一暴露的基地,事实上,在叙利亚和吉布提等地的活动,用户几乎都是美国军事人员。因此,Strava 数据可视化在某种程度上是驻扎在世界各地的美国军事人员的高度详细的地图。
在这个数据不断增长的时代,保持任何形式的隐私都变得越来越困难。为了给我们的生活带来便利,我们现在不断向公司提供信息,帮助他们改善业务运营。
虽然这有许多积极的好处,例如 IPhone 知道我的位置,能够告诉我附近的餐馆或到达某个位置的最快路线,但它也可能被用于不利的方面,可能导致对个人隐私的严重侵犯。随着我们走向一个越来越数据驱动的社会,这个问题只会越来越严重。
图片由福布斯提供。
在这篇文章中,我将谈论公开发布的数据集的隐私泄露方面的一些最大失误,可以对这些数据集进行的不同类型的攻击以重新识别个人,以及介绍当前我们在数据驱动的社会中维护隐私的最佳防御措施:差分隐私。
什么是数据隐私?
1977 年, Tore Dalenius 阐述了统计数据库的迫切需要:
没有任何关于个人的东西是可以从数据库中学习到的,如果不访问数据库就无法学习到。——托雷纽斯
这一想法希望将众所周知的语义安全概念扩展到数据库,语义安全概念是指当一条消息使用加密算法被编码成密文时,密文不包含潜在消息的任何信息,通常被称为明文。
然而,由于种种原因,哈佛大学计算机科学教授辛西娅·德沃克在一篇论文中已经证明,达勒纽斯提出的这个想法在数学上是不可能的。
这背后的一个原因是,虽然在密码学中,我们经常谈论两个用户在安全通道上通信,同时被保护免受不良行为者的影响,但在数据库的情况下,必须将用户本身视为不良行为者。
所以,我们没有太多的选择。我们可以做的一件事是确定我们的数据集在识别一个人时特别重要的方面,并以这样一种方式处理这些方面,即数据集有效地变得“匿名化”。
数据集的匿名化/去标识化本质上意味着不可能(至少表面上)识别数据集中的任何给定个人。我们使用术语重新识别来指代从匿名数据集中识别的个人。
可识别性的金字塔
个人数据存在于可识别的范围内。想象一个金字塔,在金字塔的顶端,我们有可以直接识别个人的数据:姓名、电话号码或社会保险号。
这些形式的数据统称为’ 直接标识符。
金字塔上的直接标识符下面是可以间接但明确地与个人联系在一起的数据。只需要少量数据就可以唯一地识别一个人,例如性别、出生日期和邮政编码,这些数据加起来可以唯一地识别 87%的美国人口。
这些数据统称为’ 间接标识符 或’ 准标识符。’
在准标识符下面是可以与多人模糊关联的数据——身体测量、餐馆偏好或个人最喜欢的电影。
我们的金字塔的第四个阶段是不能与任何特定的人联系起来的数据——汇总的人口普查数据,或广泛的调查结果。
最后,在金字塔的底部,有些数据与个人完全没有直接关系:天气预报和地理数据。
数据的可识别性水平。来源
这样,更难与个人联系起来的信息在可识别性的金字塔上被放在较低的位置。然而,随着数据越来越多地去除个人信息,其对研究和分析的有用性直接下降。因此,隐私和效用在这个范围的两端——楼梯顶端的数据最有用,楼梯底端的数据最隐私。随着数据变得越来越难处理,它对分析的有用性降低了。
效用和隐私之间的权衡。来源
作为一名数据科学家,这似乎是一个不能令人满意的答案。我们总是希望我们的数据尽可能准确,以便我们可以做出最准确的推断。然而,从另一个角度来看,我们对个人了解得越少,对他们的隐私就越有利。
那么我们如何平衡这种权衡呢?我们注定会失去准确性或隐私吗?我们在玩零和游戏吗?我们会发现答案实际上是肯定的。然而,现在有一些方法可以从数学上确定数据集的隐私级别,这样我们就可以获得最大量的信息,同时为数据中的个人提供合适的隐私级别。稍后将详细介绍。
用于匿名化数据的清理技术
有四种常用技术可用于取消数据集的标识:
[1]删除或修订
这种技术最常用于您不想发布的直接标识符,如电话号码、社会保险号或家庭住址。这通常可以自动完成,因为它直接对应于数据库中的主键。简单地说,删除意味着如果我们有一个 Excel 电子表格,我们将删除对应于直接标识符的列。
在下表中,可以删除第一列“姓名”,而不会影响数据对未来研究的有用性。
这种技术不是万无一失的。直接标识符往往没有明确标注,重要信息可能会被误认为个人信息而被意外删除。
【2】假名化
第二种方法只需要将“Name”类别更改为唯一的匿名值,例如列的哈希值或用户 ID。这些可以随机产生或通过算法确定。然而,这样做应该谨慎。如果你有一个学生名单,你用匿名 ID 发布他们的成绩,最好不要按字母顺序排列,因为这样很容易重新识别他们!
类似地,如果使用确定性算法来执行假名化,并且所使用的算法的性质被暴露,那么它会危及个人的匿名性。
例如,2014 年,纽约市出租车和豪华轿车委员会发布了一份当年纽约市所有出租车出行的数据集。在发布数据之前,出租车和豪华轿车委员会试图清除识别信息,特别是他们使用了出租车牌照号码和驾照号码的假名。然而,博客作者能够发现用于改变奖章号码的算法,然后逆转假名。
这种方法也有与第一种方法相同的缺点-直接标识符可能难以识别和替换,并且间接标识符会无意中留在数据集中。
如果在一个数据集内、多个数据集内或长时间内持续使用相同的唯一假名,假名也将失效。
【3】统计噪声
前两种方法几乎只适用于直接标识符,后两种方法几乎只适用于间接标识符。
我们可以设想第三种添加统计噪声的方法,将图像中某人的脸像素化。我们基本上允许数据仍然存在,但是它被随机噪声所掩盖。根据完成的方式,这可能是一种非常有效的技术。
将统计噪声引入数据集的一些方式包括:
- **概括:**具体数值可报为一个范围。例如,患者的年龄可以报告为 70-80 岁,而不是给出完整的出生日期。
- **扰动:**可以为数据集中的所有患者随机调整特定值。例如,系统地增加或减少患者接受护理的相同天数,或者从正态分布中添加噪声。
- **交换:**数据可以在数据集中的单个记录之间交换。
正如您可能已经怀疑的那样,越多的直接或间接标识符被统计噪声删除和/或模糊,我们数据的准确性就越低。
【4】聚合
第四种方法类似于“统计噪声”部分讨论的一般化思想。不是发布原始数据,而是聚合数据集,只发布汇总统计数据或子集。
例如,数据集可能只提供接受治疗的患者总数,而不是每个患者的个人记录。然而,如果只释放一个小的子样本,重新识别的概率就会增加——比如一个子集只包含一个个体。
在聚合数据集中,个人的直接或间接标识符不会公布。但是,汇总数据必须基于足够广泛的数据范围,以便不会导致对特定个人的识别。例如,在上面的例子中,只有一名女性患者到医院就诊。如果数据中包括 30 名在医院呆过的女性,那么她将更容易被重新识别。
隐私泄露
我可以提出这么多隐私泄露的问题,这实际上非常令人担忧,所以我只挑选了其中的几个故事来说明某些重要的观点。
需要注意的是,我在这里谈论的不是数据泄露:一些坏人侵入公司或政府数据库,窃取客户的机密信息,尽管这也非常普遍,并且随着物联网(IoT)的出现,越来越令人担忧。
我们明确谈论的是公开可用的数据——即你可以(至少在当时)去下载它——或随后用于识别个人身份的商业数据。这也延伸到用于揭示个人信息的商业智能。
Netflix 奖
价值 100 万美元的 Netflix 大奖是由网飞发起的一项竞赛,旨在改进该公司的电影推荐系统。在比赛中,该公司发布了一个大型匿名数据库,参赛者可以将其用作推荐引擎的输入数据。
来自奥斯丁大学的研究生 Arvind Narayanan 和 Vitaly Shmatikov 教授能够重新识别网飞发表的数据集中的两个人。
“公布数据和删除姓名对隐私没有任何好处……如果你知道他们的名字和一些记录,那么你就可以在另一个(私人)数据库中找到那个人。” — 维塔利·什马提科夫
网飞没有在他们的数据集中包括名字,而是为每个用户使用一个匿名标识符。人们发现,当电影分级的收集与分级的公共数据库相结合时,就足以识别这些人。
纳拉亚南和什马蒂科夫通过使用互联网电影数据库(IMDb)中“几十个”人发布的公开评论来确定网飞数据中两个用户的电影评级,证明了这种危险。他们在网飞发布数据后不久发表了一篇论文。
曝光评论者认为是私人的电影分级可能会暴露该人的重要细节。例如,研究人员发现,其中一个人对一些自由主义和同性恋主题的电影有强烈的——表面上是私人的——观点,并且对一些宗教电影也有评级。
更一般地说,这项研究表明,一个人认为是良性的信息可以用来在其他私人数据库中识别他们。在隐私和情报界,这个结果已经被理解了几十年,但是这个案例让大众媒体看到了这个主题。
美国在线
2006 年 8 月,AOL Research 发布了一个文件,其中包含了超过 658,000 个用户在 3 个月内提出的 2000 万个搜索关键词。该数据集旨在用于研究目的,但在获得重大恶名后 3 天后被删除。然而,此时为时已晚,因为数据已经被镜像并分布在互联网上。这一泄密事件最终导致首席技术官 Abdur Chowdhury 博士辞职。
哪里出了问题?这些数据被认为是匿名的,但被发现泄露了搜索者私人生活的敏感细节,包括社会安全号码、信用卡号码、地址,在一个案例中,显然是搜索者的意图杀害他们的妻子。
从 AOL 数据集中提取的一些杂项查询。来源
虽然 AOL 没有明确指出用户的身份,但个人身份信息出现在许多查询中。你试过谷歌自己吗?这就是导致隐私泄露的根本原因。有些人甚至天真到在搜索数据库中输入他们的社会安全号码和地址。
由于 AOL 将查询归因于特定的用户数字标识的账户,因此可以通过这样的信息来识别个人并将其与他们的账户和搜索历史进行匹配。《纽约时报》 通过交叉引用电话簿清单,能够从公布的匿名搜索记录中找到一个人。
目标少女怀孕泄密
《纽约时报》作家查尔斯·杜希格在 2002 年发表的一篇文章中披露,塔吉特百货的一名统计学家安德鲁·波尔被要求开发一个产品预测模型,根据顾客在店内购买的商品来判断她是否怀孕。
WCPO 9 台关于塔吉特怀孕预测模型的新闻报道。来源
该系统将分析购买习惯,并利用这一点来辨别客户怀孕的可能性,然后将邮寄与怀孕相关的商品的优惠券和广告。这对许多人来说似乎是无害的,甚至是积极的事情。然而,一个开始收到优惠券的购物者是一个十几岁的女孩,她愤怒的父亲打电话到商店投诉广告。目标知道,但父亲不知道,十几岁的女孩怀孕了。
支持目标“怀孕”分类的数据与青少年的身份无关。相反,Target 的结论是基于这名青少年从一组 25 种与怀孕购物者相关的产品中购买的,如无味乳液和维生素补充剂。
虽然这些数据可能不会暴露购物者的身份,但塔吉特的大数据预测系统从她的购物模式中得出一个“令人毛骨悚然”且可能不受欢迎的推论。
这本质上是机器学习中分类任务的一个例子,你会惊讶地发现,使用看似无关的信息来预测年龄、性别、种族、政治派别等个人特征是多么容易。事实上,这基本上是剑桥分析公司在 2016 年美国总统选举期间使用脸书提供的数据所做的。
拉坦亚·斯威尼
1996 年,麻省理工学院的一名博士生拉坦娅·斯威尼(Latanya Sweeney)将 GIC 集团保险公司(Group Insurance Company)公开发布和匿名发布的医疗数据与公共选民记录(她花 20 美元购买)相结合,并能够从数据集中重新确定马萨诸塞州州长威廉·韦尔德的身份。
拉坦娅·斯威尼用来重新指认威廉·韦尔德州长的信息。
然后,她使用新获得的信息,向他的家庭地址发送了一封信,解释了她所做的事情,并指出数据匿名的方式显然是不够的。
她的再识别实验结果对以隐私为中心的政策制定产生了重大影响,包括健康隐私立法 HIPAA。
详细描述这些事件的完整文章可以在这里找到。
这段时间以来,她发表了关于数据隐私话题的最重要的领域之一,名为《简单的人口统计学常常唯一地识别人(数据隐私工作论文 3)匹兹堡 2000》。
这篇论文的结论是,美国 87%的个人可以通过知道三条信息而被唯一地识别:性别、出生日期和邮政编码。
Latanya Sweeney 论文中的图表显示了通过不同的准标识符可识别的人口百分比。
拉坦亚现在是一名教授,在哈佛大学管理着数据隐私实验室。本文稍后将概述一些匿名化数据集的技术,例如’k-匿名化 '。
金州黑仔
这个故事可能是最有趣的,也是影响最深远的。臭名昭著的金州黑仔是一名杀人犯和连环强奸犯,在 1978 年至 1986 年期间活跃在加利福尼亚州的萨克拉门托县,最终在 2018 年 4 月被捕,享年 72 岁,当时他的一名亲属将 23AndMe 个人基因组测试的遗传信息上传到公共在线数据库。
调查人员将罪犯的 DNA 上传到数据库,希望能找到部分匹配。令他们惊讶的是,他们做到了。调查人员开始研究家谱,以匹配从一个犯罪现场收集的 DNA。从那里,他们调查了那些树上的个体,以缩小嫌疑人的范围。
最终,他们在合适的年龄组中找到了一个人,他生活在金州黑仔活跃的地区。他们从詹姆斯·迪安杰罗扔掉的物品上收集了他的 DNA,然后在实验室进行分析,发现与凶手直接匹配。
虽然抓住一个连环杀手显然是一件积极的事情,但这种对遗传信息的使用引发了关于如何使用遗传信息的重大伦理问题:
其中最明显的是你的基因信息直接识别你——你不能使用数学技术来“匿名化”它,如果你这样做了,它将不再以任何方式代表你。
**【2】**第二个也是更令人难以忘怀的想法是,你的一个远房亲戚有能力侵犯你的隐私。如果你的表亲被发现有患卵巢癌的遗传倾向,并且这些信息在一个在线数据库中,保险公司可以直接将这些信息与你联系起来,以提高你的保费。这破坏了大多数数据收集过程中存在的知情同意的整个概念。
2008 年,美国出台了基因信息保密法案(GINA) ,禁止某些类型的基因歧视。该法案意味着健康保险提供者不能基于基因进行歧视。
例如,如果发现一个人有突变的 BRCA2 基因——这种基因通常与患乳腺癌的风险增加有关——他们将被禁止使用这些信息以任何方式歧视这个人。
然而,该法案对人寿保险政策、残疾保险政策或长期医疗保险政策中的歧视只字未提。如果你上传了一个个人基因组测试到一个在线的公共数据库,你最好相信你的人寿保险公司知道这件事。
重新识别攻击
在上一节中,我们看到即使匿名化的数据集也可能受到重新识别攻击。这可能会对数据集中的个人以及与分析或生成数据集相关的人员造成伤害。
在讨论的泄露事件中,有几起发生在出于研究目的或作为公司商业活动的一部分而发布的公共数据集中。
这对公司和学术界都提出了重大问题。仅有的两个真正的选择是:
(1) 不使用或严格限制公共数据,这在全球社会中是行不通的,会严重阻碍科学进步,或
(2) 制定可行的隐私保护方法,允许使用公共数据,而不会给作为公共数据一部分的个人参与者带来重大隐私风险。
然而,如果我们要开发隐私方法,我们需要知道个人能够对数据库进行什么类型的攻击。
联动攻击
链接攻击试图通过将数据与背景信息相结合来重新识别匿名数据集中的个人。“链接”使用两个集合中存在的准标识符,如邮政编码、性别、工资等,来建立识别连接。
许多组织没有意识到涉及准标识符的链接风险,尽管他们可能会屏蔽直接标识符,但他们通常不会想到屏蔽或推广准标识符。这正是拉坦亚·斯威尼找到威廉·韦尔德州长地址的方法,也是网飞在 Netflix 有奖竞赛中陷入麻烦的原因!
差异攻击
在这种攻击中,攻击者可以通过组合关于数据集的多个聚合统计信息来隔离单个值。这实质上攻击了数据清理方法一节中讨论的聚合方法。
一个简单的例子是查询一个关于患有癌症的用户的数据库。我们向数据库询问患有癌症的用户,然后向数据库询问患有癌症但名字不是 John 的用户。我们可以潜在地使用这些组合的查询结果来对 John 执行差异攻击。
这里是另一个例子,考虑一个虚构的忠诚卡数据产品,其中该数据产品包含所有客户在给定的一天花费的总金额和使用忠诚卡的客户子组花费的总金额。如果正好有一个顾客在购物时没有使用忠诚卡,那么对当天的两个统计数据进行一些简单的算术运算,就可以显示出这个顾客的精确消费总额,并且只发布总值。
这种攻击的一个更一般的版本被称为**复合攻击。**这种攻击涉及组合许多查询的结果来执行差异攻击。
例如,假设一个数据库使用统计噪声来干扰其结果。如果我们对数据库进行 10,000 次相同的查询,根据它产生统计噪声的方式,我们可能能够平均统计噪声数据,并获得接近真实值的结果。解决这个问题的一种方法可能是限制允许的查询数量,或者向相同的查询添加相同的噪声,但是这样做会给系统增加额外的问题。
同质攻击
对去标识数据库的同质性攻击示例。
同质性攻击利用了敏感数据属性的所有值都相同的情况。上表提供了一个例子,其中数据库的性别和邮政编码已被收回和概括,但我们仍然能够确定邮政编码为 537**的每个人的工资的一个相当好的想法。尽管事实上我们不能明确地识别个人,但我们仍然能够找出其中一个条目与他们有关。
背景知识攻击
背景知识攻击尤其难以防御。它们本质上依赖于个人的背景知识,这可能有助于在数据集中去识别某人。
例如,假设一个人知道他们的邻居去了一家特定的医院,还知道他们的某些属性,如邮政编码、年龄和性别,他们想知道他们可能患有什么疾病。他们发现数据库中有两个条目与这些信息相对应,其中一个条目患有癌症,另一个条目患有心脏病。他们已经在数据库中缩小了搜索结果的范围,但是他们能完全识别出这个人吗?
现在想象一下,邻居是日本人,这个人知道日本人比一般人患心脏病的可能性小得多。他们可以据此合理确定地断定该人患有癌症。
背景知识攻击通常使用贝叶斯统计来建模,贝叶斯统计涉及基于数据集中的属性的先验和后验信念,因为这本质上是个人在执行这种攻击时正在做的事情。一种称为贝叶斯最优隐私的条件有助于抵御这种攻击。
k-匿名性、L-多样性和 T-紧密性
在这一节中,我将介绍三种技术,它们可以用来降低某些攻击发生的概率。这些方法中最简单的是k-匿名,其次是l-多样性,再其次是t-紧密度。有人提出了其他方法来形成一种字母汤,但这是三种最常用的方法。对于每一种情况,必须对数据集执行的分析变得越来越复杂,并且不可否认地对数据集的统计有效性有影响。
k-匿名
正如拉坦娅·斯威尼在其开创性的论文中所说:
如果包含在发布中的每个人的信息不能与至少 k-1 个其信息也出现在发布中的个人相区分,则发布提供 k-匿名保护。
这实质上意味着,只要我的数据集包含至少 k 个给定准标识符集合的条目,那么它就是 k-匿名的。如果我们像前面一样使用邮政编码、性别和年龄的超级键,如果数据集中邮政编码、性别和年龄的每个可能组合中至少有 3 个条目,那么数据集将是 3-匿名的。
原始数据集(左)和 4-匿名数据集(右)。来源
这有助于防止链接攻击,因为攻击者不能高度确定地链接到另一个数据库。然而,仍然有可能有人执行同质攻击,如在所有 k 个个体具有相同值的示例中。例如,如果三个年龄组、性别和邮政编码相同的人碰巧都患有同一种癌症,k-匿名不能保护他们的隐私。同样,它也不能防御背景知识攻击。
k-匿名数据集上的同质性和背景知识攻击的例子。来源
k-匿名的可接受水平是多少?没有明确的定义,但一些论文认为 k=5 或 k=10 的水平是优选的。学术领域的大多数个人似乎都同意 k=2 是不够的,k=3 是保护隐私所需的最低限度(尽管不一定能保证隐私)。
您可能已经意识到,选择的 k 值越高,数据的效用就越低,因为我们必须执行泛化(减少列中唯一值的数量)、模糊化(模糊某些数据特征或组合它们)和抑制(在应用其他去标识方法后删除不能满足 k=3 的行元组)。可以添加合成行,从每列的边缘分布或所有列的联合分布中获取样本,而不是隐藏。
显然,所有这些机制都会严重扭曲或偏向数据集的统计数据,这些技术的权衡仍然是学术研究的主题。Olivia Angiuli 和 Jim Waldo 在出版物“ 中讨论了 HarvardX 数据在大规模数据集 的去识别中概括和抑制之间的统计权衡。
当准标识符的数量变大时,使数据集 k 匿名可能导致大部分数据丢失(80%的数据可以仅通过抑制来移除)或被添加(例如为数据集中当前的每一行添加 3 行以使其 4 匿名),这在大多数大型公共数据集中都会发生。
在我自己尝试制作 k-anonymous 数据集之前,我可以告诉你,实现匿名并仍然拥有并非无用的数据绝非易事。
l-多样性
一些人已经注意到可以在 k-匿名数据集上执行的攻击的可能性,因此隐私研究人员更进一步,提出了 l-多样性。作者将 l-多样性定义为:
…要求敏感属性的值在每个组中得到很好的代表。
他们在数学细节上对此进行了扩展,本质上是指任何被认为是“敏感”的属性,如个人的医疗状况,或学生是否通过了某门课,在每个子集 k 内至少有 L 个不同的值。
更简单地说,这意味着如果我们从一个大学班级中取出四个人的数据块,发现这些数据块具有相同的准标识符(例如相同的邮政编码、性别和年龄),那么在该组中必须至少有 L 个不同的值——我们不能让该组中的所有人都只有及格分数。
这有助于确保个体不会在同质攻击中被唯一地识别。然而,如果敏感属性的所有值都是不利的,例如在“等级”列中所有值都有不同但较低的等级,或者在“医疗状况”列中所有值都有不同类型的癌症,这仍然可能侵犯某人的隐私。
这并不能使事情变得完美,但它比 k-匿名更进了一步。然而,在对数据集执行这一操作后,它再次提出了关于数据集的统计有效性的其他问题,因为它将涉及抑制或添加行,这将改变数据的分布,并且也作为自采样偏差的一种形式。
例如,在 EdX 数据中,发现完成课程的学生比不经意的课堂观察者提供了更多关于他们自己的信息,因此这些学生中的大多数可以被唯一地识别。因此,当在数据集上使用 k-匿名和 l-多样性时,它删除了大多数已经完成课程的人!很明显,这不是一个理想的情况,并且仍然存在关于如何处理这种情况以最小化以这种方式向数据集引入偏差的公开问题。
t-贴近度
作为 k-匿名和 l-多样性的另一种扩展,隐私研究人员还提出了 t-紧密度。他们将 t-封闭性描述为:
我们提出了一种新的隐私概念,称为 t-封闭性,它要求敏感属性在任何等价类中的分布都接近属性在整个表中的分布(即两个分布之间的距离应该不超过阈值 t)。我们选择使用推土机距离测量来满足我们的 t 接近度要求。
在上面的段落中,等价类意味着 k 个匿名子集中的 k 个个体。其思想实质上是确保不仅等价类中的值是 L-多样的,而且这 L 个不同值的分布应该尽可能接近总体数据分布。这将有助于消除引入 EdX 数据集中的一些偏见,这些偏见与删除成功完成课程的人有关。
以下是针对这些算法和其他旨在保护数据隐私的算法的参考科学论文:
存在哪些规定?
存在许多隐私法规,它们可能有很大的不同,要写下并解释它们可能有点困难,所以我选择了 HIPAA 和美国 FERPA 法规作为比较的例子。
该法规涵盖了美国的所有医疗数据,最初(1996 年发布时)规定在发布数据之前,必须从数据集中删除所有个人身份信息(PII)。根据我们前面的讨论,这对应于直接标识符,即直接识别您是谁的信息,如姓名、地址和电话号码。因此,HIPAA 只需从公开发布的数据中抑制(删除/移除)这些特性就可以满足。
此后,法规得到了更新,现在要求根据 HIPAA 隐私规则取消数据集的身份,该规则规定:
- 删除上面列出的 18 个特定标识符(安全港方法)
- 经验丰富的统计专家验证和记录重新识别的统计风险的专业知识非常少(统计方法)
受保护健康信息范围内的 18 个属性是:
- 名称
- 小于一个州的所有地理标识符,邮政编码的前三位数字除外,如果根据美国人口普查局当前公开可用的数据:由所有邮政编码和相同的前三位数字组合而成的地理单元包含超过 20,000 人。
- 与个人直接相关的日期(年份除外)
- 电话号码
- 传真号码
- 电子邮件地址
- 社会安全号码
- 医疗记录号码
- 健康保险受益人数
- 账号
- 证书/许可证号码
- 车辆标识符和序列号,包括车牌号码;
- 设备标识符和序列号;
- Web 统一资源定位符(网址)
- 互联网协议(IP)地址号码
- 生物标识符,包括指纹、视网膜和声纹
- 全脸摄影图像和任何类似的图像
- 任何其他独特的识别号、特征或代码,除了研究者指定的编码数据的独特代码
请注意,HIPAA 数据并不一定是 k 匿名的。
该法规涵盖了美国的所有教育数据,包括 K-12 和大学信息。该条例规定,不仅必须清除所有 PII,而且还必须确保任何人都不可能被高度肯定地认出。这意味着我们必须满足比 HIPAA 更严格的要求。如果我们查看大学医疗信息,必须遵循更严格的法规,因此在这种情况下,FERPA 将占主导地位,而不是 HIPAA。
使用 k-匿名可以满足 FERPA 的要求。
差异隐私
本文的主旨是让您了解不同的隐私,以及它计划如何在一个数据驱动的世界中彻底改变隐私的概念。谷歌、苹果、优步,甚至美国人口普查局都采用了不同形式的差别隐私。
我们已经看到,k-匿名、l-多样性和 t-紧密度方法绝不是完美的,它们也不能保证它们是面向未来的。我们希望能够对数据集进行统计分析,例如关于人口的推断、机器学习训练、有用的描述性统计,同时仍然保护个人级别的数据免受所有攻击策略和关于个人的辅助信息的影响。使用以前的方法,有人可以在 10 年内利用新技术或算法重新识别整个数据集——没有正式的隐私保证。
这就是差别隐私为我们提供的:隐私的数学保证,它是可测量的,是未来可预见的。差别隐私的目标是给每个人大致相同的隐私,因为他们的数据被删除。也就是说,在数据库上运行的统计函数不应该过度依赖于任何一个人的数据。
密码学不适用于数据集,因为潜在的对手是数据集用户本身。因此,隐私研究人员在假设数据分析师是对手的情况下开发了一个数学框架,旨在将敏感信息泄露给分析师的可能性降至最低,即使分析师对数据集提出了多个顺序查询。
当隐私研究人员不再试图确保隐私是数据输出的属性,而是开始认为它是数据分析本身的属性时,这一启示就出现了。这导致了差别隐私的形成,它提供了一种“设计隐私”的形式,而不是作为一种事后的想法在末尾标记隐私。
负责协调数据分析师(对手)与原始数据源(我们的数据库系统)之间的接口的管理员。
因此,我们的要求是,对手不应该知道任何个人的数据是否被任意更改。简单来说,如果我删除数据集中的第二个条目,对手将无法分辨两个数据集之间的区别。
我们使用称为ϵ.的变量来测量(1)具有个体 x 的数据集和(2)不具有个体 x 的数据集之间的差异
这是如何工作的?
让我们想象一下,我们的数据分析师问馆长,数据集中艾滋病毒阳性且血型为 b 型的人占多大比例。差分私有系统会回答这个问题,并向数据中添加已知水平的随机噪声。算法是透明的,所以数据分析师被允许确切地知道这个随机噪声是从什么分布中采样的,这对于算法隐私没有任何影响。
需要添加的噪声量是 1/n,以确保如果从数据集中删除一个人,数据分析师将无法分辨,其中 n 是数据集中的人数。这应该是有意义的,因为噪声水平为 2/n 意味着分析师无法判断个人数据是否被更改、添加或删除,或者它是添加到数据集的噪声的函数。
随着数据集中个体数量的增加,为保护单个个体的隐私而必须添加的噪声量会逐渐变小。
添加的噪声通常是拉普拉斯分布的形式,隐私的级别可以用我们称之为ϵ.的“隐私参数”来控制我们可以认为这个值是两个数据集之间的差异,这两个数据集只有一个不同:一个单独的 X 出现在其中一个数据集中,而不在另一个数据集中。
隐私参数的直观定义。
当ϵ的值非常小时,我们有更大的隐私-我们有效地向数据集添加了更大量的噪声,以掩盖特定个人的存在。当ϵ的值很大时,我们的隐私就更弱了。通常,ϵ的值小于 1,通常更接近于零,大约在 0.01-0.1 之间。
这里创造的是一种算法,它确保无论对手了解到我什么,它都可以从其他人的数据中了解到。因此,诸如吸烟和肺癌之间是否存在联系的推断仍然会在数据中清晰地出现,但关于特定个人是否吸烟或患有癌症的信息将被掩盖。
对于允许多个查询的数据集,这是如何工作的?
这是一个很重要的问题:如果我向数据集提出足够多的问题,它最终会向我坦白所有人的信息吗?这就是隐私预算概念的由来。
“机制”(我们与数据库和数据分析师交互的数据管理员)受到限制,不会泄露特定于个人的信息。每次你问一个问题,它就会回答你的问题,这就消耗了你的一些隐私预算。你可以继续询问关于同一组数据的问题,直到你达到最大隐私预算,此时该机制将拒绝回答你的问题。
请注意,这不会阻止您再次询问有关数据的问题,这意味着该机制将强制您访问的数据必须添加新的噪声量,以防止隐私泄露。
隐私预算的概念起作用的原因是隐私参数的概念构成得很好——来自后续查询的隐私值可以简单地加在一起。
因此,对于 k 个查询,我们有 kε的差分隐私。只要 kε <有隐私预算,该机制仍然会响应查询。
希望,到这一点,你开始意识到这样一个数学上保证的隐私算法的含义,并且现在理解为什么它优于 k-匿名、l-多样性和 t-封闭性的概念。这已经由公司以不同的形式实施,我们将快速查看配方之间的差异。
全局差分隐私
这是我在上面的例子中提到的最直观的差分隐私形式,其中数据库由公司或政府管理员在一个集中(单一)的位置管理。
例如,美国人口普查局正计划在 2020 年美国人口普查中使用全球差异隐私。这意味着该局将得到所有的数据,并把它放入一个数据库中。在此之后,研究人员和感兴趣的团体将能够查询人口普查数据库并检索关于人口普查数据的信息,只要他们不超出他们的隐私预算。
一些人担心全球差异隐私,因为数据以原始形式存在于其来源。如果一家私人公司这么做了(优步是目前我所知道的唯一一家使用全球差分隐私的公司),那么如果这些数据被传唤,该公司将不得不交出个人的敏感信息。幸运的是,对于美国人口普查局来说,他们在加入该局时发誓绝不通过人口普查数据侵犯个人隐私,所以他们对此非常重视。此外,该局不允许被任何政府机构传唤,即使是像联邦调查局或中央情报局这样的机构,所以你的数据在相当安全的手中。
本地差分隐私
这种形式的差别隐私被谷歌用于他们的 RAPPOR 系统用于谷歌 Chrome,以及使用 IOS 10 及以上版本的苹果 IPhones。这个想法是,信息从个人的设备发出,但噪音是在源头添加的,然后以掺假的形式发送到苹果或谷歌的数据库。因此,谷歌和苹果无法访问原始和敏感的数据,即使他们被传唤,这些数据是由他人获得的,也不会侵犯你的隐私。
局部和全局差分隐私的区别。来源
最终意见
恭喜你到文章结尾了!这是对数据隐私的一次相当深入的探讨,我敦促你跟上隐私世界正在发生的事情——了解你的数据在哪里以及如何被公司和政府使用和保护,可能会成为未来数据驱动社会的一个重要话题。
“独处是现代世界中最珍贵的东西。”
― 安东尼·伯吉斯
时事通讯
关于新博客文章和额外内容的更新,请注册我的时事通讯。
丰富您的学术之旅,加入一个由科学家,研究人员和行业专业人士组成的社区,以获得…
mailchi.mp](https://mailchi.mp/6304809e49e7/matthew-stewart)
数据隐私、安全和新冠肺炎
安全性和数据隐私之间的权衡
数据被广泛用于对抗新冠肺炎。大部分数据是个人数据:
- 各州正在聚集力量加强对我们行动和健康状况的监控。以台湾为例,它全面强制跟踪手机,以监控被隔离的个人。西班牙现在推出了一个公共卫生监测应用程序,要求个人报告他们的每日体温,以帮助更准确地绘制传播图,并为早期病例提供支持
- 私营企业正在通过应用程序收集数据,包括体温、既往病史、姓名、电话号码等,以支持对抗这种疾病的快速研究进展
- 个人自愿向私营和公共部门提供个人数据,以帮助绘制地图、追踪、跟踪和摧毁新冠肺炎。通过每天报告我们的体温,我们允许卫生服务部门快速识别冠状病毒阳性病例,并尽早解决它们。这种大规模数据报告还允许更准确地绘制扩散图和根病例定位图。但我们也在与雇主分享比平时更多的个人数据,以确保我们继续工作的能力,并与其他为新冠肺炎研究做出贡献的私营公司分享
作为个人,我们正在我们的安全和数据隐私之间进行权衡。
可以理解。今天不分享你的个人数据可能会导致人类死亡,包括你和你所爱的人,当我们的生存依赖于分享个人信息时,降低我们对隐私的担忧似乎是合理的。
但这是一个特殊的情况和一个明确的目的,不是一概的许可,也不是承认我们愿意放弃我们的隐私权。
新冠肺炎期间的数据隐私
人们有理由担心,在这场危机之后,数据隐私可能无法恢复。
历史上的许多例子表明,虽然国家在攫取权力方面速度很快,但一旦被攫取,它通常很不愿意放手。
为了避免这种情况,这种全社会范围的个人数据共享行为需要在同意的障碍下理解。
必须遵循《一般数据保护条例》( GDPR)中规定的同意原则。我们自愿提供数据的唯一目的是为了对抗新冠肺炎。
GDPR 案文第五条第一款概述的原则必须遵守。这些要求个人数据应:
“(a)以合法、公平和透明的方式对个人进行处理(“合法、公平和透明”);”
(b)为特定、明确和合法的目的收集,并且不以不符合这些目的的方式进一步处理;出于公共利益、科学或历史研究目的或统计目的的存档目的的进一步处理不应被视为与初始目的不相容(“目的限制”);”
©充分、相关且仅限于处理目的所必需的内容(“数据最小化”);”
(d)准确,并在必要时保持更新;必须采取一切合理措施,确保不准确的个人数据(考虑到处理这些数据的目的)立即被删除或更正(“准确性”)。”
(e)以允许识别数据主体的形式保存,保存时间不超过处理个人数据所需的时间;个人数据可以存储更长时间,前提是个人数据的处理仅用于出于公共利益、科学或历史研究目的或统计目的的存档目的,但需执行 GDPR 要求的适当技术和组织措施,以保护个人的权利和自由(“存储限制”);”
(f)使用适当的技术或组织措施(“完整性和保密性”),以确保个人数据适当安全的方式进行处理,包括防止未经授权或非法的处理以及防止意外丢失、破坏或损坏。”
在这种特殊情况下,我们提供数据的唯一目的是为了抗击这种病毒*(“目的限制”);我们期望我们的数据不会被非法使用,并要求了解这些数据的用途(“合法、公平和透明”);实施的监测措施应仅整理和分析相关的和需要用于此目的的数据,例如无人驾驶飞机、电话 GPS 跟踪等……(“数据最小化”);我们希望我们的数据在任何时候都不会受到未经授权的访问(“完整性和保密性”)*
新冠肺炎会议后的数据隐私工作
在克服这一危机之后,数据隐私方面的工作必须允许个人了解谁拥有他们的数据,在哪里,出于什么目的,并行使他们的数据主体权利。
所需的一些工作包括:
a) 由于冠状病毒危机期间的数据共享,盘点哪些数据保存在哪里。类似于 GDPR 生效时企业所做的工作。目前整理个人数据的公共和私营机构需要清点冠状病毒爆发期间从数据主体持有的数据(结构化和非结构化数据),以审核其是否符合存储限制、准确性和完整性以及保密性的原则。
**获取个人的同意。**公共和私人代理都需要确保对持有的任何个人数据以及在不确定的情况下应该再次收集的个人数据进行同意。目前,政府和私人公司都被默许访问和使用个人数据来对抗新冠肺炎病毒。在这种情况下,议会授予的特殊权力和个人自愿提供数据就足够了。然而,在此之后,将需要获得用户的明确同意,以继续持有和/或使用经过整理的数据,特别是在目的发生变化时。
c) **允许个人行使其数据权利。**为了抗击冠状病毒,个人应该有机会对危机期间收集的任何数据行使删除、遗忘、纠正等权利。
随着人工智能技术的普及和数据在未来几年成为重要的价值来源,保护数据隐私和个人决定使用自己数据的权利应该成为每个人的首要任务。
不管情况如何,我们应该总是要求机制来收回我们的数据并重新访问我们的同意。新冠肺炎就是这样一个例子,在那里,我们放松数据隐私要求的意愿是准时的,并且是为了特定的目的。
使用 Python 的数据处理示例
只是为分析和机器学习准备数据集所涉及的一些步骤。
来源:图片由作者创建
《福布斯》的调查发现,数据科学家工作中最不愉快的部分占据了他们 80%的时间。20%用于收集数据,另外 60%用于清理和组织数据集。就我个人而言,我不同意 80%是我们工作中最不愉快的部分。我经常将数据清理任务视为一个开放式问题。通常,每个数据集可以根据手头的问题以数百种不同的方式进行处理,但我们很少能够将同一组分析和转换从一个数据集应用到另一个数据集。我发现构建不同的处理管道并研究它们的差异如何影响模型性能是我工作中令人愉快的一部分。
也就是说,我想花点时间向您介绍准备数据集进行分析的代码和思考过程,在这种情况下,这将是一个回归(即。多元回归)。
资料组
关注我的人都知道,我对人力资源数据集特别感兴趣,因为我大部分职业生涯都在这个行业工作。
如果你有一个罕见的人力资源数据集,请与我们分享:)
我们将使用 310 名在职和离职员工的数据集,以及婚姻状况、性别、部门、工资率、州、职位等信息。由于我们正在为回归分析准备数据,我们的目标功能是 EngagementSurvey。
我们数据集的代码本可以在这里找到。
分析
import numpy as np
import pandas as pd
import datetime
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.pipeline import make_pipeline
from feature_engine import missing_data_imputers as mdi
from feature_engine import categorical_encoders as ce
from sklearn.model_selection import train_test_split%matplotlib inlinewith open('HRDataset.csv') as f:
df = pd.read_csv(f)
f.close()df.head()df.info()
加载数据后,我们可以看到许多独特的要素类型。我们有分类特征,例如“雇员姓名”和“职位”。我们有二进制特征,比如“MarriedID”。我们有持续的功能,如“支付率”和“员工满意度”。我们有离散的功能,如“DaysLateLast30”,最后我们有日期功能,如“LastPerformanceReview_Date”。
高或低特征可变性
我通常采取的第一步是检查每个要素值的唯一计数,以确定是否有任何要素会由于非常高或非常低的可变性而被快速删除。换句话说,我们是否有任何具有与数据集长度一样多的唯一值的要素,或者只有一个唯一值的要素?
for col in df.columns:
print(col, df[col].nunique(), len(df))
df.drop(['Employee_Name'], axis=1, inplace=True)
df.drop(['EmpID'], axis=1, inplace=True)
df.drop(['DOB'], axis=1, inplace=True)
df.drop(['DaysLateLast30'], axis=1, inplace=True)
我们可以安全地删除“雇员姓名”、“雇员 ID”、“出生日期”,因为大多数(如果不是全部的话)值对于每个特性都是唯一的。此外,我们可以删除“DaysLateLast30 ”,因为该特性只包含一个唯一值。
重复特征
接下来,通过检查包含每个特性的定义的码本,我们可以看到我们有许多重复的特性。例如,“MarriedStatusID”是一个数字特征,它生成与“MaritalDesc”特征中的已婚雕像相匹配的代码。我们可以放弃这些功能。
df.drop(['MaritalStatusID', 'EmpStatusID', 'DeptID'], axis=1, inplace=True)
df.drop(['GenderID'], axis=1, inplace=True)
df.drop(['PerformanceScore'], axis=1, inplace=True)
df.drop(['MarriedID'], axis=1, inplace=True)
你可能会问自己“那么‘Position id’,‘Position’,‘manager id’和‘manager name’呢?".从上面的输出可以看出,这些特征对的唯一值计数不匹配。“位置 ID”有 32 个唯一值,而“位置”有 30 个。
df[['PositionID', 'Position']].sort_values('PositionID')[50:70]
df[['ManagerName', 'ManagerID']].sort_values(by='ManagerID').tail(50)df.drop('PositionID', axis=1, inplace=True)
df.drop('ManagerID', axis=1, inplace=True)
我们将删除“PositionID ”,因为它不维护所有可用的位置;我们将删除“ManagerID ”,因为“ManagerName”不包含任何缺失值。
奇数值和数据收集错误
接下来,让我们检查每个特性的单个唯一值。这将有助于我们看到任何赔率值和需要修复的错误。
for col in df.columns:
print(col, df[col].unique(), len(df))
从码本中,我们知道诸如“FromDiversityJobFairID”和“Termd”的特征是“是”和“否”的二进制编码。为了简化我们的分析和帮助格式化,我们需要将二进制转换成字符串。我们还看到需要删除“数据分析师”和“生产”部门的尾随空格。最后,我们看到一个需要纠正的“西班牙裔拉丁人”的编码错误。
diversity_map = {1: 'yes', 0: 'no'}
termd_map = {1: 'yes', 0: 'no'}
hispanic_latino_map = {'No': 'no', 'Yes': 'yes', 'no': 'no', 'yes': 'yes'}df['FromDiversityJobFairID'].replace(diversity_map, inplace=True)
df['Termd'].replace(termd_map, inplace=True)
df['HispanicLatino'].replace(hispanic_latino_map, inplace=True)df['Position'] = df['Position'].str.strip()
df['Department'] = df['Department'].str.strip()
你可能会问自己“为什么有些邮政编码是 5 位数,而有些只有 4 位数?”。在美国,所有的邮政编码都是 5 位数。经过一点谷歌搜索,许多马萨诸塞州的邮政编码实际上是从零开始的,默认情况下,python 去掉了零,产生了 4 位数的邮政编码。因为我们将把邮政编码视为分类特征,所以长度并不重要。
处理日期时间功能
信不信由你,但是日期时间特性通常包含大量的信息等待被释放。当一个人熟悉数据来源的行业时,这一点尤其明显。
df['DateofHire'] = pd.to_datetime(df['DateofHire'])
df['DateofTermination'] = pd.to_datetime(df['DateofTermination'])
df['LastPerformanceReview_Date'] = pd.to_datetime(df['LastPerformanceReview_Date'])df['DateofHire_month'] = df['DateofHire'].dt.month
df['DateofHire_day'] = df['DateofHire'].dt.day
df['DateofHire_year'] = df['DateofHire'].dt.year
df['DateofHire_quarter'] = df['DateofHire'].dt.quarter
df['DateofHire_day_week'] = df['DateofHire'].dt.day_name()
df['DateofHire_weekday'] = np.where(df['DateofHire_day_week'].isin(['Sunday','Saturday']),'yes','no')df['DateofTerm_month'] = df['DateofTermination'].dt.month
df['DateofTerm_day'] = df['DateofTermination'].dt.day
df['DateofTerm_year'] = df['DateofTermination'].dt.year
df['DateofTerm_quarter'] = df['DateofTermination'].dt.quarter
df['DateofTerm_day_week'] = df['DateofTermination'].dt.day_name()
df['DateofTerm_weekday'] = np.where(df['DateofTerm_day_week'].isin(['Sunday','Saturday']),'yes','no')df['LastPerform_month'] = df['LastPerformanceReview_Date'].dt.month
df['LastPerform_day'] = df['LastPerformanceReview_Date'].dt.day
df['LastPerform_year'] = df['LastPerformanceReview_Date'].dt.year
df['LastPerform_quarter'] = df['LastPerformanceReview_Date'].dt.quarter
df['LastPerform_day_week'] = df['LastPerformanceReview_Date'].dt.day_name()
df['LastPerform_weekday'] = np.where(df['LastPerform_day_week'].isin(['Sunday','Saturday']),'yes','no')df['tenure_termed'] = df['DateofTermination'] - df['DateofHire']
df['tenure'] = datetime.datetime.today() - df['DateofHire']
df['days_since_review'] = datetime.datetime.today() - df['LastPerformanceReview_Date']df.drop(['DateofHire', 'DateofTermination', 'LastPerformanceReview_Date'], axis=1, inplace=True)df.head()
首先,我们需要将我们的特性转换成日期时间格式。接下来,使用“datetime”库,我们可以从原始的 datetime 特性中提取新的特性,包括月、日、年、季度、工作日字符串,甚至当天是否是周末。最后,我们可以将各个日期相减,计算出任期(终止日期—聘用日期)和任期(今天的日期—聘用日期)。一旦我们提取了必要的信息,我们可以放弃原来的功能。
df['days_since_review'] = df['days_since_review'].astype(str)
df['days_since_review'] = [i[0:3] for i in df['days_since_review']]df['days_since_review'] = df['days_since_review'].astype(str)
df['days_since_review'] = [i[0:3] for i in df['days_since_review']]df['tenure_termed'] = df['tenure_termed'].astype(str)
df['tenure_termed'] = [i[0:2] for i in df['tenure_termed']]for var in df.columns:
df[var].replace(to_replace=['NaT','Na'], value=np.nan, inplace=True)df.head()
也许我有点强迫症,但我喜欢整洁的数据集,因此,让我们从这些新功能中删除不相关的信息,如“天”和时间戳。最后,我们将 NaT 和 Na 转换为真正的 numpy NaN。
检查基数
for var in df.columns:
print(var, '\n', df[var].value_counts()/len(df))df.drop(['CitizenDesc', 'DateofHire_weekday', 'DateofTerm_weekday',
'LastPerform_quarter', 'LastPerform_weekday', 'LastPerform_year'], axis=1, inplace=True)
低方差
基数是指每个特性的唯一值/类别的数量。数字特征,尤其是连续特征,将具有非常高的基数,但是我们主要需要关注分类特征。首先,我们需要识别包含吸收所有方差的值/类别的特征。换句话说,90%以上的观察值属于一个或两个值。例如,“CitizenDesc”有三个唯一的值,但我们看到“美国公民”包含所有观察值的 95%。不幸的是,展示这种模式的其他功能是我们新设计的功能,如“DateofHire_weekday”、“DateofTerm_weekday”、“LastPerform_quarter”、“LastPerform_weekday”和“LastPerform_year”。我们可以放心地放弃这些特性,因为它们没有提供足够的可变性,没有意义。
稀有值/类别
使用与上面相同的代码,我们再次将注意力转向分类特征,但这一次我们要寻找我们认为“罕见”的值。你如何定义“稀有”真的取决于你,但我发现这个决定必须根据不同的特性来做。如果某些值出现的时间少于 1%,那么它们可能很少见。在其他特性中,阈值可能是 2%甚至 5%。我们的最终目标是将这些价值组合成一个新的价值/类别,称为“稀有”。此过程减少了特征的整体基数,如果您选择一次性编码您的类别特征,此方法将大大减少新创建的“虚拟”特征的数量。
- 声明:低于 1% 的任何东西都将被视为‘稀有’
- 位置:任何低于 2% 的情况都将被视为“罕见”
- Zip :任何低于 2% 的都将被视为“罕见”
- RaceDesc :任何低于 2% 的都将被视为“罕见”
- 招聘来源:低于 2% 的任何东西都将被视为“稀有”
- 三天的日期:任何低于 2% 的日期都将被视为“罕见”
- DateofTerm_month :小于 2% 的任何数据都将被视为“罕见”
- DateofTerm_day :小于 2% 的任何数据都将被视为“罕见”
- LastPerform_day :任何低于 2% 的情况都将被视为“罕见”
- LastPerform_day_week :小于 2% 的任何内容都将被视为“罕见”
- 一年中的日期:任何小于 2% 的日期都将被视为“罕见”
- DateofTerm_year :小于 2% 的任何数据都将被视为“罕见”
- 经理姓名:任何低于 5% 的都将被视为‘罕见’
缺少值
决定如何处理缺失值是数据科学家将做出的最重要和最有争议的决定之一。
for var in df.columns:
if df[var].isnull().sum()/len(df) > 0:
print(var, df[var].isnull().mean().round(3))df.drop('tenure_termed', axis=1, inplace=True)
TermReason 是一个分类特征,只有一些缺失的数据点。我们可以使用模式估算该数据,因为这不会改变特征的分布。此外,我们可以有把握地假设,缺少 TermReason 仅仅意味着该雇员仍然有效。剩下的带有缺失数据的特征就是我们所说的“非随机缺失”(MNAR)。换句话说,这些特征的缺失是有潜在原因的。首先,缺失值的百分比似乎在重复,这给了我们一个线索,即这些缺失值有一个可辨别的模式。其次,我们从数据中了解到,大约 67%的员工是在职的,不会有离职日期。最后,在最近的绩效考核周期后雇用的员工通常不会有与其上次绩效考核日期相关联的日期。如果您希望了解更多关于缺失值的信息,请参考此资源。
有些人会认为 67%的缺失值实际上使特性变得无用,我同意我们的“任期”特性的这种观点。输入这个数字特征可能会在我们的数据中引入太多的误差方差/偏差。然而,诸如“DateofTerm_month”和“LastPerform_month”之类的特征在本质上是分类的,它们的缺失数据具有明确的模式。我想通过用字符串“missing”输入所有缺失值来捕捉缺失值的重要性。通过这种方式,我们为每个特性引入了另一个值/类别,它恰当地捕捉到了缺失值背后的模式。
另一方面,“天数 _ 自 _ 回顾”是一个数字特征,它是 MNAR。换句话说,为了捕捉这些缺失值的重要性,我们将估算一个任意的数字(即-9999)并创建一个新特征,该新特征将指示该特征的观测值是否缺失。
- 术语原因:用模式估算
- DateofTerm_month:使用’ missing '进行估算以创建新类别
- DateofTerm_day:用’ missing '估算以创建新类别
- DateofTerm_year:用’ missing '估算以创建新类别
- DateofTerm_quarter:用’ missing '估算以创建新类别
- DateofTerm_day_week:用’ missing '估算以创建新类别
- LastPerform_month:用’ missing '估算以创建新类别
- LastPerform_day:用’ missing '估算以创建新类别
- LastPerform_day_week:用’ missing '估算以创建新类别
- 任期 _ 期限:由于大量数据缺失而下降
- 审查以来的天数:任意插补和缺失指标特征
处理异常值
离群值是另一个需要思考的有争议的话题。有许多处理异常值的方法。如果您有一个非常大的数据集和相对较少的离群值,您可以简单地删除它们。我通常对这种方法持谨慎态度,因为它会改变所述特征的分布,这可能会导致新值成为异常值。也就是说,这是一个经常使用的选项。其他方法包括添加指示要素,使用 np.log()重新缩放整个要素,以及通过应用离散化将异常值包含在一个条柱中来将连续要素转换为离散要素。
首先,我们需要确定是否有异常值。用于识别异常值的最著名的方法是 z-score 方法,该方法将特征值标准化为均值为零,标准差为一,任何低于 3 个标准差(正或负)的值都被视为异常值。就个人而言,我认为这种方法是有缺陷的,因为 z 值依赖于特征的平均值和标准差。均值和标准差都受到现有异常值的严重影响。均值和标准差计算中包含的任何异常值都将扩大 z 值的范围,并可能忽略现有的异常值。这个问题可以通过使用中间值而不是平均值来解决。
让我们利用一个更稳健的方法,它依赖于四分位距和中位数。您可以调整此方法,并使用(3 * IQR)仅识别极端异常值。
def outlier_treatment(feature):
sorted(feature)
q1,q3 = np.percentile(feature , [25,75])
IQR = q3 - q1
lower_range = q1 - (1.5 * IQR)
upper_range = q3 + (1.5 * IQR)
return lower_range,upper_rangeoutlier_treatment(df['PayRate'])
lower_range, upper_range = outlier_treatment(df['PayRate'])df[(df['PayRate'] < lower_range) | (df['PayRate'] > upper_range)]
训练/测试分割
X = df.drop('EngagementSurvey', axis=1)
y = df['EngagementSurvey']X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
数据处理流水线
**# impute categorical features with more than 5% missing values w/ a new category 'missing'**
process_pipe = make_pipeline(
mdi.CategoricalVariableImputer(variables=['DateofTerm_month', 'DateofTerm_day','DateofTerm_quarter', 'DateofTerm_day_week',
'LastPerform_month', 'LastPerform_day','LastPerform_day_week', 'DateofTerm_year'], imputation_method='missing'),
**# Imputing categorical features with less than 5% missing values w/the mode**
mdi.CategoricalVariableImputer(variables=['TermReason'], imputation_method='frequent'),
**# Imputing missing values for numerical feature 'days_since_review' with an arbitrary digit**
mdi.ArbitraryNumberImputer(arbitrary_number = -99999, variables='days_since_review'),
**# We are adding a feature to indicate (binary indicator) which records were missing**
mdi.AddMissingIndicator(variables=['days_since_review']),
**# Encoding rare categories (less than 1% & the feature must have at least 5 categories)**
ce.RareLabelCategoricalEncoder(tol=0.01, n_categories=5,
variables=['State']),
**# Encoding rare categories (less than 2% & the feature must have at least 5 categories)**
ce.RareLabelCategoricalEncoder(tol=0.02, n_categories=5,
variables=['Position', 'Zip', 'DateofTerm_day', 'LastPerform_day_week', 'DateofTerm_year', 'RaceDesc', 'TermReason', 'RecruitmentSource','DateofHire_day', 'DateofTerm_month',
'LastPerform_day', 'DateofHire_year']),
**# Encoding rare categories (less than 5% & the feature must have at least 5 categories)**
ce.RareLabelCategoricalEncoder(tol=0.05, n_categories=5,
variables=['ManagerName']),
**# Target or Mean encoding for categorical features**
ce.OrdinalCategoricalEncoder(encoding_method='ordered',
variables=['FromDiversityJobFairID', 'Termd','Position', 'State','Zip','Sex', 'MaritalDesc','HispanicLatino', 'RaceDesc', 'TermReason','EmploymentStatus', 'Department', 'ManagerName',
'RecruitmentSource', 'DateofHire_month','DateofHire_day', 'DateofHire_day','DateofHire_quarter', 'DateofHire_day_week',
'DateofTerm_month', 'DateofTerm_day','DateofTerm_year', 'DateofTerm_quarter','DateofTerm_day_week', 'LastPerform_month',
'LastPerform_day', 'LastPerform_day_week']
))
我们还没有讨论的一个话题是分类特征编码。我通常尝试并避免使用一键编码,因为它有极大扩展特征空间的趋势。如果我们使用一次性编码,“稀有”值/类别的编码当然有助于解决这个问题。也就是说,我选择使用目标或均值编码,因为它不会扩展特性集。这种方法用从 0 到 k-1 的数字替换类别。我们首先为每个类别的每个分类特征计算目标变量的平均值,然后根据平均值大小用上述数字替换平均值。例如,我们有一个二元目标,第一个分类特征是性别,它有三个类别(男性、女性和未公开)。让我们假设男性的平均值是 0.8,女性是 0.5,未披露的是 0.2。编码值将是男性=2,女性=1,未公开=0。
资料处理
process_pipe.fit(X_train, y_train)X_train_clean = process_pipe.transform(X_train)
X_test_clean = process_pipe.transform(X_test)X_train_clean.head()
摘要
如果您认为我可能错过了某个重要步骤,请随时提供反馈。感谢阅读!