为啥我的结果仍然无法重现?
Posted
技术标签:
【中文标题】为啥我的结果仍然无法重现?【英文标题】:Why are my results still not reproducible?为什么我的结果仍然无法重现? 【发布时间】:2020-02-14 10:18:58 【问题描述】:我想为 CNN 获得可重现的结果。我使用带有 GPU 的 Keras 和 Google Colab。
除了建议插入某些代码 sn-ps(应该允许可重复性)之外,我还向层添加了种子。
###### This is the first code snipped to run #####
!pip install -U -q PyDrive
from pydrive.auth import GoogleAuth
from pydrive.drive import GoogleDrive
from google.colab import auth
from oauth2client.client import GoogleCredentials
# Authenticate and create the PyDrive client.
# This only needs to be done once per notebook.
auth.authenticate_user()
gauth = GoogleAuth()
gauth.credentials = GoogleCredentials.get_application_default()
drive = GoogleDrive(gauth)
###### This is the second code snipped to run #####
from __future__ import print_function
import numpy as np
import tensorflow as tf
print(tf.test.gpu_device_name())
import random as rn
import os
os.environ['PYTHONASHSEED'] = '0'
np.random.seed(1)
rn.seed(1)
session_conf = tf.ConfigProto(intra_op_parallelism_threads=1, inter_op_parallelism_threads=1)
###### This is the third code snipped to run #####
from keras import backend as K
tf.set_random_seed(1)
sess = tf.Session(graph=tf.get_default_graph(), config=session_conf)
K.set_session(sess)
###### This is the fourth code snipped to run #####
def model_cnn():
model = Sequential()
model.add(Conv2D(32, kernel_size=(3,3), kernel_initializer=initializers.glorot_uniform(seed=1), input_shape=(28,28,1)))
model.add(BatchNormalization())
model.add(Activation('relu'))
model.add(Conv2D(32, kernel_size=(3,3), kernel_initializer=initializers.glorot_uniform(seed=2)))
model.add(BatchNormalization())
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Dropout(0.25, seed=1))
model.add(Flatten())
model.add(Dense(512, kernel_initializer=initializers.glorot_uniform(seed=2)))
model.add(BatchNormalization())
model.add(Activation('relu'))
model.add(Dropout(0.5, seed=1))
model.add(Dense(10, kernel_initializer=initializers.glorot_uniform(seed=2)))
model.add(Activation('softmax'))
model.compile(loss="categorical_crossentropy", optimizer=Adam(lr=0.001), metrics=['accuracy'])
return model
def split_data(X,y):
X_train_val, X_val, y_train_val, y_val = train_test_split(X, y, random_state=42, test_size=1/5, stratify=y)
return(X_train_val, X_val, y_train_val, y_val)
def train_model_with_EarlyStopping(model, X, y):
# make train and validation data
X_tr, X_val, y_tr, y_val = split_data(X,y)
es = EarlyStopping(monitor='val_loss', patience=20, mode='min', restore_best_weights=True)
history = model.fit(X_tr, y_tr,
batch_size=64,
epochs=200,
verbose=1,
validation_data=(X_val,y_val),
callbacks=[es])
return history
###### This is the fifth code snipped to run #####
train_model_with_EarlyStopping(model_cnn(), X, y)
我总是运行上面的代码,得到不同的结果。 原因是否在于代码,还是根本无法在支持 GPU 的 Google Colab 中获得可重现的结果?
完整代码(代码中有不必要的部分,比如没有用到的库):
!pip install -U -q PyDrive
from pydrive.auth import GoogleAuth
from pydrive.drive import GoogleDrive
from google.colab import auth
from oauth2client.client import GoogleCredentials
auth.authenticate_user()
gauth = GoogleAuth()
gauth.credentials = GoogleCredentials.get_application_default()
drive = GoogleDrive(gauth)
from __future__ import print_function # NEU
import numpy as np
import tensorflow as tf
import random as rn
import os
os.environ['PYTHONASHSEED'] = '0'
np.random.seed(1)
rn.seed(1)
session_conf = tf.ConfigProto(intra_op_parallelism_threads=1, inter_op_parallelism_threads=1)
from keras import backend as K
tf.set_random_seed(1)
sess = tf.Session(graph=tf.get_default_graph(), config=session_conf)
K.set_session(sess)
import os
local_root_path = os.path.expanduser("~/data/data")
print(local_root_path)
try:
os.makedirs(local_root_path, exist_ok=True)
except: pass
def ListFolder(google_drive_id, destination):
file_list = drive.ListFile('q': "'%s' in parents and trashed=false" % google_drive_id).GetList()
counter = 0
for f in file_list:
# If it is a directory then, create the dicrectory and upload the file inside it
if f['mimeType']=='application/vnd.google-apps.folder':
folder_path = os.path.join(destination, f['title'])
os.makedirs(folder_path, exist_ok=True)
print('creating directory '.format(folder_path))
ListFolder(f['id'], folder_path)
else:
fname = os.path.join(destination, f['title'])
f_ = drive.CreateFile('id': f['id'])
f_.GetContentFile(fname)
counter += 1
print(' files were uploaded in '.format(counter, destination))
ListFolder("1DyM_D2ZJ5UHIXmXq4uHzKqXSkLTH-lSo", local_root_path)
import glob
import h5py
from time import time
from keras import initializers
from keras.preprocessing.image import ImageDataGenerator
from keras.models import Sequential, model_from_json
from keras.layers import Dense, Dropout, Activation, Flatten, BatchNormalization, merge
from keras.layers import Convolution2D, MaxPooling2D, AveragePooling2D
from keras.optimizers import SGD, Adam, RMSprop, Adagrad, Adadelta, Adamax, Nadam
from keras.utils import np_utils
from keras.callbacks import LearningRateScheduler, ModelCheckpoint, TensorBoard, ReduceLROnPlateau
from keras.regularizers import l2
from keras.layers.advanced_activations import LeakyReLU, ELU
from keras import backend as K
import numpy as np
import pickle as pkl
from matplotlib import pyplot as plt
%matplotlib inline
import gzip
import numpy as np
from keras.utils import to_categorical
from keras.models import Sequential
from keras.layers import Dense, Conv2D, Flatten
from keras.datasets import fashion_mnist
from numpy import mean, std
import matplotlib.pyplot as plt
from sklearn.model_selection import KFold, StratifiedKFold
from keras.datasets import fashion_mnist
from keras.utils import to_categorical
from keras.layers import Conv2D, MaxPooling2D, Dense, Flatten
from keras.optimizers import SGD, Adam
from keras.wrappers.scikit_learn import KerasClassifier
from sklearn.metrics import accuracy_score, confusion_matrix, classification_report
from sklearn.metrics import precision_score
from sklearn.metrics import recall_score
from sklearn.metrics import f1_score
from sklearn.metrics import auc, average_precision_score, f1_score
import time
from keras.callbacks import EarlyStopping
from keras.callbacks import ModelCheckpoint
from sklearn.model_selection import train_test_split
from sklearn.metrics import confusion_matrix, classification_report
from sklearn.model_selection import GridSearchCV, RandomizedSearchCV
from google.colab import files
from PIL import Image
def model_cnn():
model = Sequential()
model.add(Conv2D(32, kernel_size=(3,3), kernel_initializer=initializers.glorot_uniform(seed=1), input_shape=(28,28,1)))
model.add(BatchNormalization())
model.add(Activation('relu'))
model.add(Conv2D(32, kernel_size=(3,3), kernel_initializer=initializers.glorot_uniform(seed=2)))
model.add(BatchNormalization())
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Dropout(0.25, seed=1))
model.add(Flatten())
model.add(Dense(512, kernel_initializer=initializers.glorot_uniform(seed=2)))
model.add(BatchNormalization())
model.add(Activation('relu'))
model.add(Dropout(0.5, seed=1))
model.add(Dense(10, kernel_initializer=initializers.glorot_uniform(seed=2)))
model.add(Activation('softmax'))
model.compile(loss="categorical_crossentropy", optimizer=Adam(lr=0.001), metrics=['accuracy'])
return model
def train_model_with_EarlyStopping(model, X, y):
X_tr, X_val, y_tr, y_val = split_train_val_data(X,y)
es = EarlyStopping(monitor='val_loss', patience=20, mode='min', restore_best_weights=True)
history = model.fit(X_tr, y_tr,
batch_size=64,
epochs=200,
verbose=1,
validation_data=(X_val,y_val),
callbacks=[es])
evaluate_model(model, history, X_tr, y_tr)
return history
```
【问题讨论】:
您得到的结果是显着不同还是不完全相等?我已经看到某些 GPU 操作并不总是完全可重现的(例如,大张量的总和减少的细微差异),这应该是为了更好的性能而进行的权衡。有一些关于这类事情的归档问题,例如this。不过,我不确定这种情况还会在多大程度上发生。 我的测试精度为 0.8905、0.8796、0.8849... 问题还在于我在不同时期获得了最高的精度。所以 EarlyStopping 在 epoch 15、29、30 停止... 查看您的完整代码会有所帮助,包括具有正确形状的输入数据(可以随机生成) 我已经附上了之前代码sn-ps下的完整代码 记得用“@”的名字提及用户,以通知他们您的回复 - 自己检查这个问题。 【参考方案1】:问题不仅限于 Colab,而且可以在本地重现。然而,这种行为可能是不可避免的。
底部的代码是您的代码的最小可重现版本,调整了适合参数以加快测试速度。我观察到的是,在 5 次运行中,每次运行 468 次迭代的最大损失差异仅为 0.0144%。这很不错。使用batch_size=64
、60000
样本和20
epoch,您将有 18750 次迭代 - 这将大大放大这个数字。
无论如何,GPU 并行性是驱动随机性的最有可能的罪魁祸首 - 随着时间的推移,微小的差异 确实 会累积产生很大的差异 - 下面的演示。如果1e-8
看起来很小,请尝试将随机噪声添加到一半的权重中,幅度为1e-8
,并见证其生活理念的变化。
如果你不使用它们,种子的作用会变得非常明显——试试吧,你的所有指标都会在前 10 次迭代中飞速发展。此外,loss 更适合测量运行时差异,因为准确度对数值精度误差更敏感:10 个样本批次的 60% 准确度和 70% 准确度之间的差异是一个预测,相差 @ 987654328@wrt 0.5
- 但损失几乎不会让步。
最后,请注意,您的超参数选择对模型性能的影响远大于随机性;不管你扔多少种子,它们都不会将模型变成 SOTA。 -- 我推荐这个fine clip。
您的代码 - 没问题。您已采取所有实际步骤来确保可重复性,但有一个例外:PYTHONHASHSEED
必须在您的 Python 内核启动之前设置。
你能做些什么来减少随机性?
重复运行,平均结果。可以理解,这很昂贵,但请注意,即使是完全可重现的运行也不是完全信息丰富,因为 模型方差 w.r.t.训练和验证集可能远大于噪声引起的随机性
K-Fold 交叉验证:可以显着降低数据和噪声方差
更大的验证集:由于噪声,提取的特征可能只有这么多不同;验证集越大,衡量指标中反映的权重扰动就越小
GPU 并行性:放大浮点错误
print(2. * 11. / 9.) # 2.4444444444444446
print(2. / 9. * 11.) # 2.444444444444444
操作顺序很重要,通过利用多线程,GPU 并行性不能保证操作以相同的顺序执行。乍一看,差异可能看起来很简单 - 但要给它足够的迭代......
one = 1
for _ in range(int(1e8)):
one *= (2. / 9. * 11.) / (2. * 11. / 9.)
print(one) # 0.9999999777955395
print(1 - one) # 1.8167285897874308e-08
... 而一个“一”是一个典型的小权重值1e-08
,远离它的原始自我。如果 1 亿次迭代似乎是一个延伸,请考虑该操作在大约半分钟内完成,而您的模型可以训练一个多小时,而前者完全在 CPU 上运行。
最少的可重复实验:
import tensorflow as tf
import random as rn
import numpy as np
np.random.seed(1)
rn.seed(2)
tf.set_random_seed(3)
from keras.models import Sequential
from keras.layers import Dense, Dropout, Activation, Flatten, BatchNormalization
from keras.layers import MaxPooling2D, Conv2D
from keras.optimizers import Adam
def model_cnn():
model = Sequential()
model.add(Conv2D(32, kernel_size=(3,3),
kernel_initializer='he_uniform', input_shape=(28,28,1)))
model.add(BatchNormalization())
model.add(Activation('relu'))
model.add(Conv2D(32, kernel_size=(3,3), kernel_initializer='he_uniform'))
model.add(BatchNormalization())
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Dropout(0.25))
model.add(Flatten())
model.add(Dense(512, kernel_initializer='he_uniform'))
model.add(BatchNormalization())
model.add(Activation('relu'))
model.add(Dropout(0.5))
model.add(Dense(10, kernel_initializer='he_uniform'))
model.add(Activation('softmax'))
model.compile(loss="categorical_crossentropy", optimizer=Adam(lr=0.001),
metrics=['accuracy'])
return model
np.random.seed(1)
rn.seed(2)
tf.set_random_seed(3)
X_train = np.random.randn(30000, 28, 28, 1)
y_train = np.random.randint(0, 2, (30000, 10))
X_val = np.random.randn(30000, 28, 28, 1)
y_val = np.random.randint(0, 2, (30000, 10))
model = model_cnn()
np.random.seed(1)
rn.seed(2)
tf.set_random_seed(3)
history = model.fit(X_train, y_train, batch_size=64,shuffle=True,
epochs=1, verbose=1, validation_data=(X_val,y_val))
运行差异:
loss: 12.5044 - acc: 0.0971 - val_loss: 11.5389 - val_acc: 0.1051
loss: 12.5047 - acc: 0.0958 - val_loss: 11.5369 - val_acc: 0.1018
loss: 12.5055 - acc: 0.0955 - val_loss: 11.5382 - val_acc: 0.0980
loss: 12.5042 - acc: 0.0961 - val_loss: 11.5382 - val_acc: 0.1179
loss: 12.5062 - acc: 0.0960 - val_loss: 11.5366 - val_acc: 0.1082
【讨论】:
如果我理解正确,那么最好不要使用种子并多次运行模型?实际上,我想将这个模型架构与其他 5 种模型架构进行比较。每个架构我应该运行多少次?假设我不使用 EarlyStopping 并且我使用 Fashion-MNIST 数据集,它有一个包含 10000 张图像的测试数据集,那么我可以在每次运行后简单地使用测试数据集进行测试,而不是进行 K 折交叉验证,因为测试数据集应该提供足够的方差吗? (很抱歉直接提出问题,但我是深度学习的初学者) @CodeNow 没问题 - see here。我已经在相当复杂的数据上训练了相当复杂的模型,我可以告诉你的是 - 随机种子在 GPU 上并不完美,但它们绝对足够好。对于您的申请,我会 (1) 选择 X(请参阅链接); (2) 为 X 个 epoch 运行每个超参数配置; (3) 选择具有最佳超参数的模型。放弃测试集,与 val 集合并 - 然后,为了估计测试性能,在您的最终模型上运行 K-fold CV。 @CodeNow 另外,尝试编写自己的提前停止代码(例如回调),因为 loss 并不总是最好的指标(尤其是对于不平衡的数据分类)。类似于if f1_score > best_f1_score: model.save()
,并在模型名称中包含最佳指标(例如'model' + str(best_f1_score) + '.h5'
)以便于比较。理想情况下,在模型名称中包含关键超参数及其值以便于比较
\w@OverLordGoldDragon 如果我只是通过 GridSearchCV 为每个模型架构优化超参数,如学习率、时期数等(所以没有提前停止),会有什么问题。我总是确保为每个模型完成相同的 KFoldSplits(通过使用相同的 np.random.seed)。之后,我使用完整的训练数据集分别使用找到的最佳超参数运行每个模型(不设置任何种子)多次,并使用完整的测试数据集进行测试。然后为了比较我对每个模型的这些结果进行平均并计算标准偏差?
@CodeNow 这使问题的范围大大超出了原始范围,但我会简短地回答:没有任何“错误”,但我建议改用我描述的方案。原因是,提前停止是“免费午餐”,从某种意义上说,在 X
之后提取的特征是最好的,epochs 和 X
会因您的 K-Fold 拆分而有所不同 - 但您不是通过改变X
来牺牲“控制”值,因为这样的控制是多余的。如果您将提前停止的模型与固定的X
模型进行比较,您的最终模型可能会好得多(但您确实在每个折叠中都需要相同的种子)以上是关于为啥我的结果仍然无法重现?的主要内容,如果未能解决你的问题,请参考以下文章
为啥 connect() 会给出 EADDRNOTAVAIL?