第12章 让对象像数值一样工作

本文详细介绍了自定义类中的隐式类型转换、友元函数以及模板成员函数的概念与应用。通过实例演示了如何在构造函数中避免隐式转换,以及如何使用友元函数访问私有成员。此外,还展示了模板成员函数的实现方式,并解释了重载操作符的重要性。文章最后以简化版字符串类为例,综合运用上述概念,强调了编码中遵循惯用法的重要性。

自定义类产生的隐式类型转换:当构造函数有且仅有一个未指定默认值的参数的时候,可能会产生参数类型到自定义类型的隐式转换。例如:

class Test{

    int v_;

public:

    Test(int i) : v_(i){}

    int GetV() const{return v_;}

};

 

void fun(const Test& test)

{

    std::cout<<test.GetV();

}

 

int main()

{

    fun(1);

    return 0;

}

这样的代码就产生了一个从整型数值1到自定义类型Test的隐式转换,当然这里例子没有实际意义。

当自定义类定义了隐式转换操作符的时候,也可能会产生自定义类型到其他类型的隐式转换,这里的其他类型就是隐式转换操作符的时候的类型。例如:

class Test{

    int v_;

public:

    Test(int i) : v_(i){}

    operator int() const{return v_;}

};

 

void fun(int i)

{

    std::cout<<i;

}

 

int main()

{

    fun(Test(1));

    return 0;

}

这样的代码就产生了一个从自定义类型Test到整型数值1的隐式转换,当然这里例子没有实际意义。

在实际编程中,隐式转换会给程序员带来很多困扰,因为是隐式的,所以往往发生在程序员不知情的情况下,更是难以调试。

为了防止这两种方式产生的隐式类型转换,我们可以在有且仅有一个未指定默认值的参数的构造函数前加上explicit关键字修饰,这个关键字的作用是让这个构造函数只能显示的调用,避免发生隐式转换。我们也尽量不去定义隐式转换操作符,除非不会给使用者带来任何困扰和问题。

 

友元函数:有些时候我们或许会希望授权某个外部函数能够访问我们的私有成员,但是其他未被授权的函数不能访问,这个时候就可以用到友元函数了。将希望授权的函数声明为自定义类的友元函数,那么它就能访问自定义类的私有成员了。例如:

class Test{

    int v_;

public:

    explicit Test(int i) : v_(i){}

    friend void fun(const Test&);

};

 

void fun(const Test& test)

{

    std::cout<<test.v_;

}

 

int main()

{

    fun(Test(1));

    return 0;

}

 

模板成员函数:自定义类的成员函数也可以是模板函数,这样类似于定义了一系列相同名字的成员函数。例如:

class Printer{

    std::ostream& o_;

public:

    explicit Printer(std::ostream& o) : o_(o){}

    template<typename T>

    void Print(const T& v)

    {

       o_<<v;

    }

};

 

int main()

{

    Printer printer(std::cout);

    printer.Print(1);

    return 0;

}

 

重载操作符:自定义类可以通过重载操作符来让自己可以通过操作符来完成一定的功能。例如:std::string就重载了+操作符,完成了字符串的拼接功能。

 

以上就是本章的一些概念,教材中通过实现一个简化版的字符串类Str,来对这几个概念分别讲解并指出了一些编码中应该遵守的惯用法和如何避免可能会带来的问题,课堂上将不再赘述。

