神经网络变得轻松(第二十三部分):构建迁移学习工具

我们继续沉浸在人工智能世界之中。 今天,我邀请您来掌握迁移学习技术。 赫兹期货量化曾在各种文章中提到过这项技术,但从未用到过。 同时,这是一个强力的工具,可以提高开发神经网络的效率,并降低训练它们的成本。

1. 迁移学习的目的

什么是迁移学习,为什么我们需要它? 迁移学习是一种机器学习方法,其用来为解决一个问题而采用训练的模型知识,可被重用作解决新问题的基础。 当然,为了解决新问题,模型需在新数据上进行了初步的额外训练。 在一般情况下,采用正确选择的供体模型,与从头开始训练类似的模型相比,额外的训练运行得更快,结果更好。

可以采用完整的供体模型或其中的一部分。

与这项技术类似的情况是,赫兹期货量化曾用聚类和数据压缩结果来预处理神经网络的源数据。 在这种情况下,我们用到了整个预训练模型。 但在建立解决新问题的模型时,我们没有对供体模型进行额外的培训。 我们仅用它来预处理“未加工”源数据,并基于此数据训练一个新模型。

当赫兹期货量化开始研究自动编码器时,我们还讨论过在模型训练后再使用迁移学习的可能性。 但在这种情况下,我们不能完全使用自动编码器作为完整的供体模型,因为我们训练它的目的是压缩原始数据,然后再从压缩态中恢复它。 因此,完全使用自动编码器作为供体模型毫无意义。 对于数据预处理,仅把它当作编码器来用会更有效。 在这种情况下,整体模型将更小,并且更深层数的效率将更高,因为处理相同的信息量所需的可训练权重更少。

但迁移学习的运用不只限于无监督学习的结果。 回想一下,当您需要添加或删除一个神经层时,您曾多少次重新开始训练您的模型。 在这种情况下,有部分神经层其实可以重用。

这项技术还有另一个应用领域。 由于梯度衰落问题,近乎不可能完全训练深度模型。 运用迁移学习则允许模块化训练神经层,并逐渐增加模型的规模。

当然,这项技术还有许多其它可能的用途,您可自行探索。 现在,赫兹期货量化继续研究一款允许它所用它的工具。

2. 创建一款工具

我们先要决定我们将创建工具的用途。 首先,我们回到如何保存经过训练的模型。 所有这些都保存在一个二进制文件当中。 每个模型对象都有自己的严格数据记录结构。 因此,很难简单地从编辑器中的文件里删除部分数据。 故此,赫兹期货量化要先从文件中加载整个训练模型,执行必要的复原操作,并将新模型保存到新文件中,或覆盖以前的旧模型。 新文件更胜一筹,因为供体模型能够更深入地解决训练目标针对的问题。

此外,赫兹期货量化的神经网络只能与训练它们的数据一起配套操作。 对于全新的数据,结果则是不可预测的。 这也适用于单神经元层。 因此,对于迁移学习,我们只能采用连续神经层,从输入数据层开始。 您不能从模型的中间或末端抽取模块。 也就是说,我们可以使用整个供体模型或其最开头的若干层。 然后我们向其添加若干个不同的神经层,并保存新模型。

同时,赫兹期货量化需要确保新模型在训练模式和操作中的全部功能。 当然,必须首先训练模型。

以下几点需要注意。 来自供体模型的神经层会保留其权重。 它们还保留了在模型预训练阶段获得的所有知识。 新的神经层将接收随机权重,就像模型初始化阶段一样。 如果我们如同往常一样开始训练一个新模型,那么随着训练新神经层,赫兹期货量化先前训练过的神经层就会失衡。 因此,我们必须首先阻止供体模型神经层的训练。 通过这种方式,我们确保只需训练新神经层。

2.1 设计

