【深度学习】自动炼丹炉

在玩深度学习的时候,了解到超参数对于模型的训练效果有重要的影响。优化器不同的batchsize能够影响训练速度,同时也影响训练的损失值和准确率;不同的学习率对于模型的收敛速度也有影响。因此,对于指定数据集,能够准确预测的模型往往是模型工程师大量调整训练超参数的成果。这个调整超参数的过程,俗称炼丹,因为在训练之前往往很难得知训练出来的模型究竟能提供什么样的性能。
然而,随着深度学习理论的不断发展,模型能够调整的超参数越来越多。虽然这有助于人们训练出更好的模型,但调参的成本也大大增加。更高效率的调参过程自然要用到编程、日志等技能和工具,而不是人工守在电脑前等待每一次训练结果。

嵌套for循环:丑陋的倒立金字塔

基于嵌套for循环,写出了如下的自动调参程序:``

k_toUse = [3]
numEpochs_toUse = [50]
lr_toUse = [0.001]
weightDecay_toUse = [0.1]
batchSize_toUse = [16]
dropOutRate_toUse = np.linspace(0, 1, 10).tolist()
activations_toUse = [nn.Sigmoid, nn.ReLU, nn.Tanh, nn.LeakyReLU]
netType_toUse = ["multi", "self-defined", "single"]
for k in k_toUse:
    for num_epoch in numEpochs_toUse:
        for lr in lr_toUse:
            for weight_decay in weightDecay_toUse:
                for batch_size in batchSize_toUse:
                    for dropout_rate in dropOutRate_toUse:
                        for activation in activations_toUse:
                            for netType in netType_toUse:
                            	# 构建网络、训练模型、测试

这段代码块能够读取各个超参数列表中的待选数值,将所有可能组合,供模型使用。然而,按照python的语法,如果调整的超参数越多,缩进越多,代码块的列数呈线性增长,最后可读性大大降低,可维护性也变差,变成一个丑陋的倒立金字塔。

方法整合:眼不见,心不烦

但是没什么用
要解决这个问题,我首先使用了”眼不见,心不烦“的方法,将整个代码块整合成方法,返回超参数列表。

def get_parameters():
    """调参部分"""
    k_toUse = [3]
    numEpochs_toUse = [50]
    lr_toUse = [0.001]
    weightDecay_toUse = [0.1]
    batchSize_toUse = [16]
    dropOutRate_toUse = np.linspace(0, 1, 10).tolist()
    activations_toUse = [nn.Sigmoid, nn.ReLU, nn.Tanh, nn.LeakyReLU]
    netType_toUse = ["multi", "self-defined", "single"]
    parameters_lists = []
    for k in k_toUse:
        for num_epoch in numEpochs_toUse:
            for lr in lr_toUse:
                for weight_decay in weightDecay_toUse:
                    for batch_size in batchSize_toUse:
                        for dropout_rate in dropOutRate_toUse:
                            for activation in activations_toUse:
                                for netType in netType_toUse:
                                    parameters_lists += [
                                        [k, num_epoch, lr, weight_decay, batch_size, dropout_rate, activation, netType]
                                    ]
    return parameters_lists

训练部分的网络主函数只需要按照如下方法使用即可:

for parameters in get_parameters():
    k, num_epochs, lr, weight_decay, batch_size, dropout_rate, activation, netType = parameters

使用这种方法虽然让主函数看起来美观了一些,但是修改的地方变多了。于是我想到,能不能写一个方法,自动生成多列表元素的排列,同时避免过多的缩进。

自动生成排列:Reduce()方法

很多博客使用的方法都用到了python的functools模块的reduce()方法,源码如下:

def reduce(function, sequence, initial=_initial_missing):
    """
    reduce(function, iterable[, initial]) -> value

    Apply a function of two arguments cumulatively to the items of a sequence
    or iterable, from left to right, so as to reduce the iterable to a single
    value.  For example, reduce(lambda x, y: x+y, [1, 2, 3, 4, 5]) calculates
    ((((1+2)+3)+4)+5).  If initial is present, it is placed before the items
    of the iterable in the calculation, and serves as a default when the
    iterable is empty.
    """

    it = iter(sequence)

    if initial is _initial_missing:
        try:
            value = next(it)
        except StopIteration:
            raise TypeError(
                "reduce() of empty iterable with no initial value") from None
    else:
        value = initial

    for element in it:
        value = function(value, element)

    return value