Python 3 File Edit View Insert Cell Kernel Widgets 代码 手写字识别项目 问题描述 在日常生活中,我们经常需要处理各种形式的手写数字,比如填写表格、开具单据、手写笔记等。随着数字化时代的发展,人们对自动化处理手写数字数据的需求不断增加。许多现代应用程序和技术都需要利用手写数字识别技术来提高准确性和工作效率,以避免人为错误的发生,并促进信息共享和交流的便利性。例如,在金融领域进行支票处理,邮政和物流行业进行地址识别,以及图书馆和档案馆进行纸质文档的数字化等场景中,手写数字识别技术都发挥着重要作用。因此,本将深入研究手写数字识别技术,探索如何快速、准确地将手写数字转化为可识别的文本形式。 问题分析 数据集 本采用常用的Python机器学习库Scikit-learn内置的一个经典手写数字数据集(Digits Dataset)。该数据集最初由美国国家标准与技术研究所(NIST)收集和创建,共包括了1797个手写数字图像及各图像对应的标签值,具体样例如图所示。其中,每张图像均由8x8像素点的矩阵构成,标签范围为0-9。所有样本图像均经过预处理,使手写数字位于图像中心,并具有一定的标准化和一致性,以便于模型的训练。image.png 需求解的问题 需求解的问题:根据手写数字数据集(Digits Dataset)构建手写数字识别模型,以准确分类输入的手写体数字图片并输出相应的数字值。 区别于前几次实验的数据集,手写数字数据集具有图像数据的特性,其中的特征之间存在复杂的非线性关系。线性回归模型和逻辑回归模型等都无法有效地处理图像数据中的非线性特征,需要使用更加复杂的非线性分类模型,才能更好地捕捉图像数据中的特征,从而实现准确的分类预测。 因此,本次实验采用一种常用的人工神经网络—BP神经网络来学习已有数据集中的权值关系,从而对输入图片与其标签之间的关系进行近似求解。相较于传统的线性模型,BP神经网络利用多层的神经元和非线性激活函数捕获手写数字数据集中的复杂非线性特征,通过反向传播算法自动调整权值,从而不断优化模型的性能,具备更强大的非线性拟合能力和自动特征学习能力。 由于手写数字数据集的数据维数和数量并不是很大,因此只需要采用标准的三层BP神经网络进行求解。 神经网络基本原理 三层BP神经网络模型 BP(Back Propagation)神经网络由Rumelhart和McClelland等科学家于1986年提出,是一种通过误差逆传播算法进行训练的多层前馈网络,目前已在多领域广泛应用。 在处理各种问题时,BP神经网络将学习和存储大量的输入与输出模式之间的映射关系,而无需事先揭示描述这种映射关系的数学方程,因此具有更强的普适性和灵活性。同时,BP神经网络利用误差的反向传播不断调整网络的权值和阈值,使网络的误差最小,有利于提高手写数字识别的准确性。如图所示,三层BP神经网络模型包括一层输入层(Input Layer)、一层隐含层(Hide Layer)和一层输出层(Output Layer)。image.png BP神经网络算法包括两个主要步骤:前向传播和误差的反向传播。 在前向传播过程中,输入数据通过隐藏层作用于输出节点,从而形成网络的预测输出。如果实际输出与期望输出不一致,就需要进行误差的反向传播。 误差的反向传播是一种逐层将输出误差反向传递到输入层的过程。通过这个过程,误差会在各层的神经元之间进行分摊,从而获得各层神经元的误差信号。这个误差信号的大小和方向表示了每个神经元对整体误差的贡献程度。 我们可以用实际例子进行说明。当我们在学习一项分类任务时,我们首先尝试进行猜测,然后与实际结果进行比较。如果我们犯了错误,我们会回顾我们的猜测结果并尝试找出导致错误的方面。类似地,误差的反向传播就是神经网络进行自我纠正的过程。 通过不断调整权重,神经网络将进行学习和训练。这个过程被称为迭代优化,为了最小化网络输出与期望输出之间的误差。此过程会持续进行,直至满足终止条件。其中,终止条件可以是误差减少到可接受的程度,或是进行到预先设定的学习次数。 M-P神经元模型 M-P神经元模型是一种模拟生物神经元工作原理的数学模型,是神经网络的基本组成单位之一。它们模拟了生物神经元的基本功能,并且在网络中相互连接形成复杂的神经网络结构。M-P神经元会接收来自其他神经元的输入,经过加权求和和激活函数的处理后,产生输出信号,这个输出信号又可以传递到其他神经元。在神经网络中,M-P神经元通过这种方式对输入信号进行处理,从而实现信息的传递和处理。 我们可以用神经细胞与神经元进行比较。神经元是神经网络中的基本组成单位,就像我们身体中的神经细胞一样。它们通过相互连接来传递和处理信息,帮助神经网络感知和理解世界。而M-P神经元的工作原理也与其相似。 从组成上看,M-P神经元由输入信号、权重连接、激活函数和输出信号组成,如图所示。image.png 在这个模型中,神经元接收到来自其他n个神经元传递过来的输入信号,这些输入信号通过带权重的连接进行加权求和,然后将神经元接收到的总输入值将与神经元的阈值进行比较,之后利用激活函数对加权求和结果进行非线性转换,最终将处理后的信号发送给其他神经元。 我们假设第个神经元传过来的刺激为𝑥𝑖 ,权重为𝑤𝑖 ,神经元接收到的总输入值是所有神经元传过来的刺激按照权重累加的结果,即: 𝑠=∑𝑖=1𝑛𝑤𝑖𝑥𝑖 同样的,这个神经元也需要像其他神经元一样需要向外传播刺激信号。 常用的激活函数包括Sigmoid函数、Sgn函数和ReLU函数等。每个激活函数都具有独特的性质和用途。本次实验采用Sigmoid函数,它能将输出值映射到[0,1]的范围内。image-2.png 激活函数在神经网络中起着至关重要的作用,如果不使用激活函数时,每一层0的输出将仅仅是上一层输入的线性函数。这意味着无论神经网络有多少层,它只能进行简单的线性组合,激活函数可以理解为引入的非线性变换,它能够将输入信号进行非线性映射,从而将输入空间映射到更高维的特征空间,使得神经网络能够学习并捕获更丰富的数据特征。 前向传播 如图所示,三层的BP神经网络结构由一个输入层、一个隐藏层和一个输出层组成。其中,输入层有n个神经元,隐藏层有m个神经元,输出层有r个神经元。image.png 输入向量表示为𝑋=(𝑥1,𝑥2,...,𝑥𝑛) ;隐藏层向量表示为𝑌=(𝑦1,𝑦2,...,𝑦𝑚) ;输出层向量表示为𝑂=(𝑜1,𝑜2,...,𝑜𝑟) ;输入层到隐藏层的权值表示为𝑉=(𝑉1,𝑉2,...,𝑉𝑚) ,𝑉𝑗 是其中一个列向量,共有n行,表示输入层所有神经元通过𝑉𝑗 加权,以得到隐藏层的第j个神经元的输入;隐藏层到输出层的权值表示为𝑊=(𝑊1,𝑊2,...,𝑊𝑟) ,𝑊𝑘 是其中一个列向量,共有m行,表示隐藏层的所有神经元通过𝑊𝑘 加权,以得到输出层的第k个神经元输入。 值得注意的是,为了更清楚的表现出这个过程,上述说明中没有考虑偏置和激活函数。 反向传播 输出层的期望输出:𝐷=(𝑑1,𝑑2,⋯,𝑑𝑟) ,输出层的实际输出:𝑂=(𝑜1,𝑜2,⋯,𝑜𝑟) 。 假设实际输出和期望输出之间的误差是𝐄 ,则对于单个样本所产生的误差损失为: 𝐸=12∑𝑖=1𝑟(𝑑𝑖−𝑜𝑖)2 则𝐄 是一个关于输入𝐗 、权值𝐖 和𝐕 、输出𝐎 的函数。要修正𝐖 ,则需要求解具体的修正增量Δ𝑊 ,可得: Δ𝑤𝑗𝑘=−∂𝐸∂𝑤𝑗𝑘=−∂𝐸∂𝑜𝑘∂𝑜𝑘∂𝑤𝑗𝑘 其中,∂𝐸∂𝑜𝑘 是输出的增量,因此有: ∂𝐸∂𝑜𝑘=𝑜𝑘−𝑑𝑘 暂时记𝛽𝑘=∑𝑗=1𝑚𝑤𝑗𝑘𝑦𝑗,𝑜𝑘=𝑓(𝛽𝑘) (𝑓 为激活函数,选择Sigmoid()函数),故: ∂𝑜𝑘∂𝑤𝑗𝑘=∂𝑜𝑘∂𝛽𝑘∂𝛽𝑘∂𝑤𝑗𝑘 其中, ∂𝑜𝑘∂𝛽𝑘=∂𝑓(𝛽𝑘)∂𝛽𝑘=−(11+𝑒−𝛽𝑘)2(−𝑒−𝛽𝑘)=𝑜𝑘(1−𝑜𝑘) ∂𝛽𝑘∂𝑤𝑗𝑘=∂(∑𝑗=1𝑚𝑤𝑗𝑘𝑦𝑗)∂𝑤𝑗𝑘=𝑦𝑗 可以得到: ∂𝑜𝑘∂𝑤𝑗𝑘=∂𝑜𝑘∂𝛽𝑘∂𝛽𝑘∂𝑤𝑗𝑘=𝑜𝑘(1−𝑜𝑘)𝑦𝑗 至此,可以得到: Δ𝑤𝑗𝑘=−∂𝐸∂𝑤𝑗𝑘=−∂𝐸∂𝑜𝑘∂𝑜𝑘∂𝑤𝑗𝑘=(𝑑𝑘−𝑜𝑘)𝑜𝑘(1−𝑜𝑘)𝑦𝑗 因此,可以相应地调整权值W。另一个权值V的调整同理可得: Δ𝑣𝑖𝑗=−∂𝐸∂𝑣𝑖𝑗=(∑𝑘=1𝑟((𝑑𝑘−𝑜𝑘)𝑜𝑘(1−𝑜𝑘)𝑤𝑗𝑘))𝑦𝑗(1−𝑦𝑗)𝑥𝑖 所以,调整后的新权值公式为: 𝑤′𝑗𝑘=𝑤𝑗𝑘+𝜂1Δ𝑤𝑗𝑘 𝑣′𝑖𝑗=𝑣𝑖𝑗+𝜂2Δ𝑣𝑖𝑗 即 𝑤′𝑗𝑘=𝑤𝑗𝑘+𝜂1(𝑑𝑘−𝑜𝑘)𝑜𝑘(1−𝑜𝑘)𝑦𝑗 𝑣′𝑖𝑗=𝑣𝑖𝑗+𝜂2(∑𝑘=1𝑟((𝑑𝑘−𝑜𝑘)𝑜𝑘(1−𝑜𝑘)𝑤𝑗𝑘))𝑦𝑗(1−𝑦𝑗)𝑥𝑖 其中,𝑤′𝑗𝑘 和𝑣′𝑖𝑗 为调整后的新权值,𝑤𝑗𝑘 和𝑣𝑖𝑗 为调整前的权值,Δ𝑤𝑗𝑘 和Δ𝑣𝑖𝑗 为权值修正量,𝜂1 和𝜂2 是学习率。𝜂1 和𝜂2 控制着每一轮迭代中的更新步长。若值越大,则调节越快,容易振荡;若值越小,则调节越慢,收敛就越慢。训练时,应该根据实际情况调节学习率。 神经网络就是通过输入样本训练后计算误差来不断迭代调整权值W和V的。当误差达到预设的阈值或学习次数大于预设的最大次数时,则训练结束。 算法流程 BP神经网络算法的步骤如下: 步骤1:数据读取。 步骤2:数据预处理。 步骤3:模型初始化。 步骤4:模型训练。 步骤5:模型评估。 导入库 采用sklearn库进行数据导入、数据预处理、模型建立、模型训练、预测和评价工作,导入numpy库进行数据处理工作。 import numpy as np # 用于处理数组和数值计算 from sklearn.model_selection import train_test_split # 用于划分训练集和测试集 from sklearn.neural_network import MLPClassifier # 导入MLPClassifier 数据加载与处理 数据已经事先进行处理后并保存为data.npy文件,我们可以用numpy库进行导入。 data = np.load("/data/bigfiles/data.npy") # 1797个样本 输入64维 10分类问题 接下来,我们将X和Y分别进行存储,再将Y数据转换为独热编码的形式。 在分类问题中,标签数据通常是离散的类别或整数值,但机器学习或深度学习算法通常期望接收输入数据的形式是独热编码。独热编码是一种表示离散特征的常见方法,它将每个类别映射为一个向量,其中只有一个元素为1,其他元素都为0。例如,对于一个具有3个类别的分类问题,可以使用独热编码将类别A表示为[1,0,0],类别B表示为[0,1,0],类别C表示为[0,0,1]。这能够使算法更好地理解类别之间的关系和区别,提供更好的模型性能,并且能够避免数值偏好,避免算法对类别之间的数值大小产生偏好。 X = data[:,:-1] # (1797,64) Y = data[:,-1].astype(np.int32) # (1797,1) (n, m) = X.shape # n=1797 m=64 Y = np.eye(10)[Y] # 转one-hot编码,10分类问题 同样的,我们需要进行数据集的划分工作。 # 划分训练集和测试集 X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size=0.2, random_state=42) 模型初始化 我们可以利用MLPClassifier来直接创建模型对象。 MLPClassifier的重要参数如下:image.png 由于本次使用三层神经网络进行识别,因此可通过设置hidden_layer_sizes参数确定一层隐藏层的参数数量(维度),本实验中我们设置隐藏层维度为16,训练次数为3000。 bp = MLPClassifier(hidden_layer_sizes=16, max_iter=3000) 模型训练 bp模型建立完成后,可利用fit方法对测试集进行训练。 bp.fit(X_train,Y_train) MLPClassifier(hidden_layer_sizes=16, max_iter=3000) 模型预测 模型训练完成后,可利用predict方法预测测试集内各图像对应的数字。 predictions = bp.predict(X_test) 模型评估 为了评估模型效果,本实验将每个测试数据的输出值与实际值进行对比,计算得到最终准确率并输出。 acc = np.sum( predictions * Y_test ) / Y_test.shape[0] print("Training accuracy is: %f"%(acc)) Training accuracy is: 0.958333 思考¶ 试着调整神经网络模型的隐藏层函数维度和训练次数,看看准确率结果有什么改变。 ​ ​
最新发布
12-02
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值