我们不仅需要用到源供体模型的程序。 我们还需要以某种方式进行处理,并将其重新保存到新文件当中。 复制层的数量,以及模型体系结构始终是独立的。 因此,我们需要一个工具,允许用户快速便捷地单独配置每个模型。 即,我们的工具需要一个便捷的用户界面。 那么,我们就从 UI 设计开始吧。

如以,我看到了三个清晰的模块。 在第一个模块中,赫兹期货量化将操控供体模型。 在此,我们要能够选择含有已训练模型的文件。 从文件加载模型后,该工具必须提供已加载模型的体系结构的说明。 这是因为用户应当了解加载了哪个模型,以及将要复制哪些神经层。 我们还要通知工具有关复制图层的数量。 如上所述,我们将从源数据层开始按顺序复制神经层。

在第二个模块当中,将添加神经层。 在此,我们将创建字段,用于输入有关正在创建的神经层的信息。 与程序代码一样,我们将按顺序逐个定义每个神经层,并将其添加到新模型的架构之中。

第三个模块将显示所创建模型的整体架构,并能够指定一个文件来保存它。 下面呈现该工具的设计示例。

编辑切换为居中

添加图片注释,不超过 140 字(可选)

该工具的设计及其实现,两者均仅用于演示目的。 您始终能修改它们来更好地满足您的需求。

2.2 实现用户界面

现在我们就可以继续实现设计了。 为此,我们创建一个新类 CNetCreatorPanel,它应继承自 CAppDialog 对话框应用程序基类。

面板中的每个控件都将作为单独的对象创建。 因此,我们要在新的类中声明相当多的对象。 为方便起见,我们将它们划分为多个模块。

第一个模块包含的对象与预训练模型的可视化相关:

  • m_edPTModel — 指定预训练模型文件名的元素

  • m_edPTModelLayers — 显示预训练模型中神经层的总数

  • m_spPTModelLayers — 将复制到新模型的神经层数量

  • m_lstPTMode — display of the architecture of the pre-trained model

 
 

class CNetCreatorPanel : protected CAppDialog { protected: //--- pre-trained model CEdit m_edPTModel; CEdit m_edPTModelLayers; CSpinEdit m_spPTModelLayers; CListView m_lstPTModel; CNetModify m_Model; CArrayObj* m_arPTModelDescription;

此外,赫兹期货量化将在这里声明要与预训练模型配套使用的对象:

  • m_Model — 预训练模型的对象

