ReduNet 代码解读【Numpy 版本】

最近学习了ReduNet里面代码的框架,作为一个经常使用R的童鞋来说受益匪浅。本篇博客主要来介绍一下里面的代码的结构。

这里我们主要针对用Numpy库构建的网络结构与代码逻辑进行学习与分析。下面对一个Iris数据的demo进行分析。


首先是通过parser进行传参,在使用下述命令运行代码时,可以将参数纳入到代码中。

python3 iris.py --layers 4000 --eta 0.1 --eps 0.1

iris.py文件中传参部分如下所示。参数包括layers(网络层数), eta(超参数,学习率), eps(超参数,误差项平方), tail(文件夹名的额外信息), save_dir(储存路径)。下面为代码主体的说明:

# hyperparameters
parser = argparse.ArgumentParser()
parser.add_argument('--layers', type=int, default=20, help="number of layers")
parser.add_argument('--eta', type=float, default=0.5, help='learning rate')
parser.add_argument('--eps', type=float, default=0.1, help='eps squared')
parser.add_argument('--tail', type=str, default='',
                    help='extra information to add to folder name')
parser.add_argument('--save_dir', type=str, default='./saved_models/',
                    help='base directory for saving PyTorch model. (default: ./saved_models/)')
args = parser.parse_args()

而后将目录与文件名进行合并,作为输出文件储存路径。save_params表示是将函数的输入参数储存至一个json文件中。

# pipeline setup
model_dir = os.path.join(args.save_dir, "iris", "layers{}_eps{}_eta{}"
                         "".format(args.layers, args.eps, args.eta))
os.makedirs(model_dir, exist_ok=True)
utils.save_params(model_dir, vars(args))

下面载入Iris数据,并将训练集与测试集按照7:3的比例,进行设置。

# data setup
X_train, y_train, X_test, y_test, num_classes = dataset.load_Iris(0.3)

load_Iris函数具体构造如下,主要需注意的是在读入数据后,进行了normalize标准化处理:

# data setup
def load_Iris(test_size=0.3, seed=42):
    X, y = load_iris(return_X_y=True)
    X_train, X_test, y_train, y_test = train_test_split(X, y, shuffle=True,
                                                        test_size=test_size,
                                                        random_state=seed)
    X_train = F.normalize(X_train)
    X_test = F.normalize(X_test)
    num_classes = 3
    return X_train, y_train, X_test, y_test, num_classes

下面是最关键的模型设置部分:首先是通过搭建Vector类构造网络的层结构,只需要传入layersetaeps三个参数即可。所有的计算迭代细节,均在Vector类中。而涉及到模型损失的相关函数则均在Architecture类中进行构造(init_lossupdate_loss)。

# model setup
layers = [Vector(args.layers, eta=args.eta, eps=args.eps)]
model = Architecture(layers, model_dir, num_classes)

这里不再进行全部Vertor类中函数的展示,只展示部分关键的函数:

class Vector:
    def __init__(self, layers, eta, eps, lmbda=500):
        self.layers = layers
        self.eta = eta
        self.eps = eps
        self.lmbda = lmbda

    def __call__(self, Z, y=None):
        for layer in range(self.layers):
            Z, y_approx = self.forward(layer, Z, y)
            self.arch.update_loss(layer, *self.compute_loss(Z, y_approx))
        return Z

Vertor类中最重要的是初始化函数__init__,以及__call__函数。__init__相当于一但调用类时,首先进行的初始化操作;而__call__函数则相当于重载了括号运算符,这个类型就成为了可调用的(可以把这个类型的对象当作函数来使用)。下面我们用Architecture类中的__call__函数进行举例。

首先来看类似的Architecture类中的这两个函数,也是同样的作用:

class Architecture:
    def __init__(self, blocks, model_dir, num_classes, batch_size=100):
        self.blocks = blocks
        self.model_dir = model_dir
        self.num_classes = num_classes
        self.batch_size = batch_size

        
    def __call__(self, Z, y=None):
        for b, block in enumerate(self.blocks):
            block.load_arch(self, b)
            self.init_loss()

            Z = block.preprocess(Z)
            Z = block(Z, y)
            Z = block.postprocess(Z)
        return Z

    def __getitem__(self, i):
        return self.blocks[i]

可以看到通过下述语句:

model = Architecture(layers, model_dir, num_classes)

Architecture类实例化成model后,我们的model对象就可以成为一个函数,直接使用:

Z_train = model(X_train, y_train)

将参数传到示例中的__call__中进行调用,可以看到就对每个block(其实是前面的Vertor类构造的layers实例的每个元素,其实针对Iris数据集而言就是一共只有一个block)进行下述操作:

  1. Architecture类的实例化对象,通过Vector类中的load_arch函数传入。self表示传入示例整体的信息,b表示传入对应的层;
  2. 初始化损失函数,构建一个loss的字典,里面包括三部分损失:loss_totalloss_expdloss_comp
  3. 对当前空间中的样本总体进行预处理(标准化);
  4. 对当前空间进行循环迭代变换;
  5. 对变换后空间中的样本总体进行后处理(标准化);
    每一个block重复上述步骤,直到跳出循环。

注意到这里引入了一个新的__getitem__方法。在类内构造了此函数后,它的实例对象(假定为p),可以像 p[i] 取值,当实例对象做 p[i] 运算时,会调用类中的方法__getitem__。这里就是输出对应层的信息。

下面我们再看最核心的第四步过程:

Z = block(Z, y)

这里的block其实是Vector类的一个实例,因此block(Z, y)就相当于调用Vector类的__call__函数。

def __call__(self, Z, y=None):
    for layer in range(self.layers):
        Z, y_approx = self.forward(layer, Z, y)
        self.arch.update_loss(layer, *self.compute_loss(Z, y_approx))
    return Z

对每一层,我们需要做两件事:

  1. 根据计算的梯度,进行一步前向算法(对整体空间的膨胀,与类内空间的压缩);
  2. 计算并更新损失。

前向算法的计算公式如下:

def forward(self, layer, Z, y=None):
    if y is not None:
        self.init(Z, y)
        self.save_weights(layer)
        self.save_gam(layer)
    else:
        self.load_weights(layer)
        self.load_gam(layer)
    expd = Z @ self.E.T
    comp = np.stack([Z @ C.T for C in self.Cs])
    clus, y_approx = self.nonlinear(comp)
    Z = Z + self.eta * (expd - clus)
    Z = F.normalize(Z)
    return Z, y_approx

其与论文中的迭代过程完全一致,这里不进行细述:

而后代码分别对训练集训练权重,以及用训练好的权重对测试集进行测试,最后再将对应的loss函数进行储存。

# train/test pass
print("Forward pass - train features")
Z_train = model(X_train, y_train)
utils.save_loss(model.loss_dict, model_dir, "train")
print("Forward pass - test features")
Z_test = model(X_test)
utils.save_loss(model.loss_dict, model_dir, "test")

至此,一个完整的前向网络框架就构建出来了。


参考

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值