异常数据检查
1)问题场景(背景,常见的问题)
2)问题解决思路(遇到这种问题,应该如何分析与解决)
3)实操指导(给出一些具体的步骤)
概述
在对神经网络进行训练之前,一般需要准备好大量、丰富的可训练数据,使模型得到充分的训练,以提升其准确率、泛化能力。
数据的来源是多样的,在准备训练数据时,有可能从本地读取数据,也可能远端服务器拉取数据。当读取到的数据丰富度不够时,可能会使用一些通用的数据增强方法对数据的样式和种类进行扩展,或者根据具体的需要对数据进行预处理计算(如token to id等),以满足神经网络的输入需要。
在对数据进行处理的过程中,可能会因为计算错误、数值溢出等因素,产生了异常的结果数值,从而导致训练网络时算子计算溢出、权重更新异常等问题。本章节将介绍如何基于MindSpore的脚本检查异常的数据行为/数据结果。
检查思路
关闭混洗,固定随机种子,确保可重现性
在一些数据处理的场景中,我们会使用随机的函数,作为数据运算的一部分。由于随机运算本身的特性,每一次运行的数据结果都不相同,这样很有可能会出现上一次运行的结果中存在异常的数值,但在下一次运行的时候,却没有检查到异常的数值,那么很有可能是因为随机索引/随机计算的影响。这种情况下,可以关闭数据集的混洗选项,并固定不同的随机种子,通过多次运行寻找可能引入的随机问题。
以下例子把一个随机值当做是一个除数,在偶然的情况下会出现除0的情况。
import numpy as np
import mindspore as ms
class Gen():
def __init__(self):
self.data = [np.array(i) / np.random.randint(0, 3) for i in range(1, 5)]
def __getitem__(self, index):
data = self.data[index]
return data
def __len__(self):
return len(self.data)
dataset = ms.dataset.GeneratorDataset(Gen(), ["data"])
for data in dataset:
print(data)
复制
通过set_seed
控制随机结果的可重现性,可以进一步排查代码中随机运算是否符合预期。
ms.set_seed(1)
ms.dataset.GeneratorDataset(Gen(), ["data"], shuffle=False)
复制
多次运行结果均一致,可以看到第2条数据和第3条数据出现的除零的结果。间接可以说明,在第2条和第3条数据的计算上存在异常导致出现了inf的数值。
[Tensor(shape=[], dtype=Float64, value= 1)]
[Tensor(shape=[], dtype=Float64, value= inf)]
[Tensor(shape=[], dtype=Float64, value= inf)]
[Tensor(shape=[], dtype=Float64, value= 4)]
复制
利用NumPy等工具快速校验结果
上一个例子中的数据量较少,基本上可以通过检查代码发现出现异常数值的位置。对于一些大型的高维数组,代码检查或者打印数值就不太方便了。这个时候,可以配置MindSpoer的数据集以NumPy的形式返回数据,并借助NumPy的一些常用检查数组内容的手段去检查数组中是否存在异常数值。
以下例子构造了一个大型的高维数组,并对其中的数值进行随机的运算。
import numpy as np
import mindspore as ms
class Gen():
def __init__(self):
self.data = np.random.randint(0, 255, size=(16, 50, 50))
def __getitem__(self, index):
data = self.data[index] / np.random.randint(0, 2)
return data
def __len__(self):
return 16
dataset = ms.dataset.GeneratorDataset(Gen(), ["data"])
for data in dataset:
print(data)
复制
为了检查在数据运算时存在异常数值如nan、inf等,可以在遍历数据集对象时,指定其输出为NumPy类型。
指定了输出类型后,打印的data
对象中各个元素均为NumPy类型,基于此可以采用NumPy中的一些非常方便的函数校验其中的数值是否异常
for data_index, data in enumerate(dataset.create_tuple_iterator(output_numpy=True)):
print(data_index) # 第N条数据。如果期望固定数据顺序,可以参考上一节关闭混洗与固定随机种子
print(data) # NumPy形式的数据
print(np.isnan(data).any()) # 检查是否存在nan数值
print(np.isnan(data)) # 检查nan数值的位置,改函数返回一个mask标记,True为nan位置
print(np.isinf(data).any()) # 检查是否存在inf数值
print(np.isinf(data)) # 检查nan数值的位置,改函数返回一个mask标记,True为inf位置
复制
数据Pipeline分段检查数值输出
在一些情况下,数据集Pipeline的定义可能比上述例子要复杂的多,主要体现在多了map
操作、shuffle
操作、batch
操作等。由于每个操作都会引入一些运算,用户的代码很可能在任意一个运算环节产生异常的数值。为了寻找可能是哪一个数据节点引入了异常数值,可以采用Pipeline分段的方式检查各个数据节点的输出值。
以下例子构造了一个较复杂的数据Pipeline,由GeneratorDataset -> map -> batch
组成。
import numpy as np
import mindspore as ms
class Gen():
def __init__(self):
self.data = np.random.randint(0, 255, size=(16, 50, 50))
def __getitem__(self, index):
data = self.data[index]
return data
def __len__(self):
return 16
def map_fun(data):
data = data / np.random.randint(0, 2)
return data
dataset = ms.dataset.GeneratorDataset(Gen(), ["data"], shuffle=False)
map_dataset = dataset.map(map_fun, ["data"])
shuffle_dataset = map_dataset.shuffle(2)
batch_dataset = shuffle_dataset.batch(4)
for data in batch_dataset.create_tuple_iterator(output_numpy=True):
print(np.isnan(data).any())
print(data)
复制
此数据Pipeline的功能为:由GeneratorDataset产生数据,数据流转到map
操作,对每一条数据执行map_fun
的内容并返回,紧接着数据流转到shuffle
操作,对数据进行混洗和采样,最后数据流转到batch
操作,组合成batch size为4的高维数组。
运行上述代码是会发现np.isnan(data).any()
返回的是True,也就是说数据处理流程中引入了异常数值。但由于存在4个数据节点,粗略一看难以检查异常数据究竟引入自哪一个数据节点。因此可以对不同的数据节点获取数据,以便进一步确定异常数值引入的起始点。数据Pipeline可以获取某一个数据节点的数据,如
# 获取batch操作节点的输出数据,并指定输出数据类型为NumPy,校验其中的数值
for data in batch_dataset.create_tuple_iterator(output_numpy=True):
print(np.isnan(data).any())
print(np.isinf(data).any())
# 获取shuffle操作节点的输出数据,并指定输出数据类型为NumPy,校验其中的数值
for data in shuffle_dataset.create_tuple_iterator(output_numpy=True):
print(np.isnan(data).any())
print(np.isinf(data).any())
# 获取map操作节点的输出数据,并指定输出数据类型为NumPy,校验其中的数值
for data in map_dataset.create_tuple_iterator(output_numpy=True):
print(np.isnan(data).any())
print(np.isinf(data).any())
# 获取GeneratorDataset节点的输出数据,并指定输出数据类型为NumPy,校验其中的数值
for data in dataset.create_tuple_iterator(output_numpy=True):
print(np.isnan(data).any())
print(np.isinf(data).any())
复制
由于上述代码是故意在map
数据节点引入异常数值的,因此在获取batch
、shuffle
、map
数据节点的数据时,均会发现存在nan或inf数据。而在GeneratorDataset
数据节点,多次运行均无发现异常数值,即可判断异常数值的引入起始点是GeneratorDataset
节点的下一个节点,即map
数据节点。
当大致确定map
操作引入了异常数据后,可以尝试在map
操作所执行的函数map_fun
前后增加一些日志打印,以确认具体哪一步操作计算出异常数值。
def map_fun(data):
print("orginal data", data)
data = data / np.random.randint(0, 2)
print("processed data", data)
return data
复制
结语
数据处理仅是神经网络训练的其中一个环节。在神经网络训练中途也可能因为各种数学运算、网络操作导致输出或梯度值异常。总的来说,可以优先从以下几个方面检查。
-
数据问题:输入数据中可能存在NaN或Inf(无穷大)的值,这可能会导致计算结果为NaN。可以按照上文建议检查输入数据。
-
模型结构问题:模型中的某些层或操作可能会导致NaN的输出。例如,当计算log或sqrt等函数的负数时,会出现NaN。建议检查模型结构,确保模型中的所有操作都是数值稳定的。
-
梯度问题:当模型的梯度变得非常大时,会导致计算结果为NaN。这可能是由于学习率设置过高或梯度爆炸导致的。建议检查学习率设置,或尝试使用梯度剪裁来控制梯度范围。