  • m_arPTModelDescription — 一个动态数组,描述预训练模型的架构

注意以下两个时刻。 所有对象都声明为静态,但模型体系结构描述的动态数组除外。 使用静态对象可以将内存操作传输到系统。 这是因为静态对象是与包含它们的对象一起创建和删除的,不需要程序员任何额外的操作。 但这种方式,只能在我们类的结构中创建对象。 架构的描述将从预训练的模型中获取。 因此,此对象是通过动态指针声明的。

而第二个时刻。 是为了声明预训练的模型对象,我们采用的是 CNetModify 类。 但之前我们为神经网络模型创建了 CNet 类。 这是因为我们需要来自神经网络的附加功能。 为了实现它,我们将创建一个派生自 CNet 类的新类 CNetModify。 但是在描述工具功能时,我们将回到这一部分。

下一个模块包含的对象,用于描述正在创建的新神经层。 这些对象与描述神经层体系结构的 CLayerDescription 类的元素并排。 这就是为什么我们不会详细审查每个元素的原因。 但我想提一下创建两个按钮,它们是添加新的神经层和删除已创建的神经层。 只能删除已加入的神经层。 为了控制需复制的神经层数量,我们要用到前一个模块的元素。

 
 

//--- add layers CComboBox m_cbNewNeuronType; CEdit m_edCount; CEdit m_edWindow; CEdit m_edWindowOut; CEdit m_edStep; CEdit m_edLayers; CEdit m_edBatch; CEdit m_edProbability; CComboBox m_cbActivation; CComboBox m_cbOptimization; CButton m_btAddLayer; CButton m_btDeleteLayer;

新模型的最后一个对象模块仅包含 3 个元素。 这些是用于显示模型一般体系结构的对象,用于保存新模型的按钮,以及描述我们正在添加的神经层体系结构的动态数组。 在这种情况下,我们创建了一个动态数组的静态对象,描述被加入 m_arAddLayers 的神经层架构。 神经层的架构将在工具内部创建。 该对象也可创建为静态对象。

 
 

//--- new model CListView m_lstNewModel; CButton m_btSave; CArrayObj m_arAddLayers;

赫兹期货量化将使用一个类的公开方法的基础清单。 其中包括类构造函数和析构函数、面板创建方法和事件处理程序。

父类的三个方法已被重写。 这本来可以通过公开继承来避免。

 
 

public: CNetCreatorPanel(); ~CNetCreatorPanel(); //--- main application dialog creation and destroy virtual bool Create(const long chart, const string name, const int subwin, const int x1, const int y1); //--- chart event handler virtual bool OnEvent(const int id, const long &lparam, const double &dparam, const string &sparam); virtual void Destroy(const int reason = REASON_PROGRAM) override { CAppDialog::Destroy(reason); } bool Run(void) { return CAppDialog::Run();} void ChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { CAppDialog::ChartEvent(id, lparam, dparam, sparam); } };

因为我们使用了静态对象,故我们类的构造函数和析构函数实际上是空的。

与界面元素的创建和排列相关操作的主要部分在对话框窗口创建方法 Create 中实现。 但在我们继续方法描述之前,我们还要做些准备工作。

首先,我们需要定义常量的数量,来帮助我们正确规划界面的内部空间。 附件中提供了完整清单。

还应该注意的是,除了输入元素之外,我们的界面还包含了一定数量的文本标签。 但是我们还没有为它们声明对象。 这样做是为了简化的我们类结构。 我们需要它们仅仅是为了可视化,因此在我们的工具里,它们并不参与创建的功能。 但是,我们需要创建这些对象。 创建这类对象的过程将会重复,除某些数据之外。 这也许包括对象文本及其位置。 为了构造我们的代码,我们将创建一个单独的 CreateLabel 方法来创建这类标签。

在方法参数中,我们将传递在面板上的对象标识符、标签文本及其坐标。

在方法主体中,我们首先创建一个新的标签对象,并检查操作结果。 然后我们在图表上创建一个对象,向其传递必要的内容,并将创建的对象指针添加到含有界面对象集合的动态数组之中。

赫兹期货量化在私密变量中创建了一个含有指针的新对象。 在方法执行操作期间,检查每个操作的结果,如果出现错误,则删除已创建的对象。 但是退出该方法后,我们不会保留指向在类中所创建对象的指针,以便将来当程序关闭时能删除它。 这是因为我们将指向所创建对象的指针传递给对话框对象集合,完整功能已在父类中实现。 此功能包括在程序关闭时删除集合内所有对象。 那么,现在我们就可以将指针传递给集合,并忘记它。

 
 

bool CNetCreatorPanel::CreateLabel(const int id, const string text, const int x1, const int y1, const int x2, const int y2) { CLabel *tmp_label = new CLabel(); if(!tmp_label) return false; if(!tmp_label.Create(m_chart_id, StringFormat("%s%d", LABEL_NAME, id), m_subwin, x1, y1, x2, y2)) { delete tmp_label; return false; } if(!tmp_label.Text(text)) { delete tmp_label; return false; } if(!Add(tmp_label)) { delete tmp_label; return false; } //--- return true; }

类似地,我们将创建一个用于创建输入对象的方法。 但是,我们使用以前在类中创建的对象,替代创建新对象。 相关指针则在方法参数中传递。

 
 

bool CNetCreatorPanel::CreateEdit(const int id, CEdit& object, const int x1, const int y1, const int x2, const int y2, bool read_only) { if(!object.Create(m_chart_id, StringFormat("%s%d", EDIT_NAME, id), m_subwin, x1, y1, x2, y2)) return false; if(!object.TextAlign(ALIGN_RIGHT)) return false; if(!object.ReadOnly(read_only)) return false; if(!Add(object)) return false; //--- return true; }

此外,赫兹期货量化使用枚举和常量来描述所创建的神经层架构。 为了避免用户在这类元素中输入不正确的值,我们要创建特殊的控件。 用户只能从预定的列表中选择一个元素。 我们需要若干个这类元素。 我们首先创建一个元素来指示神经层的类型。 此功能将在 CreateComboBoxType 方法中实现。 由于此方法旨在创建特殊元素,因此我们不需要在参数中传递指向对象的指针。 在此,我们只需要指定正在创建的元素坐标。

在方法主体中,我们在图表上以指定坐标创建一个元素,并检查结果。

接下来,我们需要为元素填充文本描述和数字 ID 。 我们可用神经层的类型标识符作为 ID。 但是我们没有文字描述。 因此,要将数字标识符转换为文本描述,我们将创建一个单独的 LayerTypeToString 方法来完成这件事。 它的算法非常简单。 您可以在附件中查看它。 在此,我们仅针对每个神经层的类型调用此方法。

在方法末尾处,我们将对象指针添加到界面对象的集合当中。

请注意,赫兹期货量化将动态和静态对象两者都添加到集合之中。 这是因为集合功能更广泛,比程序完成后再删除对象的控制强得多。 与此同时,集合元素参与判定图表上对象的坐标,并处理事件。 指定集合的常规目的是将所有对象作为一个完整有机体运作。

 
 

bool CNetCreatorPanel::CreateComboBoxType(const int x1, const int y1, const int x2, const int y2) { if(!m_cbNewNeuronType.Create(m_chart_id, "cbNewNeuronType", m_subwin, x1, y1, x2, y2)) return false; if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronBaseOCL), defNeuronBaseOCL)) return false; if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronConvOCL), defNeuronConvOCL)) return false; if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronProofOCL), defNeuronProofOCL)) return false; if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronLSTMOCL), defNeuronLSTMOCL)) return false; if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronAttentionOCL), defNeuronAttentionOCL)) return false; if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronMHAttentionOCL), defNeuronMHAttentionOCL)) return false; if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronMLMHAttentionOCL), defNeuronMLMHAttentionOCL)) return false; if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronDropoutOCL), defNeuronDropoutOCL)) return false; if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronBatchNormOCL), defNeuronBatchNormOCL)) return false; if(!m_cbNewNeuronType.ItemAdd(LayerTypeToString(defNeuronVAEOCL), defNeuronVAEOCL)) return false; if(!Add(m_cbNewNeuronType)) return false; //--- return true; }