try:
    from _functools import reduce
except ImportError:
    pass

翻译一下就是:

def reduce(function, sequence, initial=_initial_missing):
    """
    按照自左向右的顺序,对一个序列或者可迭代对象中的元素调用方法,从而将可迭代对象转化为一单值。
    如果initial值存在,则在计算过程中会将其放置在元素之前,若可迭代对象为空,则将其作为默认值。
    """

@xue123__的这篇博客就是使用了reduce()方法实现了多列表的排列组合,稍微改一下就能输出不同取值排列的列表(e.g. [[‘A’, ‘C’, ‘F’], [‘A’, ‘C’, ‘G’], [‘A’,‘C’,‘H’], …]),而不是从多列表中取值运算的结果列表。

精益求精:使用生成器节省内存

为了节省内存,我决定设计一个用生成器返回结果的方法。参考python中对于可迭代对象排列的实现方法,递归实现似乎是个不错的选择。

def permutation(res, *args):
    """
    生成超参数列表。
    :param res: 结果列表。每个输出的列表都以`res`为前缀。
    :param args: 超参数列表。每个超参数输入均为列表,列表中的值为该超参数可能取值
    :return: 超参数取值列表生成器。
    """
    if len(args) == 0:
        yield res
    elif len(args) == 1:
        for arg in args[0]:
            yield res + [arg]
    else:
        for arg in args[0]:
            for p in permutation(res + [arg], *args[1:]):  # 不能直接yield本函数,否则不会执行
                yield p

这段只使用python内置模块的方法,每次从下一个备选超参数列表中取出一个值放入结果列表,以备选超参数长度为0、1为出口,返回一个可用超参数列表。这样,主函数内只用写for语句、待调整超参数的备选值列表就可以实现自动调参了:

base_s = [2]
epochs_es = [300]
batch_sizes = [8, 16, 32, 64, 128]
loss_es = ['entro']
lr_s = [1e2, 1e1, 1e0, 1e-1, 1e-2, 1e-3]
optim_str_s = ['adam']
w_decay_s = [0.]

for base, epochs, batch_size, loss, lr, optim_str, w_decay in permutation(
        [], base_s, epochs_es, batch_sizes, loss_es, lr_s, optim_str_s, w_decay_s
):

虽然省去了添加新待调整超参数时,需要在排列方法内部进行的改动,但是主函数的for语句看起来还是太长了……

配置文件调参:少写一些东西

于是我想到了用.json文件来进行参数的配置,只需编写一个符合规定格式的.json文件,列出所有待调整超参数作为key,value值为其调整备选值:

test_hyper_params.json文件内部

{
	"datasets": [
		"FashionMNIST"
	],
	"base_s": [
		2
	],
	"epochs_es": [
		20,
		30
	],
	"batch_sizes": [
		256
	],
	"loss_es": [
		"entro"
	],
	"lr_s": [
		0.01
	],
	"optim_str_s": [
		"sgd"
	],
	"w_decay_s": [
		0.0
	]
}

于是使用python的json模块,就可以实现自动读取所有待调整超参数的备选值,输入到方法中,生成超参数列表:

config = open('test_hyper_params.json', 'r', encoding='utf-8')
args = json.load(config)

for dataset, base, epochs, batch_size, loss, lr, optim_str, w_decay in permutation([], *args.values()):

而这样主函数写的调用代码就看上去美观多了。每次添加新的待调整超参数,主函数只需要加个变量名,.json文件中只需要按照字典格式添加一个列表即可。不过这样增加了维护超参数文件的成本,而且超参数的变量名还是需要手动添加,似乎还有优化的空间……?

白努力:itertools模块内置笛卡尔积方法

写文章的时候才看到,itertools模块的product()方法就可以实现我的需求……我的需求总结成数学运算,就是求多个列表的笛卡尔积,那么这样有定义的数学运算应该就有现成的方法的,我咋没想到……于是自己写的permutation()函数可以丢掉,主函数调用代码就可以写成这样了:

import itertools

config = open('test_hyper_params.json', 'r', encoding='utf-8')
args = json.load(config)

res = itertools.product(*args.values(), repeat=1)
for dataset, base, epochs, batch_size, loss, lr, optim_str, w_decay in res:
想想,在这个探究的过程中,我至少学到了怎么用python的yield语句构造生成器,还是有点收获的……(试图安慰自己)

你可能感兴趣的:(深度学习,人工智能,python)