最近学习了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
类构造网络的层结构,只需要传入layers
,eta
,eps
三个参数即可。所有的计算迭代细节,均在Vector
类中。而涉及到模型损失的相关函数则均在Architecture
类中进行构造(init_loss
与update_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)进行下述操作:
- 将
Architecture
类的实例化对象,通过Vector
类中的load_arch
函数传入。self
表示传入示例整体的信息,b
表示传入对应的层; - 初始化损失函数,构建一个loss的字典,里面包括三部分损失:
loss_total
,loss_expd
,loss_comp
; - 对当前空间中的样本总体进行预处理(标准化);
- 对当前空间进行循环迭代变换;
- 对变换后空间中的样本总体进行后处理(标准化);
每一个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
对每一层,我们需要做两件事:
- 根据计算的梯度,进行一步前向算法(对整体空间的膨胀,与类内空间的压缩);
- 计算并更新损失。
前向算法的计算公式如下:
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")
至此,一个完整的前向网络框架就构建出来了。