类似地,为激活函数和参数优化方法的枚举创建对象。 为了将枚举转换为文本形式,我们将调用标准的 EnumToString 函数。 因此,我们就能在循环里将元素添加到列表之中。 附件中提供了这些方法的完整代码。

至此准备工作完毕,赫兹期货量化可以继续创建用户界面了。 此功能是在 Create 方法中执行。 在参数中,我们只接收在图表上的面板右上角位置坐标。 然而,若要创建对象,我们还需要面板的尺寸。 为了便于操作和将来修改(如有必要),我通过预定义的常量来设置面板的尺寸。 该面板是由父类的类似方法创建的。 它在方法主体中是第一个被调用的。

 
 

bool CNetCreatorPanel::Create(const long chart, const string name, const int subwin, const int x1, const int y1) { if(!CAppDialog::Create(chart, name, subwin, x1, y1, x1 + PANEL_WIDTH, y1 + PANEL_HEIGHT)) return false;

接下来,将界面对象添加到所创建面板之中。 对象将从左上角开始按顺序添加。 每个新对象的坐标将与前一个对象的坐标相接。 这种方式令我们能够把对象构建成均匀的结构。

根据上述逻辑,赫兹期货量化开始创建预训练模型工作组对象。 第一个就是组标签。 若要创建它,确定标签的坐标,并调用之前创建的 CreateLabel 方法。 标签文本和坐标一并传递给该方法。 不要忘记添加独有的标签 ID。

 
 

int lx1 = INDENT_LEFT; int ly1 = INDENT_TOP; int lx2 = lx1 + LIST_WIDTH; int ly2 = ly1 + EDIT_HEIGHT; if(!CreateLabel(0, "PreTrained model", lx1, ly1, lx2, ly2)) return false;

接着,我们创建一个输入字段,用来选择含有预训练模型的文件名称。 为此,垂直平移所创建对象的坐标,并保持水平坐标不变。 因此,2 个对象将严格位于彼此上下方。

用户将无法手动指定文件名。 代之,我们会提示用户从现有文件中选择一个。 稍后我们将回到此动作的功能。 至于现在,我们把文件名称字段设置为只读。 调用之前创建的 CreateEdit 方法创建对象。 创建字段后,向其添加信息消息。

 
 

ly1 = ly2 + CONTROLS_GAP_Y; ly2 = ly1 + EDIT_HEIGHT; if(!CreateEdit(0, m_edPTModel, lx1, ly1, lx2, ly2, true)) return false; if(!m_edPTModel.Text("Select file")) return false;

以下,我们将指定已训练模型的神经字段数量。 为此,需为神经层数创建一个文本标签和一个输入字段(在本例中为输出)。 此字段也是只读的。

 
 

ly1 = ly2 + CONTROLS_GAP_Y; ly2 = ly1 + EDIT_HEIGHT; if(!CreateLabel(1, "Layers Total", lx1, ly1, lx1 + EDIT_WIDTH, ly2)) return false; //--- if(!CreateEdit(1, m_edPTModelLayers, lx2 - EDIT_WIDTH, ly1, lx2, ly2, true)) return false; if(!m_edPTModelLayers.Text("0")) return false;

类似地,为了输入要复制的神经层数,创建一个标签和字段。 赫兹期货量化需要在这里实现一种机制,限制用户选择神经层数。 它不得小于 0,或大于模型中的神经层总数。 这可利用 CSpinEdit 类对象的实例轻松完成。 该类允许我们指定有效值的范围。 其余的均已在类中实现。

 
 

ly1 = ly2 + CONTROLS_GAP_Y; ly2 = ly1 + EDIT_HEIGHT; if(!CreateLabel(2, "Transfer Layers", lx1, ly1, lx1 + EDIT_WIDTH, ly2)) return false; //--- if(!m_spPTModelLayers.Create(m_chart_id, "spPTMCopyLayers", m_subwin, lx2 - 100, ly1, lx2, ly2)) return false; m_spPTModelLayers.MinValue(0); m_spPTModelLayers.MaxValue(0); m_spPTModelLayers.Value(0); if(!Add(m_spPTModelLayers)) return false;

接下来,赫兹期货量化应该只显示一个包含预训练模型架构描述的窗口。 请注意,在此之前,我们总是将创建对象的坐标向下移动一级。 在这种情况下,我们只是从前一个对象的上边框移动到底部。 对象的下边框设置为从窗口高度开始的缩进。 因此,我们将对象拉伸到窗口的大小,并在创建的界面底部获得平滑的边缘。

 
 

lx1 = INDENT_LEFT; lx2 = lx1 + LIST_WIDTH; ly1 = ly2 + CONTROLS_GAP_Y; ly2 = ClientAreaHeight() - INDENT_BOTTOM; if(!m_lstPTModel.Create(m_chart_id, "lstPTModel", m_subwin, lx1, ly1, lx2, ly2)) return false; if(!m_lstPTModel.VScrolled(true)) return false; if(!Add(m_lstPTModel)) return false;

预训练模型模块的操作完成,并进入第二个描述所加神经层架构的对象模块。 模块对象也是从上至下创建的。 定义新对象的坐标时,我们平移水平坐标,并在距窗口顶部边缘缩进级别处定义上边框。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值