
关注人工智能与各学科的交叉发展
题目:编写一个程序,用递归的方法实现查找数组中的最大值。 C++实现 1 #include<iostream> 2 3 using namespace std; 4 //第一种方法是常规方法,不是使用递归,首先将第一个元素的值赋值给max,然后遍历数组, 5 //当遇到超高max的值时将其赋值给max,最后就将得到最大值 6 int getMax_fir(int *arr,int n) { 7 int max = arr[0]; 8 for (int i = 1; i < n; i++) { 9 if (max < arr[i]) 10 max = arr[i]; 11 } 12 return max; 13 } 14 15 //第二种方法是使用递归,递归就是讲大规模问题转成小规模的相同问题,将数组看成第一个元素与后面的数组的最大值作比较, 16 //后面的数组求最大值又可以看成它的第一个元素与后面的数组最大值比大小,以此类推性,形成递归 17 int getMax_sec(int *arr, int n) { 18 if (n == 1) //设置终止条件 19 return arr[0]; 20 int tem = getMax_sec(arr + 1, n - 1); //指针加一表示下一个元素开始 21 if (arr[0] > tem) 22 return arr[0]; 23 else 24 return tem; 25 } 26 27 int main(int argc, char *argv[]) { 28 int arr[10] = { 2,4,5,65,2,8,2,5,6,55 }; 29 cout << getMax_fir(arr, 10) << endl; 30 cout << getMax_sec(arr, 10)<<endl; 31 getchar(); 32 return 0; 33 } 说明: (1)第一种方法是常规方法,不是使用递归,首先将第一个元素的值赋值给max,然后遍历数组,当遇到超高max的值时将其赋值给max,最后就将得到最大值。 (2)第二种方法是使用递归,递归就是讲大规模问题转成小规模的相同问题,将数组看成第一个元素与后面的数组的最大值作比较,后面的数组求最大值又可以看成它的第一个元素与后面的数组最大值比大小,以此类推性,形成递归。第二种方法符合题目要求。
1.1计算机视觉 (1)计算机视觉的应用包括图像分类、目标检测、图像分割、风格迁移等,下图展示了风格迁移案例: (2)图像的特征量非常之大,比如一个3通道的1000*1000的照片,其特征为3*1000*1000达到300万,如果第一个隐藏层有1000个单元那么W[1]有20亿个参数,计算量不仅大,而且由于图像样本相对于特征实在是太少,导致很容易过拟合,所以需要其他的方式来连接,即卷积。 1.2边缘检测示例 (1)卷积运算是输入图像与过滤器(也叫核)进行的运算,得到输出图像。卷积核与图像对应的位置相乘求和得到一个新值,如下图所示: 输出中第一个绿色框的值为: (2)每个不同的核可以检测到不同的边缘特性,如下面的核就可以检测到图像的垂直特性,即输入图像中的边缘会在输出图像中用白色显示出来,非边缘部分显示为黑色或灰色。同理还有其他水平边缘检测等各种核(过滤器)。 1.3更多边缘检测的内容 (1)除了上面提到的卷积核,还有其他许多卷积核,把上面3*3的卷积核看成9个参数,然后不是通过人工的确定,而是通过神经网络来学习这些参数,这就是卷积神经网络。 1.4Padding (1)边缘不填充会有两个缺点:第一是随着不断卷积,图像会变得越来越小,有时你可不想让它变小;第二是最角落的点只被使用了一次,这意味着在下传的过程中丢掉了图像边缘位置的信息。如下图所示(角落的绿色点只被计算了一次,中间红色点可以被计算多次): (2)Padding经常可以设置成为两个参数:第一个是Valid,即不做填充;第二个是Same,即输出尺寸与输入尺寸相等。 (3)在步长为1是有公式:n+2p-f+1为输出的尺寸。其中n是输入的尺寸(比如说宽),f是卷积核的大小(比如3),p是每一边额外添加的列数(如添加一列就为1)。所以根据这个式子很容易计算出用Same参数时,p=(f-1)/2,注意此处前提都是步长为1。 1.5卷积步长 (1)输入与输出的尺寸关系如下,注意当结果不是整数时是向下取整。 (2)在深度学习的卷积没有必要像数学或者信号处理教材中,先将卷积核顺时针旋转90°,然后在水平翻转,最后再进行与上面相同的卷积运算。深度学习直接忽略了那些旋转翻转的步骤。影响不大。 1.6三维卷积 (1)三维的卷积方法如下图所示,卷积核的通道数与输入图像的通道数相同,输出图像的通道数为所使用的卷积核的个数,至于高和宽还是按照上面提到的公式计算: 1.7单层卷积网络 (1)每一个卷积核的输出对应一个实数b(偏差),然后在进行激活函数的非线性转换得到输出,如下图所示: (2)参数个数的计算:比如10个卷积核3*3*3,b的个数跟卷积核个数相同,所以总的参数为(3*3*3+1)*10=280,不管输入尺寸多大,参数个数始终保持不变,而在全连接网络中参数个数是会随着输入不同而不同的。 (3)一些符号如下所示,习惯上w用m*nH[l]*nW[l]*nC[l]表示,b用1*1*1*nC[l]。 1.8简答的卷积网络示例 (1)案例图如下:最核心的就是要会计算输出尺寸(公式((n+2p-f)/s)+1,向下取整)。 1.9池化层 (1)池化层中没有需要学习的参数,所以通常不把池化层当做独立的一层来看。 (2)池化层是一般不会设置padding,即一般padding为0。 (3)fitter为2,stride为2是最常见的参数设置,尺寸图像缩小为原来的一半。 (4)卷积时用的尺寸计算公式同样适用于池化层,如下图所示: (5)最大池化层比平均池化层更为常用。 1.10卷积神经网络示例 (1)下面是一个0-9数组分类的网络,包括了卷积层、池化层、全连接层: (2)一般而言,不断卷积之后,图像的高度和宽度会变小,通道数(深度)会增加。 (3)在神经网络中,另一个常见的模式就是一个或多个卷积层之后跟随一个池化层,然后一个或多个卷积层之后跟随一个池化层,然后跟几个全连接层,最后是一个softmax. (4)下面是针对上面网络的一些输出和参数的个数,其中参数一栏最后三行的值应该是48000+120、10080+84、840+10。 1.11为什么使用卷积 (1)卷积网络的参数远少于全连接的原因主要有两点:第一是参数共享,如左上角用一个垂直的卷积核检测,那么这个卷积核也同样适用于图像的其他区域;第二是稀疏连接,如某个输出值只与特定的几个值相连接(如九个值)。 (2)卷积神经网络善于捕捉偏移不变形,例如把图像往右平移几个像素,对于网络而言没什么影响。
题目:请说出下面图形中包含多少个三角形?请用一个程序完成计算。 C++版本 1 #include<iostream> 2 3 using namespace std; 4 5 const char NO_POINT = '0'; 6 7 //任意的一条线 8 const char *map[] = { "ad","ab","db","ae","aj","ah","ej","eh","jh","af","ak","ai","fk","fi","ki","ag","ac","gc", 9 "de","df","dg","ef","eg","fg","bj","bk","bg","jk","jg","kg","bh","bi","bc","hi","hc","ic" }; 10 //共线的点 11 const char *line[] = { "adb","aejh","afki","agc","defg","bjkg","bhic" }; 12 13 //点是否在线上 14 int contain( const char *str, char a) { 15 int i = 0; 16 while (str[i] != '\0') { //注意字符使用单引号,字符串是双引号 17 if (str[i] == a) 18 return 1; 19 i++; 20 } 21 return 0; 22 } 23 24 //三个点是否在一条线上函数 25 int isInALine(const char *str[], char a, char b, char c) { 26 int i ; 27 for (i = 0; i < 7; i++) { 28 if (contain(str[i], a) && contain(str[i], b) && contain(str[i], c)) { 29 return 1; 30 } 31 } 32 return 0; 33 } 34 35 //两条线的交点函数 36 char getCrossPoint(const char *str1, const char *str2) { 37 if (*str1 == *str2) 38 return *str1; 39 if (*str1 == *(str2 + 1)) 40 return *str1; 41 if (*(str1 + 1) == *str2) 42 return *(str1 + 1); 43 if (*(str1 + 1) == *(str2 + 1)) 44 return *(str1 + 1); 45 return NO_POINT; 46 } 47 48 //三条线两两必须有交点,并且三条线不能共线才能构成三角形。 49 int isTriangle(const char *str1, const char *str2, const char *str3) { 50 char Point1, Point2, Point3; 51 Point1 = getCrossPoint(str1, str2); 52 if (Point1 == NO_POINT) 53 return 0; 54 Point2 = getCrossPoint(str1, str3); 55 if (Point2 == NO_POINT) 56 return 0; 57 Point3 = getCrossPoint(str2, str3); 58 if (Point3 == NO_POINT) 59 return 0; 60 if (isInALine(line, Point1, Point2, Point3)) 61 return 0; 62 return 1; 63 } 64 65 int getTriangelCount( const char *str[]) { 66 int i, j, k,count=0; 67 for (i = 0; i < 36; i++) { 68 for (j = i+1; j < 36; j++) { 69 for (k = j+1; k < 36; k++) { 70 if (isTriangle(str[i], str[j], str[k])) 71 count++; 72 } 73 } 74 } 75 return count; 76 } 77 78 int main(int argc, char *argv[]) { 79 cout << getTriangelCount(map); 80 getchar(); 81 return 0; 82 } 解题思路: (1)给每个交点做标记,如下: (2)总共有36条线段,如果三条线段两两之间存在交点,但一条线上(已经包含了三条线交于同一点),则可以构成三角形。如下图所示,最左边的构成三角形,右边两个不构成三角形: (3)故需要有如下一些子函数:求两条线的交点,三个点是否共线等。
1.1为什么是ML策略 (1)当对一个实际的应用系统进行优化时,可能有很多想法:如提高数据量,提高网络深度,正则化等等,一个错误的选择可能浪费非常多的时间,本课就是让你在面对很多选择时做出正确的选择,这就是ML策略。提高效率,让你的深度学习系统更快投入使用。 1.2正交化 (1)使用以下的老式电视机来说明什么是正交化,即一个按钮只调节宽度(不会对其他造成影响),一个只调节高度,一个只调节角度,这样就可以很容易的讲画面调节到正中央,如果一个按钮既影响高度有影响角度,那么将非常难调整。 (2)同样在机器学习系统中,可能出现测试集效果好,验证集效果不好,这时如果有一个策略可以改善这个,同时不影响其他的东西,那就是正交化;又比如为什么不推荐使用early stopping,因为它一方面改善了过拟合,但是会使得训练集拟合变得不那么好,所以这个策略不是正交化。下图是深度学习系统各阶段可能出现的问题,应该找到一可以改善它而不影响其他性能的策略(即正交化的策略)。 1.3单一数字评价指标 (1)F1分数公式:2(RP)/(R+P);其中P是查准率,R是查全率。 1.4满足和优化指标 (1)当有N个指标时,选出其中一个最想优化的指标来最小化,其余N-1个指标作为满足指标,即在满足满足指标的前提下(如运算速度要小于100ms),选择优化指标最好的模型。 1.5训练、开发和测试集划分 (1)记住一点:务必让开发集和测试集是同分布,否则训练好的网络其实没什么用。 (2)设置好单一评估的开发集,就像是一个靶心,优化的过程就是去瞄准靶心,然后瞄的很准之后,测试集和开发集不同分布就相当于把靶心换到其他位置了,所以效果会非常的差。如下图所示(靶心变了)。 1.6开发集和测试集的大小 (1)在大数据时代,70/30、60/20/20的分法已经不再适用了,现在流行把大把数据拿去训练,只要留有足够的开发集和测试集就行。这里开发集足够的标准是可以判断出哪个模型好,然后这里测试集足够的标准是可以准确评估最终的成本偏差。 1.7什么时候该改变开发/测试集和指标 (1)总体方针:如果你当前的指标和当前用来评估的数据和你真正关心必须做好的事情关系不大是,这时候就应该更改你的指标或者你的开发集了,让它们能更好的反应你的算法需要处理好的数据。 (2)案例1:A、B模型的误差分别为3%、5%,根据指标看A更好,但是在使用过程中发现A会把情色照片推送给用户,B不会,显然这种情况下其实B是更好的算法,这时说明需要修改评价标准如修改代价函数: 改成: 当把色情照片推送给用户时给予很大的权重,这样根据新的指标可以选出B算法是更好的,这样就与实际符合了。 (3)案例2:开发集、测试集都是使用网上的高清猫的照片,A、B模型的误差分别为3%、5%,根据指标A更好,而实际需要分类的是来自普通用户拍摄的低像素模糊的照片,这时B算法表现的更好,出现这种情况说明要更改开发集、测试集了,使其与实际中的照片更加接近。 (4)在上面提到的各种方法中,其实已经不自觉的使用了正交化,即每种修改只影响一个方面,对其他不造成影响。 1.8为什么是人的表现 (1)当性能低于人类的表现时可以提高的很快,但是超过人类的表现之后,性能的提升将会变慢。如下图所示 (2)把性能的极限(即模型可能达到的最高性能)称为贝叶斯最优错误率(Bayes optimal error)。 (3)超过人类表现之后性能提高变慢主要有两个原因:首先因为超过人类之后说明已经快接近贝叶斯极限了,进步的空间本身就小了;其次是因为超过人类之后,很多的工具将不再能起作用,如低于人类表现时人类还可以帮忙做误差分析。 1.9可避免的偏差 (1)在计算机视觉中,人类是非常擅长的,人类的表现和贝叶斯最优误差相差不大,可把前者当做后者。 (2)把测试集误差减去人类的误差称为可避免误差,这是可以提高的部分,下如图:同样的训练误差和开发集误差,但是根据人类的表现不同,需优先优化的也不同,左边应先降低偏差,右边应该先降低方差(因为8%-7.5%已经很小了): (3)开发集误差减去训练集误差叫可避免方差。 1.10理解人的表现 (1)案例:医学图像分类,普通人3%错误率,……,专家组讨论结果能达到0.5%,这时应该把最好的表现来当做人类的表现。 (2)在实际应用中,不一定要达到人类的最高水平才能投入使用,比如一个系统超过了普通放射科医生的水平,这时这个系统就应该可以投入使用;所以这也说明不同情况下,定义人类的水平错误率时,要弄清楚目标在哪里(不一定是最高的人类水平),超过这个目标就应该可以拿来用。 1.11超过人类的表现 (1)在结构化问题上,如电影推荐等,机器远超人类水平。 (2)在计算机视觉,语音识别等也达到和超过人类的水平。 (3)在自然感知方面还有待加强。 1.12改善你模型的表现 (1)降低偏差和方差的策略如下图所示: 降低偏差:更大更深的模型,更长的训练,其他的优化方法,更换网络结构 降低方差:更多的数据、正则化、dropout、数据增强、提前终止(不推荐使用)、更换网络
3.1调试处理 (1)不同超参数调试的优先级是不一样的,如下图中的一些超参数,首先最重要的应该是学习率α(红色圈出),然后是Momentum算法的β、隐藏层单元数、mini-batch size(黄色圈出)、再之后是Layer、learning rate decay(紫色圈出)、最后是Adam算法中的β1、β2、ε。 (2)用随机取值代替网格点取值。下图左边是网格点取值,如果二维参数中,一个参数调试的影响特别小,那么虽然取了25个点,其实只相当于取了5个不同的点;而右图中随机取值取了多少个点就代表有多少不同值的点。 (3)由粗糙到精细的取值,先粗糙取值,然后发现最好的点,再在这个点附近进行精细的取值。如下图所示 3.2为超参数选择合适的范围 (1)随机取值并不是在取值范围内随机均匀取值,而是要选择合适的标尺来随机取值。 (2)案例1:在选择网络层数时,其范围是[2,4],那么直接均匀取值2,3,4都是合理的。 (3)案例2:如果在给学习率取值时,其范围是[0.0001,1],如果均匀取值,将会有90%的点落在0.1到1之间,这时不合理的;此时应该用对数坐标0.0001=10-4,1=100,所以应该是在[-4,0]上随机均匀取值作为r,然后10r作为学习率α。如下图所示 (4)指数加权平均的超参数β取值范围是[0.9,0.999],其方法是:1-β=[0.1,0.001],然后再根据学习率提到的用对数坐标来随机取值。 (5)在取值微小变化会带来巨大结果不同的地方(β在0.9990到0.9995敏感度就比0.9到0.9005高)即灵敏度高,需要去更多更密集的值,这就是为什么要选择合适的标尺。 3.3超参数训练的实践 (1)当计算资源少的时候,只能一个模型慢慢调参,悉心照顾,当计算资源丰富时,可以模型同时选择不同参数进行训练,然后找出最优的。如下图所示 3.4归一化网络的激活函数 (1)计算过程如下图所示(总共包括四个式子): (2)特征输入归一化之后均值为0,方差为1,但是对隐藏层的归一化而言,她的均值和方差是空调的,即通过γ、β两个超参数调整。之所以不希望都是均值为0,方差为1,因为那样的话可能都集中再激活函数的线性区域,导致可能没法得到任意想要的值。如下图所示 (3)一般情况下都是对z(即激活函数之前)进行归一化的。 3.5将Batch Norm拟合进神经网络 (1)使用以下公式来进行更新参数,其中原来的b已经可以去掉,因为不管是多少都会在归一化中被消除,然后用新的参数β替代(此处的β是归一化时的参数,不是优化算法中的β): 除了以上的这种更新方式之外,也可以用其他优化算法进行更新。 3.6Batch Norm为什么奏效 (1)浅层的理解可以按照之前提到的,把输入特征归一化之后,可以加快训练的思路来理解每一层归一化的作用。 (2)深层原因:当已经学的x到y的映射,然后当x的分布发生变化是,该映射将需要重新学习,这里的x可以理解成中间的某一隐藏层,x的分布是受到它前面层参数的影响的,为了时x的分布尽量不受到影响(这样x到y的映射可以尽量少做调整),所以加入了归一化,这样x的均值可以始终固定为β,方差固定为α。这样即使x值会发生变化,但是其分布是不变的(或者说变得更少),这样一来减弱了前层参数对后层参数的影响,互相之间相对较独立,更有利于各层之间学习自己的映射,这样有助于加速网络的训练。如下图中框选出来的中间层它的值受前面参数影响,同时又是后层的输入,归一化保证了该层的分布不变性。 3.7测试时的Batch Norm (1)训练时mini-batch有样本来计算均值和方差,如下式子(式子中的m是mini-batch size): (2)但是在测试集时,是一个一个进行测试的,一个样本求均值和方差是没有意义的。所以使用的到方法就是:在训练是每一个批次获得对应的均值和方差,然后用之前提到的指数加权平均来实时获得最新的均值和方差给测试时来用(当然还有其他估算均值和方差的方法)。有了均值和方差之后,测试数据就可以按照上面的式子进行归一化了,使用的β、γ是训练出来的。 3.8Softmax回归 (1)softmax激活函数常用于多分类问题的最后一层作为激活函数,它将最后一层算出来的z[L]取幂函数,然后求和,最后再把每个单元取幂函数之后都分别除以求和,得到各自的概率输出。如下所示 3.9训练一个Softmax分类器 (1)分类器的损失函数(一个样本): 如四分类器中样本标签(左边)和预测值(右边)如下: , 所以损失函数简化为: (2)代价函数: 3.10深度学习框架 (1)一些常见的深度学习框架 3.11TensorFlow (1)给一个TensorFlow的简单使用案例:
题目:我们把只含有因子2、3、5的数称为丑数。例如6、8都是丑数,而14不是丑数,因为它含有因子7.通常也把1当做丑数。编程找出1500以内的全部丑数。注意:使用的算法效率应尽量高。 C++实现: 1 #include<iostream> 2 3 using namespace std; 4 5 //判断一个数是否是丑数,uglynum=2*2……*2*3*3*……*3*5*5……*5组成 6 int isUglyNumber(int n) { 7 while (n%2==0) 8 { 9 n /= 2; 10 } 11 while (n%3==0) 12 { 13 n /= 3; 14 } 15 while (n%5==0) 16 { 17 n /= 5; 18 } 19 return n == 1; 20 } 21 //方法1:逐个判断是否是丑叔,思路简单,但是计算冗余,因为越到后面很多都不是丑数也在计算。 22 int get_Ugly_fir(int number) { 23 int i = 1; 24 int count = 0; 25 while (i<=number) 26 { 27 if (isUglyNumber(i)) { 28 cout << i << "\t"; 29 count++; 30 if (count % 10 == 0) 31 cout << endl; 32 } 33 i++; 34 } 35 return count; 36 } 37 38 //方法二:算法效率高。 39 //思路:(1)后面的丑数肯定是已存在的丑数乘以2、3或者5,找到比现有丑数大的且是最小的丑数作为下一个丑数(如何找是关键)。 40 //2分别从现有丑数中从前往后乘以丑数,找到第一个大于当前所有丑数的值以及位置,3、5同样如此,再把他们相乘之后的结果做对比,取最小的。 41 //下次将从上一次的位置开始往下找,这样将不会出现冗余,详细见如下函数。 42 43 int Next_Ugly(int ugly_arr[], int *loc2, int *loc3, int *loc5, int *index) { 44 while (ugly_arr[*loc2] * 2 <= ugly_arr[*index]) { //千万注意这里是小于等于,不要写成小于了 45 (*loc2)++; 46 } 47 while (ugly_arr[*loc3] * 3 <= ugly_arr[*index]) { 48 (*loc3)++; 49 } 50 while (ugly_arr[*loc5] * 5 <= ugly_arr[*index]) { 51 (*loc5)++; 52 } 53 if (ugly_arr[*loc2] *2< ugly_arr[*loc3]*3) { 54 return (ugly_arr[*loc2] * 2 < ugly_arr[*loc5] * 5) ? ugly_arr[*loc2] * 2 : ugly_arr[*loc5] * 5; 55 } 56 else { 57 return (ugly_arr[*loc3] * 3) < ugly_arr[*loc5] * 5 ? ugly_arr[*loc3] * 3 : ugly_arr[*loc5] * 5; 58 } 59 } 60 61 int get_Ugly_sec(int num) { 62 int ugly_arr[1000]; 63 int index = 0, value = 1; 64 int loc2=0, loc3=0, loc5=0; 65 while (value<=num) { 66 ugly_arr[index] = value; 67 cout << ugly_arr[index] << "\t"; 68 if ((index + 1) % 10 == 0) 69 cout << endl; 70 71 value = Next_Ugly(ugly_arr, &loc2, &loc3, &loc5, &index); 72 index++; 73 } 74 return index; 75 } 76 77 int main(int argc, char *argv[]) { 78 get_Ugly_fir(1500); 79 cout << endl; 80 get_Ugly_sec(1500); 81 getchar(); 82 return 0; 83 } (1)说明:总共使用了两种办法,第一种算法效率低,编程简单,第二种算法效率高,编程相对复杂。 (2)方法二思路:后面的丑数肯定是已存在的丑数乘以2、3或者5,找到比现有丑数大的且是最小的丑数作为下一个丑数(如何找是关键)。用2分别从现有丑数中从前往后乘以丑数,找到第一个大于当前所有丑数的值以及位置,3、5同样如此,再把他们相乘之后的结果做对比,取最小的。下次将从上一次的位置开始往下找,这样将不会出现冗余。
题目:有10个任意的正整数,将其分为两组A和B,要求组A中每个数据的和与组B中每个数据的和之差的绝对值最小。请设计算法实现数的分组(找出一个答案即可)。 C++版本: 1 #include<iostream> 2 3 using namespace std; 4 5 void get_groupAB(int arr[]) { 6 int sum_a, sum_b; 7 int index, difference=0; 8 int i,i_copy; 9 //数组所有的元素求和给difference赋初值 10 for ( i = 0; i < 10; i++) { 11 difference += arr[i]; 12 } 13 //从0000000000到1111111111,尾数为零时,对应的数组元素划分给a数组,否则划分给b数组 14 for ( i = 0; i < 0x3FF; i++) { 15 sum_a = 0, sum_b = 0; 16 i_copy=i; 17 for (int j = 0; j < 10; j++) { 18 if ((i_copy & 1) == 0) { 19 sum_a += arr[9 - j]; 20 } 21 else { 22 sum_b += arr[9 - j]; 23 } 24 i_copy >>= 1; 25 } 26 if (difference > abs(sum_a - sum_b)) { 27 difference = abs(sum_a - sum_b); 28 index = i; //存储最小值对应的索引 29 } 30 if (difference == 0) { 31 break; 32 } 33 } 34 //存储分组 35 int sub_a[10], sub_b[10]; 36 int count_a=0, count_b=0; 37 for (i = 0; i < 10; i++) { 38 if ((index & 1) == 0) { 39 sub_a[count_a] = arr[9 - i]; 40 count_a++; 41 } 42 else { 43 sub_b[count_b] = arr[9 - i]; 44 count_b++; 45 } 46 index >>= 1; 47 } 48 //输出显示部分 49 cout << "group A:" << endl; 50 for (i = 0; i < count_a; i++) { 51 cout << sub_a[i] << endl; 52 } 53 cout << "group B:" << endl; 54 for (i = 0; i < count_b; i++) { 55 cout << sub_b[i] << endl; 56 } 57 cout << "difference:" << difference << endl; 58 } 59 60 int main(int argc, char *argv[]) { 61 62 int arr[10]; 63 int i=0; 64 while (i < 10) { 65 cin >> arr[i]; 66 i++; 67 } 68 get_groupAB(arr); 69 getchar(); 70 getchar(); 71 return 0; 72 } 思路:可以用一个10位的二进制数表示,对应位置为零时,分给一个组,为1时分给另外一个组;任何一个数都可以分给组A或者组B两种情况,故总的情况共有2^10,即1024种,其中不能全给A,也不能全给B,所以总共1024-2=1022种情况,进行枚举即可。另外如果出现差值为0时可以马上终止循环,因为不可能出现比0小的数了。
2.1Mini-batch梯度下降 (1)例如有500万个训练样本,这时可以每1000个组成一个Mini-batch,共用5000个Mini-batch。主要是为了加快训练。 (2)循环完所有的训练样本称为(1 epoch)。 (3)使用大括号X{t},Y{t}表示一个Mini-batch。(小括号(i)表示第i个样本,中括号[l]表示神经网络第l层)。 2.2理解mini-batch梯度下降法 (1)batch梯度下降时,每一次迭代代价函数都会降低(如果某一次不是,说明出问题了,可能要改变学习率),而mini-batch梯度下降时,不一定每次都降低,但是总的趋势是下降的。如下图所示: (2)Mini-batch的大小设为m(总样本数)时,变成了batch梯度下降(训练慢当样本总数大时),当设为1,变成了随机梯度下降(这时没能很好利用多样本的向量化的优势,也会导致变慢)。所示实际中选择不大不小的mini-batch尺寸,下降速度达到最快。 (3)不管是随机梯度下降还是mini-batch梯度下降都不会达到收敛,如下图所示紫色线条,所以后期需要减小学习率来使其趋向收敛。 (4)当样本数小于2000时可直接使用batch梯度下降,当样本数很大时,一般把mini-batch的大小设为2的n次方,比如64,126,512等,这样是考虑到电脑内存设置和使用方法。 (5)同第四点,所以在调参mini-batch的大小时常常设置2的不同次方。 2.3指数加权平均数 (1)下图中蓝色为每天的的温度,红色是温度的指数加权平均数,使用如下公式计算而来: (2)将上式更一般的表示如下: β越大,反应越快,波动性却强,如下图中黄色线;β越小,延迟多大,越平缓,如下图中绿色线。 (3)在计算时,可以认为vt是天的平均温度,如β等于0.5时,看成是两天的平均温度,β=0.98,则是50天的平均温度。 2.4理解指数加权平均数 (1)根据下面式子: 进行展开: 所以V100其实是下面两个图对应点乘积的求和(右图是从0.1开始往左指数下降): (2)当β为0.9时,下降到第十天(0.9)10约等于0.35,大约是1/e,认为降到这个数之后可以忽略不计了。 (3)指数加权平均数公式的好处之一就在于,他占用极少内存,电脑内存中占用一行数字而已,然后把最新的数据代入公式,不断覆盖就可以了。 2.5指数加权平均数的偏差修正 (1)按照前面提到的公式计算,比如β为0.9,得不到下图中绿色的线,而是得到紫色的线,因为一开始已经没有之前的数据可以加了,导致小了很多。 (2)解决方法是引入偏差修正,即按照之前的公式算出vt之后,再除以(1-βt),如下所示: 当t=2时: 这样将能能到绿色的线,当t很大时,分母也趋向于1,那时将不起作用(也不需要起作用)。 2.6动量梯度下降法(Momentum) (1)下图中蓝色是梯度下降的过程: (2)我们希望沿着垂直方向的振幅能变小,因为是无效的,同时因为振幅的存在使得学习率不能太大,太大振幅会更大,甚至训练的越来越差;同时横轴方向希望学习率能增大,更加快速的到达最优点,以上矛盾的两者,可以通过上一节讲到的指数加权平均来解决(因为垂直方向的均值为0,通过加权平均之后将会减小或者消除振幅,横轴方向因为始终吵着一个方向,所以进行指数加权平均没多大影响),公式如下: (3)是否进行偏差修正影响不大,β取0.9是一个比较好的参数,学习率α会随之β的修改做一定的修改。 2.7RMSprop (1)下图中,沿着b振幅大,说明db比较大: (2)使用如下式子进行参数更新,理解:前两个式子平方之后将不会出现Momentum中权值平均后变为0的情况,哪边振幅大对应计算结果大,所以Sdw较小,Sdb较大;然后dw除以一个较小的数也就相当于取了较大的学习率,db除了一个较大的数,也就相当于取了一个较小的学习率,这就是RMSprop: (3)最终会按照绿色的线优化: 2.8Adam 优化算法 (1)按照下列式子顺序即可得到Adam,他完美的讲上面提到的两种算法结合在了一起,需要注意一点是,Adam需要参数修正: (2)参数涉及学习率α,β1(Momentum),β2(RMSprop),ε(RMSprop),其中学习率需要调整,其他三个参数使用下面图中的默认值就很好。 2.9学习率衰减(Learning rate decay) (1)以下公式是一个学习率衰减的式子,其中包含了初始学习率a0好衰减率(decay-rate)两个参数需要调整: (2)其他降低学习率的式子: (3)还有离散的学习率,过一会变成原来的一半,还有不少人直接手动调整。 2.10局部最优的问题 (1)在低维(如二维)可能陷入局部最优,如下图: (2)但是在高维中,比如20000维,陷入局部最优的概率是2-20000(即每一维度都梯度为零,几乎不可能),所以更多的时候是出现处在鞍点上:如下图: (3)存在的问题是:在平稳端学习缓慢,上面提到的算法如Adam,能够更快的走出平稳区。
丢番图的一生1/6是童年,青少年时代占了他一生的1/12,随后1/7他说过着独身的生活,结婚后5年他生了一个儿子,他感到很幸福,可是这孩子的生命只有他父亲的一半,儿子去世后,丢番图就在深深痛苦中活了4年,结束了生命,请问丢番图活了多少岁?丢番图的一生1/6是童年,青少年时代占了他一生的1/12,随后1/7他说过着独身的生活,结婚后5年他生了一个儿子,他感到很幸福,可是这孩子的生命只有他父亲的一半,儿子去世后,丢番图就在深深痛苦中活了4年,结束了生命,请问丢番图活了多少岁? C++版本 1 #include<iostream> 2 3 using namespace std; 4 5 int get_age() { 6 for (float i = 20; i < 120; i++) { 7 if (i/6.0+i/12.0+i/7.0+5.0+i/2.0+4.0==i) { //注意用浮点数 8 return (int)i; 9 } 10 } 11 return -1; 12 } 13 14 int main(int argc, char *argv[]) { 15 cout << get_age(); 16 getchar(); 17 return 0; 18 } Python版本 1 # -*- coding:utf-8 -*- 2 3 def get_age(): 4 for age in range(20,120): 5 if(age/6.0+age/12.0+age/7.0+5.0+age/2.0+4.0==age): 6 return age 7 return -1 8 9 if __name__=="__main__": 10 print(get_age()) 方法:列出数学等式,然后枚举即可,注意用浮点数。
1.1训练,验证,测试集(Train/Dev/Test sets) (1)深度学习是一个按照下图进行循环的快速迭代的过程,往往需要多次才能为应用程序找到一个称心的神经网络。 (2)在机器学习中,通常将样本分成训练集,验证集和测试集三部分,数据规模相对较小,适合传统的划分比例(如6:2:2),数据集规模比较大的,验证集和测试集要小于数据总量的20%或者10%甚至更低。 (3)交叉验证集和测试集务必来自同分布。 (4)有时候只有训练集和验证集,没有独立的测试集(将无法提供无偏性能评估),这时人们也会把验证集称为测试集。 1.2偏差,方差(Bias/Varicance) (1)以下三个图分别表示欠拟合(高偏差),适度拟合,过拟合(高方差): (2)最优误差也称为贝叶斯误差,本节中假设最有误差为零(如在图像分类中人可以辨别出所有图像的类别)。 (3)训练误差减去左右误差为偏差,结果大说明偏差大;验证集误差减去训练误差为方差,结果大说明偏差大。 (4)是存在高偏差高方差的情况的,如下图,直线导致高偏差,局部过拟合导致高方差: 1.3机器学习基础 (1)偏差和方差是两种完全不同的情况,有分别对应的处理方法,不要盲目的使用一些策略。 (2)在深度学习时代,只要正则适度,通常构建一个更大的网络便可以在不影响方差的同时减少偏差,而采用更多数据通常可以在不过多影响偏差的同时减少方差。 1.4正则化 (1)过拟合常用的两种解决方法:添加正则化项(容易实现),增加更多数据(有时候很难得到更多数据)。 (2)L1正则化往往会使得W最终稀疏,即w向量中很多是0,事实证明它并没有减少太多的存储空间,所以现在越来越多人还是使用L2正则。 (3)L2正则式子如下: 其中被定义为矩阵中所有元素的平方求和。 (4)正则化常常被称为“权重衰减”,是因为正则项会试图让W变得更小,实际上相当于给矩阵W乘以(1-αλ/m),如下所示: 1.5为什么正则化有利于预防过拟合 (1)第一种直观理解,首先一个很复杂的神经网络(过拟合): 然后添加正则项,使λ,这时候很多权重变成0,然后相当于消除了很多隐藏单元,复杂网络变成很简单的网络(欠拟合),从过拟合到欠拟合中间会经历最优拟合的情况,如下图所示: (2)第二种理解,W实际不会变成零,只会变得非常小,这时候z也会变得非常小,那么根据以下的激活函数,将会在中间线性的地方活动,那么相当与经过很多次线性变换,所以这也导致网络变得简答,消除了过拟合情况。 1.6dropout正则化 (1)原网络如下: (2)设置keep-prob为0.8(相当于一个d[l]向量中80%为1,百分之20%为零),这个向量与某一层的输出a[l]相乘(与零相乘自然输出就为零了),其网络示意图如下(图中是设置为0.5): (3)在上一步乘积之后的值又会除以0.8,如下面的公式,这样可以保证均值不会发生改变(因为单元数减少会导致后面一层的输入减少,通过除以减少量来维持不变) (4)测试的时候不使用dropout。 1.7理解dropout (1)直观上理解:不要依赖于任何一个特征,因为该单元的输入可能随时被清除,或者说该单元的输入也都可能被随机清除,因此不愿意在任何一个输入单元上加上太多的权重,会把权重分摊给其他单元,这其实产生了收缩权重的平方范数的效果。 (2)dropout被正式的作为一种正则化的替代方式,L2对不同权重的衰减是不同的,他取决于倍增的激活函数的大小。 (3)不同层之间可以使用不同的keep-prob,一般矩阵W越大的层,越容易导致过拟合,所以keep-prob的值设置的越低(输入层一般为1),如下所示: (4)计算机视觉中常用dropout,因为像素(特征)太多,数据量太少,常常导致过拟合。 (5)dropout的一大缺点就是代价函数J不再明确定义,每次迭代,都会随机移除一些节点,或者说某种程度上很难准确计算。 1.8其他正则化方法 (1)数据增强:旋转、扭曲、任意裁剪放大等。 (2)early stopping:在交叉验证集代价函数(误差率等)下降又上升的拐点处停止,如下图所示: early stoping的主要缺点就是控制w不太大的时候,也终止的优化代价函数J,而不能向其他方式一样:一方面不断的使代价函数变小,用另外的方式来控制使其不发生过拟合。 1.9归一化输入 (1)归一化需要两步:零均值(减去均值)、归一化方差(除以方差)(测试集用的是训练集的均值和方差做处理,不要再计算测试集的均值方差),其效果如下: 公式分别如下: x-=μ x/=σ 2 (2)各特征取值在同一个数量级时(如分别为0-1,-1到1,1-2)时不需要归一化,如果在不同不同数量级时要进行归一化(如0-1,0-1000)。 (3)归一化之后的代价函数如下右图所示(左图为未归一化),归一化之后可以使用更大的学习率,因为每一步都是朝向梯度下降的方向进行的。 1.10梯度消失/梯度爆炸(Vanishing/Exploding gradients) (1)假设为线性激活函数,忽略b,那么对于以下的网络,有如下的输出: 假设每个权重为: 则有: 他是1.5倍的单位矩阵,y=1.5(L-1)x,这时候输出是随着层数增加呈现指数增大的(梯度爆炸,导数时也有这个性质);同理,如果把1.5改成0.5时,将会呈现指数减小,即梯度消失(导数时也有这个性质)。 (2)上面虽然只讨论的激活函数的指数级递增递减,但它同样适用于于层数L相关的导数和梯度函数,也是呈现指数级增长或指数递减。 (3)合理的初始化能够较有效(虽然不能完美解决)解决如上问题。 1.11神经网络的权重初始化 (1)z是由参数与特征乘积求和得到,如下式,我们不希望z过大(爆炸)或者过小(消失),所以当特征特别多时,很自然的希望初始化时w能比较小,所以w的初始化应该与各层的输入个数有关。 (2)使用ReLU激活函数时,对w常用的初始化(因为是看输入个数,即上一层的神经元个数): (3)使用tanh激活函数时,对w常用的初始化: (4)其他初始化方法: (5)以上给出的初始化方差都是默认值,如果想改变方差,可以在上面的公式再乘以一个系数。(通常这一步的调优优先级不高) 1.12梯度的数值逼近 (1)双边误差公式比单边误差公式更准确。 双边误差公式: 、 单边误差公式: 1.13梯度检验 (1)对代价函数的每一个参数进行双边梯度检测: (2)检查计算值和偏到的欧氏距离,当小于10-7,很好;10-5,需要检查;10-3很可能存在错误。 1.14梯度检验应用的注意事项 (1)不要在训练的时候应该梯度检测,它只用于调试。 (2)如果算法的梯度检验失败,需要检测所有项。 (3)当代价函数含有正则化项时,dθ务必将正则项添加进去,不要漏了。 (4)梯度检验和dropout不要同时使用,梯度检验是关掉dropout。后者的存在将会难以计算代价函数J。 (5)这一点一般情况下不会出现,比较微妙:只有在w,b较小的时候,梯度检验才会正确,所以一般过程是先初始化,然后就进行梯度检验,再进行训练(训练一般会时w,b变大导致梯度检验越来越不准确)。
A、B、C、D、E5个渔夫夜间合伙捕鱼,各自在河边的树丛中休息。待日上三竿,渔夫A第一个醒来,他将鱼分作5份,把多余的一条扔回河中,拿自己的一份回家了。渔夫B第二个醒来,也将鱼分作5份,扔掉多余的一条,拿走自己的一份,接着后三个也按同样的办法分鱼,问5个渔夫至少合伙捕了多少条鱼。 1 #include<iostream> 2 3 using namespace std; 4 5 6 int isInteger(float i) { //用于判断是否是整数 7 if (i - (int)i == 0) { 8 return 1; 9 } 10 else { 11 return 0; 12 } 13 } 14 15 //从后面往前面推,初始化count表示E醒来看到的条数,故四次循环之后是A看到的条数, 16 int fish_count() { 17 float count; 18 for (int n = 1; n < 10000; n++) { 19 count = 5 * n + 1; 20 int flag = 1; 21 for (int i = 0; i < 4; i++) { 22 count = count * 5 / 4 + 1.0; 23 if (!isInteger(count)) { 24 flag = 0; 25 break; 26 } 27 } 28 if (flag) { 29 return (int)count; 30 } 31 } 32 return -1; 33 } 34 35 int main(int argc, char *argv[]) { 36 cout << fish_count(); 37 getchar(); 38 return 0; 39 }
4.1深层神经网络 (1)到底是深层还是浅层是一个相对的概念,不必太纠结,以下是一个四层的深度神经网络: (2)一些符号定义: a[0]=x(输入层也叫做第0层) L=4:表示网络的层数 g:表示激活函数 第l层输出用a[l],最终的输出用a[L]表示 n[1]=5:表示第一层有五个神经元,第l层神经元个数用n[l]表示 4.2前向传播和反向传播 (1)前向传播:输入a[l-1],输出是a[l],缓存为z[l],步骤如下:(下面第一个式子应该是a[l-1]) 向量化: (2)反向传播:输入da[l],输出da[l-1],dw[l],db[l] (4)da[l-1]=w[l]T·dz[l] 由第四个式子带入到第一各式子中得 向量化: (3)总结:第一层可能是Relu激活函数,第二层为另一个Relu函数,第三层可能是sigmoid函数(如果做二分类的话),输出值为a[L],用来计算损失,这样就可以以向后迭代进行反向传播就到来求dw[3],db[3],dw[2],db[2],dw[1],db[1].在计算的时候,缓存会把z[1]z[2]z[3]传递过来,然后回传da[2],da[1],可以用来计算da[0],但是不会使用它。整个过程如下图所示 4.3深层网络的前向传播 (1)前向传播归纳为: 向量化实现过程: 4.4核对矩阵的维数 (1)w的维度是(下一层的维数,上一层的维数),即w[l]:(n[l],n[l-1]) (2)b的维度时(下一层的维数,1) (3)z[l],a[l]:(n[l],1) (4)dw[l]和w[l]维度相同,db[l]和b[l]维度相同,且w,b向量化维度不变,但z,a以及x的维度会向量化后发生改变。 向量化后: Z[l]:(n[l],m),A[l]同Z[l] 4.5为什么使用深层表示 增加网络的深度比广度更有效。 4.6搭建神经网络块 (1)针对一层的正向和反向传播: (2)整个过程示意图: 4.7参数VS超参数 (1)W,b是参数 (2)学习率、迭代次数、层数、每层的单元数、momentum、mini batch size、regularization perameters等能影响W、b的都称为超参数,超参数的选择需要不断尝试和靠经验,以及一些策略。 4.8深度学习和大脑的关联性 深度学习和大脑其实没什么直接关系。
3.1神经网络概述 (1)神经网络每个单元相当于一个逻辑回归,神经网络由逻辑回归的堆叠起来。下图是网络结构: 针对网络结构进行计算: 1.第一层的正向传播 2.第一层的反向传播 3.第二层的反向传播(正向只要把微分符号去掉即可) 3.2神经网络的表示 (1)神经网络各层分别较输入层、掩藏层和输出层,其中说一个网络有几层时一般不包括输入层,如下图是一个两层的网络: (2)a[0]chang也常用来表示输入特征,a[1]b表示第一层的输出,如第一层(不算输入层)有四个神经元,其输出为(用a表示是因为activation激活函数的缩写): (3)关于W[m],b[m]是和第m层输出有关的系数,W的维度(第m层单元数,上一层单元数),b的维度为(第m层单元数,1)。 3.3计算一个神经元网络的输出 (1)神经结构如下: (2)每一个神经元做的计算: (2)向量化表示下面四个式子: (3)一个输入样本,神经网络的计算 3.4多样本向量化 (1)多样本的计算示意图(a[2](1)前面的2表示第二层,后面的1表示第一个样本): (2)向量化: (3)以矩阵A为例,从水平上看,每一列对应着不同的训练样本;从垂直方向看,每一行对应着同一层的不同神经元。 3.5向量化实现的解释 (1)矩阵乘列向量得到列向量: (2)上面式子中省略了b[1],b[1]的维度与Z[1]相同,再加上python具有广播的功能,所以可以使得向量b与每一列相加。 3.6激活函数 (1)sigmoid激活函数:除了输出层是一个二分类问题基本不会用它。存在梯度消失问题,其函数表达式如下: (2)tanh激活函数:tanh是非常优秀的,可以中心化数据(-1到1),几乎适合所以场合。存在梯度消失问题,其函数表达式如下: (3)ReLU激活函数:最常用的默认函数,如果不确定用哪个激活函数,就是用ReLU(函数表达式为a=max(0,z))或则Leaky ReLU(函数表达式为a=max(0.01z,z),0.01参数可改)。ReLU在负半区梯度为零,产生所谓的稀疏性,但由于有足够多的掩藏层是z大于0,所以学习过程还是非常的快。 (4)下面的四种激活函数的图像: 3.7为什么需要非线性激活函数 (1)如果没有非线性激活函数,那么无论网络有多少层,输出始终是输入的线性组合,与一层网络毫无区别。举例如下: (2)有时候输出可能会用到线性激活函数。 3.8激活函数的导数 3.9神经网络的梯度下降 (1)正向传播四个式子: (2)反向传播六个式子(下面公式3.3.2中应该是dz[2]): 3.10(选修)直观理解反向传播 (1)主要推导过程: 3.11随机初始化 (1)W不能初始化为零否则一层中每个单元都做相同的计算,和一个单元没什么区别,b可以初始化为零。可按照如下方式初始化(0.01的作用是时输出不会太大,太大由由sigmoid、tanh激活函数是将会导致梯度特别小):
2.1二分类 (1)以一张三通道的64×64的图片做二分类识别是否是毛,输出y为1时认为是猫,为0时认为不是猫: y输出是一个数,x输入是64*64*3=12288的向量。 (2)以下是一些符号定义(数据集变成矩阵之后进行矩阵运算代替循环运算,更加高效) x:表示一个nx维数据,维度为(nx,1) y:表示输出结果,取值为(0,1); (x(i),y(i)):表示第i组数据; X=[x(1),x(2),……,x(m)]:表示按列将所有的训练数据集的输入值堆叠成一个矩阵;其中m表示样本数目; Y=[y(1),y(2),……,y(m)]:表示所有输入数据集对于的输出值,其维度为1×m; 2.2逻辑回归 (1)逻辑回归的输出值是一个概率,算法思想如下: (2)激活函数使用sigmoid,它使得输出值限定在0到1之间,符合概率的取值。 (3)关于偏置项(偏差)b,可将其变成θ0,对应的x0恒定为1,如下所示: 2.3逻辑回归的代价函数 (1)损失函数(针对单个样本): (2)代价函数(针对全部训练样本): 2.4梯度下降法 (1)下图中左边为凸函数,右边为非凸函数,逻辑回归中代价函数为凸函数,故任意的初始化都能收敛到最优点: (2)参数w、b的更新方式: 2.5导数 导数即斜率。 2.6跟多的导数例子 记住一些常见的导数求法或者直接查看导数表。 2.7计算图 (1)下图展示计算图计算的过程: (2)正向传播用于计算代价函数 2.8计算图的导数计算 (1)反向传播利用链式法则来进行求导,如对a进行求导,其链式法则公式为: 2.9逻辑回归中的梯度下降 针对于单个样本 (1)计算图如下: (2)首先计算da: (3)然后计算dz: (4)最后计算dw,db(下面的式子其实已经对所有样本进行的求导): 2.10m个样本的梯度下降法 (1)以下代码显示了对整个数据集的一次迭代 (2)以上过程会有两个循环,一个循环是循环是遍历样本,第二个循环是当w很多时是要循环的,上面之写出了两个w,所以没体现出来。 2.11向量化 (1)使用循环的方式计算:ωTx (2)使用向量的方式 后者不仅书写简单,更重要的是计算速度可以比前者快特别多。 2.12向量化的更多例子 (1)消除w带来的循环 设置u=np.zeros(n(x),1)来定义一个x行的一维向量,从而替代循环,仅仅使用一个向量操作dw=dw+x(i)dz(i),最后我们得到dw/m。 2.13向量化逻辑回归 (1)将样本x横向堆叠,形成X,同时根据python的广播性质(把实数b变成了(1,m)维),得到: (2)继续利用Python的计算方法,得到A: 2.14向量化logistic回归的梯度输出 (1)没有用向量化时使用的代码: (2)使用向量化之后的代码: 其中前面五个式子完成了前向和后向的传播,也实现了对所有训练样本进行预测和求导,再利用后两个式子,梯度下降更新参数。另外如果需要多次迭代的话,还是需要用到一个循环的,那是避免不了的。 2.15Python中的广播 (1)下图形象的总结了Python中的广播 (2)在Python的numpy中,axis=0是按照列操作,axis=1,是按照行操作,这一点需要注意。 2.16关于python_numpy向量的说明 (1)使用a=np.random.randn(5)生成的数据结构在python中称为一维数组,它既不是行向量也不是列向量,用a.shape的结果是(5,)这表示它是一个一维向量,a和它的转置相乘其实得到的是一个数。 (2)应该使用a=np.random.randn(5,1)这样生成的是一个行向量,它和他的转置乘积会是一个矩阵: 2.17Jupyter/iPython Notebooks快速入门 2.18(选修)logistics损失函数的解释 (1)首先需要明确,逻辑回归的输出表示y等于1的概率。故有: (2)合并成一个式子(要使得式子越大越好): (3)根据对数函数log的单调递增性,对上式取对数有: (4)要最大化上式,最小化上式取反,得到一个样本的损失函数。 (5)所有样本时,认为样本间独立同分布,故联合概率就是每个样本的乘积: (6)两边取对数得到: (7)要最大化上式(最大似然估计)也就是最小化: 总结一下:为了最小化成本函数J(w,b),我们logistic回归模型的最大似然估计的角度出发,假设训练集中的样本都是独立同分布的条件下。
1.1欢迎 主要讲了五门课的内容: 第一门课:神经网络基础,构建网络等; 第二门课:神经网络的训练技巧; 第三门课:构建机器学习系统的一些策略,下一步该怎么走(吴恩达老师新书《Machine Learning Yearning》就是针对这个以及上一课); 第四门课:卷积神经网络相关; 第五门课:循环神经网络相关。 1.2什么是神经网络 (1)常说的深度学习指的就是训练神经网络,或者也指特别大规模的神经网络。 (2)每一个神经元都代表着从输入到输出的函数映射,如下的房价预测: (3)激活函数Relu(Rectified Linear Unit)其实就是max(0,x)。 (4)神经网络非常擅长计算从x到y的精确映射函数(个人理解:神经网络实质就是非线性的多项式拟合),神经网络的输入单元个数一般是特征个数,中间称为隐藏层,然后输出单元个数依据实际情况而定,如下输出是房价的预测值,故是一个神经元。 1.3神经网络的监督学习 (1)神经网络在监督学习上的应用: (2)数据包括结构化数据和非结构化数据,图像语言语音都是非结构化数据,是神经网络要研究解决的重点。 1.4为什么深度学习会兴起 (1)三点原因:数据规模大、计算速度提高、算法的创新。事实上如今提高性能最可靠的方法就是运用更大的神经网络和投入更多的数据。下图展示了数据量、模型大小与性能之间的关系: (2)算法创新的一个小案例:激活函数从sigmoid(存在梯度消失)变成ReLU,训练的速度变得更快了。 (3)在实践应该按照下图方式进行快速迭代: 1.5关于这么课 总共四周,分别是前言,预备知识,浅层神经网络和深层神经网络。 1.6课程资源
(1)涉及到的算法 1.监督学习:线性回归,逻辑回归,神经网络,SVM。 线性回归(下面第三行x0(i)其实是1,可以去掉) 逻辑回归 神经网络(写出前向传播即可,反向框架会自动计算) SVM 2.非监督学习:聚类算法(K-mean),降维(PCA) K-mean PCA 3.异常检测 4.推荐系统 (2)策略 1.偏差与方差,正则化 训练误差减去人类最高水平为偏差(欠拟合),交叉验证集误差减训练误差为方差(过拟合); 正则化解决方差问题,不对θ0正则化; 2.学习曲线 全过程观测偏差与方差,所以更全面。 3.误差分析 找到哪种原因造成误差最大,最该花时间的地方。 4.评价方法 尽量使用单一指标评价,准确率不适合类偏斜,用精确度和召回率判定 精确度是预测的视角(预测为正样本中有多少是正样本),召回率是样本视角(正样本有多少被预测到了) F1=2(PR)/(P+R) 5.数据集的拆分 训练集用于训练模型,,交叉验证集用于筛选模型/调参,测试集用来做最终评价。 6.上限分析 每一步假设输出完全正确时,能提高多少的正确率,提高最高的地方就是最该马上花时间解决的地方。 (3)应用 1.OCR 检测,分割,识别,现在常常不分割了,直接序列化识别。 2.大规模的机器学习 小批量的训练方法以及使用并行计算。
16.1问题形式化 (1)讲推荐系统的原因主要有以下几点: 1.推荐系统是一个很重要的机器学习的应用,虽然在学术界上占比较低,但是在商业应用中非常的重要,占有很高的优先级。 2.传达机器学习的一个大思想:特性是可以学习而来的,不需要人工去选择。 (2)说明的案例:电影推荐系统 希望创建一个算法来预测每个人可能会给他们没看过的电影打多少分,并以此作为推荐依据。 (3)此外引入一些标记: nu代表用户的数量, nm代表电影的数量, r(i,j)如果用户j给电影i评过分则r(i,j)=1, y(i,j)代表用户j给电影打的分数, mj表示用户评分的电影的总数。 16.2基于内容的推荐系统 (1)总结:基于内容其实就是已经有了电影的特征X,然后求拟合的参数θ,后面提到的基于用户,则是已经有了参数θ,来求拟合的电影特征X。 (2)假设每部电影已知特征(基于内容): 参数说明:θ(j)表示用户j的参数,x(i)表示电影i的特征, 对于用户j和电影i,我们预测评分为:(θ(j))Tx(i) 对于单用户的代价函数(省略了样本数m,对θ0不做正则化,只计算有评分的)如下: 故对于所有用户的代价函数为: 梯度下降式的梯度更新方式: 16.3协同过滤 (1)基于用户的(即已知用户的参数θ,求电影特征x),其代价函数为: (2)协同过滤算法是既不知道特性X,也不知道用户参数θ时同时对二者进行优化。 其代价函数为: 对代价函数求偏导数: (3)协同过滤的算法步骤: 1.初始化x(1),x(2),……,x(nm),θ(1),θ(1),……,θ(nu)为一些随机小值; 2.使用梯度下降算法最小化代价函数; 3.在训练完算法后,我们预测(θ(j))Tx(i)为用户j给电影i的评分。 (4)如何给用户推荐: 1.根据计算出来的评分,把该用户评分高的电影给该用户; 2.如果用户观看某电影,根据计算电影特征间的相似度,推荐相似的电影给该用户。 16.4协同过滤算法 16.5向量化:低秩矩阵分解 将数据集评分存储在矩阵中->通过协同过滤学习得到元素为(θ(j))Tx(i)的预测矩阵->根据电影特征距离求电影间的相似性 16.6推行工作的细节 总结:怎么给新用户推荐电影(会把每部电影的平均分作为该用户的评分) (1)用户评分数据以及新用户Eve: (2)对每部电影做均值归一化,然后作为数据来训练模型 (3)预测的值加上该电影的均值为最终对电影的评分: (4)学习到的模型会把每部电影的平均分作为新用户对电影的评分。
15.1问题的动机 将正常的样本绘制成图表(假设可以),如下图所示: 当新的测试样本同样绘制到图标上,如果偏离中心越远说明越可能不正常,使用某个可能性阈值,当低于正常可能性阈值时判断其为异常,然后做进一步的检查。异常检测常用于工业生产、异常用户等实际场景中。 以上这种方法叫密度评估: 15.2高斯分布 (1)高斯分布也称为正态分布,其记为: (2)高斯分布的概率密度函数: 其中均值和方差的计算公式: 均值影响水平移动;方差越大,分布越矮胖,方差越小,分布越瘦高。 (3)在求均值方差是到底用1/m还是1/(m-1)不做深究,二者差别很小(除非数据样本特别少),机器学习上习惯用前者。 15.3算法 (1)首先求出每个特征的均值和方差: (2)获取新数据之后根据模型计算密度(注意此处算的是密度,而不是概率): (最后一项应该把1改成n) (3)根据设定的判断边界,当p(x)小于判断边界是则判别为异常。 以下的三维图是表示密度估计函数: 15.4开发和评价一个异常检测系统 (1)异常检测是一个非监督学习,故不可以根据结果变量y的值来高斯我们数据是否真的是异常。 (2)异常检测系统开发的方法:从带有标记(正常和异常)的数据着手,选择部分正确数据集构建模型,然后剩余正常和异常构成交叉验证集和测试集,交叉验证集作为选取阈值ε 案例:10000台正常的引擎数据,20台异常引擎数据,分配如下: 6000台正常作为模型构建 2000台正常和10台异常作为交叉验证集 2000台正常和10台异常作为测试集 具体评价方法如下: 1.根据训练集数据,我们估计特征的平均值和方差并构建p(x)函数; 2.对交叉验证集,尝试用不同的ε值作为阈值,并预测数据是否异常,根据F1值或者查准率与查全率的比例来选择ε; 3.选出ε后,针对测试集进行预测,计算异常检测系统的F1值,或者出准率与查全率之比。 15.5异常检测与监督学习的对比 通常来说,正例(异常)样本太少,甚至为0,也就是说,出现了太多没见过的不同的异常类型,对于这些问题,通常应该使用的算法是异常检测算法。 15.6选择特征 (1)异常检测是假设特征符合正态分布(不是当然也能用,但不好),故需要将非正态分布的特征转换成正态分布,例如使用对数函数x=log(x+C),其中C是非负常数,常用1;或者x=xc,c为0-1之间的一个分数。下图就是一个通过对数转换得到的正态分布 (2)误差分析:一个常见问题是一些异常的数据可能也会有较高的p(x)值,因而被认为是正常的,这种情况下可以做误差分析,从中找到一些新特征,是异常的p(x)变小。如下图中一个异常样本在一个特征中p(x)值很大,然后寻找其他特征,使其p(x)变小。 通常可以通过一些相关特征的组合获得很好的新特征,如在检测数据中心的计算机状况,使用CPU的负载与网络通信的比例作为新的特征,该值异常大时意味着出现问题。 15.7多元高斯分布(选修) (1)当特征之间具有相关性时,原来的高斯分布可能无法正确的边界(当然通过特征组合成新特征可以一定的解决该问题),如下图紫色的线是原来的高斯分布,蓝色的线是多元高斯分布: (2)原来的高斯分布计算过程: 多元高斯分布计算过程(计算均值、协方差、概率密度函数): (3)协方差矩阵的影响: (4)原高斯分布模型(特例)与多元高斯分布模型(一般)的比较: (5)特征之间具有相关性时,解决方法有二,其一通过 多元高斯分布,其二通过特征组合形成新特征。 15.8使用多元高斯分布进行异常检测(选修): 简要的讲就是先用数据集计算均值和协方差,然后计算p(x),利用测试数据带入到p(x)中求得的值与阈值作比较,小于阈值则判断为异常。
18.1问题描述和流程图 (1)图像文字识别是从给定的一张图片中识别文字。 (2)流程包括: 1.文字侦测 2.字符切分(现在不需要切分了) 3.字符分类 18.2滑动窗口 在行人检测中,滑动窗口是首先训练一个固定尺寸输入的判断是否有行人的网络,然后在一张图片中裁该尺寸的图片,送入到网络中;然后不断移动裁剪区,重复以上过程,知道裁剪到最后,这时按比例放大裁剪区,然后将裁剪到的图片缩放到网络的输入,如此循环。 首先滑动窗口同样用于文字识别,做字符与非字符区分,然后把字符区域适当扩展,然后合并重叠区域,按照高宽比进行过滤(认为长度大于高度),如下图所示: 然后进行文字的分割,通用训练一个模型,数据集如下: 分割出单个字符之后,利用神经网络、支持向量机或者逻辑回归训练一个分类器即可。 18.3获取大量数据和人工数据 (1)从网上下载字体,然后随机添加跟着背景创造实例; (2)利用已有数据进行旋转、扭曲、模糊处理等产生新数据; 有关获取更多数据的方法: (1)人工数据合成; (2)手动收集、标记数据; (3)众包; 18.4上限分析:哪部分管道该接下去做 如下下面的流程中,本来正确率为72%,如果提供完全正确的文字检测作为文字分割的输入,发现系统正确率提升到了89%,说明要下功夫在文字检测上了。 下表是每一步如果完全正确,会带来多大的提升,如果提升越大,说明越要花功夫在这一步上。下表首先要花功夫在文字检测上,然后是文字识别,而文字分割已经做得很好了。
14.1动机一:数据压缩 将特征进行降维,如将相关的二维降到一维: 三维变二维: 以此类推把1000维数据降成100维数据。 14.2动机二:数据可视化 如50个维度的数据是无法进行可视化的,使用降维的方法可以使其降到2维,然后进行可视化。 降维的算法只负责减少维度,新产生的特征的意义就必须有我们自己去发现了。 14.3主成分分析问题 (1)主成分分析的问题描述:问题是要将n维数据降至k维,目标是找到k个向量,使得总的投射误差最小。 (2)主成分分析与线性回归的比较: 二者是不同的算法,前者是最小化投影误差,后者是最小化预测误差;前者不做任何分析,后者目的是预测结果。 线性回归是垂直于轴投影,主成分分析是垂直于红线的投影。如下图所示: (3)PCA是对新求出来的“主元”向量的重要性进行排序,根据需要去前面重要的部分,将后面的维数省略。 (4)PCA的一个优点是完全依赖数据,而不需要人为设定参数,与用户是独立的;同时这也是也可以看做缺点,因为,如果用户对数据有一定的先验知识,将无法派上用场,可能得不到想要的效果。 14.4主成分分析算法 PCA将n维减少到k维: (1)均值归一化,即减均值除以方差; (2)计算协方差矩阵; (3)计算协方差矩阵的特征向量; 对于一个n x n维度的矩阵,上式中的U是一个具有与数据之间最小投影误差的方向向量构成的矩阵,只需要去前面的k个向量获得n x k维度的向量,用Ureduce表示,然后通过如下计算获得要求的新的特征向量z(i)=UTreduce*x(i)。 14.5选择主成分的数量 主成分分析是减少投射的平均均方误差,训练集的方差为: 希望可以尽可能的减少二者的比值,比如希望二者的比值小于1%,选择满足这个条件的最小维度。 14.6重建的压缩表示 降维式子: 重建(即从低维回到高维): 示意图如下所示:左图是降维,右图是重建。 14.7主成分分析法的应用建议 正确使用案例: 100 x 100像素的图片,即1000维特征,采用PCA将其压缩至1000维,然后对训练集运行学习算法,在预测时,对测试集采用之前学到的Ureduce将测试集的x转换成z,再进行预测。 错误使用情况: (1)尝试用PCA来解决过拟合,PCA是无法解决过拟合的,应该用正则化来解决。 (2)默认把PCA作为学习过程的一部分,其实应该尽量使用原始特征,只有在算法运行太慢或者占用内存太多的情况下才考虑使用主成分分析法。
13.1无监督学习:简介 将没有标签的样本分成不同的集合(簇),这种算法叫做聚类。常用的领域有市场分割、社交网络分析、计算机集群管理、了解星系等。 13.2K-均值算法 (1)K-均值是最普及的聚类算法,是一种迭代算法,假设需要将数据聚类成n个组,这时候首先随机选择K个点,称为聚类中心。 将每个样本归属到最近的聚类中心,然后重新计算每个类的中心变成新的聚类中心,重复以上步骤,直到聚类中心不变。 伪代码如下: 13.3优化目标 k-均值的最小化问题,就是每个样本点到对应聚类中心的距离之和: 与其他算法不同的是,k-均值每一次迭代都会是代价函数变小。 13.4随机初始化 (1)K应该小于样本数m; (2)从样本中随机选取K个实例作为初始聚类中心。 K-均值可能会出现局部最小的情况,如下所示: 解决方案:多次运行该算法,最后在比较K-均值代价函数最小的结果,这种方法适用于K取较小的时候(2-10),K太大没有明显效果。 13.5选择聚类数 绘制聚类数与代价函数的图,然后选取出现斜率突然变小的地方的值(“肘部法则”)。
12.1目标优化 (1)以下是逻辑回归以及单个样本的代价函数 (2)首先将使用上图中紫色的线(称为cost1或者cost0)的代替曲线,然后将样本数m去掉,最后将C代替1/λ(可以这么理解,但不完全是),从而实现逻辑回归的代价函数到SVM的转换。 (3)SVM的输出将不再是逻辑回归的概率,而就是0或者1: 12.2大边界的直观理解 (1)首先对z的要求更加严格了,在逻辑回归中只要求大于或小于零,,这里将会是大于等于1或小于等于-1。 (2)假设C非常大时,我们的优化会尽量时第一项为零,假设可以得到这样的参数,那么就可以将代价函数转换为: 即在后面的约束下求解前面的最小值。 (3)C非常大时(即λ非常小),会尽量去满足上面的约束,这样会导致对异常点非常敏感(过拟合),如下所示: 这时将会得到紫色的线,如果将C适当减小,会得到满意的黑色线的。即C不那么大时,可以忽略掉一些异常点。 (3)支持向量机经常被称为最大间距分类器,在C很大时确实如此,但C不是那么大时,将不是,如上一点的例子所示。但是这么理解是有助于理解SVM的。 (4)C较大相当于λ较小,会出现过拟合;反之则出现欠拟合。 12.3数学背后的最大边界分类(选修) (1)向量的内积:一个向量投影到另一个向量投影长度与向量的范数的乘积,也就是对应坐标相乘再相加。 (2)目标函数要使得θ尽可能小,这时只要使得x在θ上的投影尽可能的大,就能够在θ取越小的值时满足约束条件,这就是SVM背后的数学原理。 (3)θ和边界呈现90°垂直,另外θ0为零时边界通过原点,反之不通过原点。 12.4核函数1 (1)如果直接用多项式取拟合下面的边界的话,肯能需要多项式的次数很高,特征很多。 (2)利用x的各个特征与我们预先选定地标(landmark)l(1),l(2),l(3),的近似程度选取新的特征f1,f2,f3。 上面是一个高斯核函数,注:这个函数与正态分布没什么实际上的关系,只是看上去像而已。 (3)与地标越近结果f越接近1,越远f越接近0。 (3)通过一下式子将很容易进行分类: (4)核函数计算的结果即为新的特征。 12.5核函数2 (1)地标的个数设置为样本数m,即每个样本的位置即为地标的位置: (2)将核函数运用到支持向量机中, 给定x,计算新特征f,当θTf>0时,预测y=1,否则反之。 相应的修改代价函数为: 在具体实施过程中,还需要对最后的正则化想微调,在计算时,用θTMθ代替θTθ。M跟选择的核函数有关,用相关库几块使用带核函数的SVM。 不带核函数的SVM称为线性核函数。 (3)以下是支持向量机的两个参数C和σ的影响: C=1/λ; C较大时,相当于λ较小,可能会导致过拟合,高方差; C较小时,相当于λ较大,可能会导致欠拟合,高偏差; σ较大时,可能会导致低方差,高偏差。 σ较小时,可能会导致低偏差,高方差。 12.6使用支持向量机 (1)尽管不需要自己去写SVM函数,直接使用相关库,但需要做一下几件事: 1.是提出参数C的选择。在之前视频中已经讨论了C对方差偏差的影响。 2.选择内核参数或你想要使用的相似函数。 (2)以下是逻辑回归和支持向量机的选择: 1.相比于样本数m,特征数n大的多的时候,没有那么多数据量去训练一个非常复杂的模型,这时考虑用SVM。 2.如果n较小,而且m大小中等,例如n在1-1000之间,而m在10-1000之间,使用高斯函数的支持向量机。 3.如果n较小,而m较大,例如n在1-1000之间,而m大于50000,则使用支持向量机会非常慢,解决方案是创造增加更多的特征,然后使用逻辑回归或不带核函数的支持向量机。 神经网络在以上三种情况下都可以有较好的表现,但神经网络训练可能非常慢,选择支持向量机的原因主要在于它的代价函数是凸函数,不存在局部最小值。
11.1首先要做什么 本章将在随后的课程中讲误差分析,然后怎样用一个更加系统性非方法,从一堆不同的方法中,选取合适的那一个。 11.2误差分析 构建一个学习算法的推荐方法为: (1)从一个简单的能快速实现的算法开始,实现该算法并用交叉验证集数据测试这个算法; (2)绘制学习曲线,决定是增加更多数据,或者添加更多特征,还是其他选择; (3)进行误差分析:人工检查交叉验证集中我们算法中产生预测误差的实例,看看这些实例是否有某种系统化的趋势。 在交叉验证集上做误差分析,不要在测试集在做误差分析。 1.3类偏斜的误差度量 举例:恶性肿瘤的概率只有0.5%,这时如果不用神经网络全部预测为两性,其误差为0.5%,而用神经网络可能的出来的误差为1%,显然通过误差率来作为系统好坏的判别标准是不好的,在这种类偏斜的问题中,这时候需要用到精确度(precision,又称查准率)和召回率(recall,又称查全率)。 (1)精确度(precision):是从预测的视角看,真正为真与预测为真的比值即TP/(TP+FP)。 (2)召回率(recall):是从样本的视角看,被预测出来的正例与总正例的比值即TP/(TP+FN)。 11.4查准率和查全率之间的权衡 (1)查全率与查准率的关系: (2)为了有一个单一的指标定义了F1值: 11.5机器学习的数据 有大量的数据(避免了方差),以及足够多的特征(避免了偏差),一般可以得到一个高性能的算法。
10.1决定下一步该干什么 当系统的效果很差时,你可能考虑到收集更多的样本,也可能: (1)尝试减少特征的数量; (2)尝试获得更多的特征; (3)尝试增加多项式特征; (4)尝试减少正则化程度λ; (5)尝试增加正则化程度λ。 如果做决策将是本章的内容。而不是盲目的选择一种策略。 10.2评估一个假设 将数据集分为训练集和测试集,在测试集上计算误差: (1)对于线性回归模型,我们利用测试集数据计算代价函数J; (2)对于逻辑回归模型,不仅可以利用测试集计算代价函数外,还可以利用误分类的比率来计算结果: 10.3模型选择和交叉验证集 将数据集按照8:2:2分为训练集,交叉验证集和测试集。 模型选择的方法: (1)使用训练集训练处10个模型; (2)用10个模型分别对交叉验证集计算得出交叉验证集误差; (3)选取 代价函数数值最小的模型; (4)用步骤3中选出的模型对测试集计算得出推广误差。 10.4诊断偏差和方差 (1)偏差(欠拟合)、方差(过拟合) (2)误差随多项式次数的关系 次数低时,训练误差和验证误差都大,欠拟合;次数高时,训练误差小,验证误差大,过拟合。 训练误差和验证误差相近时,欠拟合;验证误差高于训练误差时过拟合。 10.5正则化和偏差/方差 (1)正则化的影响: (2)λ的选择(以2为倍数增加,如0,0.01,0.02,0.04,0.08,0.16,0.32……): 1.使用训练集训练出12个不同程度的正则化模型; 2.用12个模型分别对交叉验证集计算出交叉验证误差; 3.选择得出交叉验证误差最小的模型; 4.运用步骤3中选出模型对测试集计算得出推广误差。 (3)训练误差和验证误差与λ的关系: 10.6学习曲线 (1)学习曲线是将训练误差和交叉验证集误差作物训练样本数量(m)的函数绘制的图表: (2)训练误差很大,高偏差(增加数据不会有改观) (3)验证集误差与训练集误差相差很大,高方差(增加数据可以提高算法效果) 10.7决定下一步做什么 通过以上的诊断,下面是一些策略: (1)获得更多的训练实例——解决高方差 (2)尝试减少特征的数量——解决高方差 (3)尝试获得更多的特征——解决高偏差 (4)尝试增加多项式——阶段高偏差 (5)尝试减小正则化程度——解决高偏差 (6)尝试增加正则化程度——解决高方差 一般使用较大的网络加上正则化会比使用小网络更有效。
9.1代价函数 (1)假设神经网络的训练样本有m个,每一个包含一组输入x和一组输出信号y,L表示神经网络的层数,Sl表示每一层的神经元个数,SL代表最后一层中处理单元的个数。 则代价函数为(同样不对θ0正则化): 9.2反向传播算法 前向传播算法: 用δ表示误差,则δ(4)=a(4)-y 前一层的误差为: 再前一层的误差为: 。 输入层不存在误差。 每一层有了误差之后,即可分别进行求偏导,然后更新θ。 9.3反向传播算法的直观理解 略 9.4实现注意:展开参数 略 9.5梯度检验 用某点领域的两个点的连线的斜率作为该点的估算值,然后用该值与神经网络计算出来的值作比较。 9.6随机初始化 参数的初始化应该随机的,如果是相同的值的话,第二层的所有激活单元都会有相同的值,后面也类似。 9.7综合起来 使用神经网络时的步骤: (1)网络结构:第一件要做的事是选择网络结构,即决定选择多少层以及决定每层分别有多少单元。 第一层的单元数即为我们训练集的特征数量。 最后一层的单元数是我们训练集的结果的类的数量。 (2)训练神经网络: 1.参数的随机初始化; 2.利用正向传播方法计算所有的hθ(x); 3.编写计算代价函数J的代码; 4.利用反向传播方法计算所有的偏导数; 5.利用数值检验方法检验这些偏导数; 6.使用优化算法来最小化代价函数。 9.8自动驾驶 略。
8.1非线性假设 (1)无论线性回归还是逻辑回归当特征量太多时,计算的负荷会非常大。如50x50像素有2500特征,如果两两组合将会有25002/2个(接近300万个特征)。普通的线性回归和逻辑回归模型不能有效处理这么多特征,这时候需要用神经网络了。 8.2神经元和大脑 大脑的某一块可以经过学习,学会其他功能,比如某一块感受触觉,但是接受视觉训练之后,能够感受视觉。 8.3模型表示1 (1)神经元有树突和轴突,许多树突接受电信号,一个轴突发送电信号。 (2)根据神经元模型,创建逻辑回归模型: (3)多神经元、多层时,分别称为输入层,掩藏层,输出层: 8.4模型表示2 (1)向量表示比循环编码更高效: 以上只是针对一个训练实例,如果是整个训练集进行计算的话,需要将X进行转置,使得同一个实例在一列。 (2)神经网络比线性回归和逻辑回归更强大,在于前者将特征不断进行高级化。 8.5特征和直观理解1 (1)从本质上讲,神经网络能够通过学习得出其自身的一系列特征。 (2)逻辑与运算 其中θ为[-30,20,20]T. (3)逻辑或运算 其中θ为[-10,20,20]T。 (4)非运算 8.6样本和直观理解II 通过上一节的简单运算构造出复杂运算(同为1或者同为0时取1) 按照这种方法我们可以逐步构造出越来越复杂的函数,也能得到更加厉害的特征值。这就是神经网络厉害之处。 8.7多类分类 如要在一个图片中识别路人,汽车,摩托车,卡车四类,这是神经网络可以设置四个输出,每一个用1或0代表是否有某一类即可。
7.1过拟合的问题 训练集表现良好,测试集表现差。鲁棒性差。以下是两个例子(一个是回归问题,一个是分类问题) 解决办法: (1)丢弃一些不能帮助我们正确预测的特征。可以使用工选择保留哪些特征,或者使用一些模型选择的算法来帮忙(PCA); (2)正则化。保留素有的特征,但是减少参数的大小。 7.2代价函数 其中λ称为正则化参数。 经过正则化处理的模型和原模型的可能对比如如下: 不对θ0正则化。 7.3正则化线性回归 对于j=1,2,3……有: 可以看出,正则化线性回归的梯度下降法的变化在于,每次都会在原有算法的更新规则的基础上令θ值减少了一个额外的值。 7.4正则化的逻辑回归模型
6.1分类问题 回归问题的输出可能是很大的数,而在分类问题中,比如二分类,希望输出的值是0或1,如何将回归输出的值转换成分类的输出0,1成为关键。 6.2假说表示 其中: hθ(x)的作用是,对于给定的输入变量,根据选择的参数计算输出变量=1的可能性即hθ(x)=P(y=1|x;θ)。 6.3判定边界 g(z)中的z即为判定边界,如下 6.4代价函数 如果用之前回归时用的平方损失函数,代价函数将是非凸函数,会收敛到局部最优,而不是全局最优。 定义新的代价函数: 求导结果: 虽然式子看上去与回归的相同,但是hθ(x)实际定义不一样,所以二者是两回事。 6.5简化的成本函数和梯度下降 6.6高级优化 共轭梯度法、变尺度法、限制变尺度法。 6.7多类别分类:一对多 将某一类分为一类,剩余的其他类分为另一类,如下所示: 得到多个分类器,取分值最高的分类器作为判别:
推荐使用python,本节略。
4.1多维特征 上图中列数即为特征的个数,行数是样本数。函数假设如下: 其中x0=1。 4.2多变量梯度下降 和单变量的损失函数相同: 其中, 求导迭代如下: 4.3梯度下降法实践1-特征缩放 特征之间的尺度变化相差很大(如一个是0-1000,一个是0-5),梯度算法需要非常多次的迭代才能收敛,如下图所示: 方法:将各个特征缩放至大致相同的尺度,最简单的方法就是特征减去均值除以方差。如下所示: 4.4梯度下降法实践2-学习率 学习率过小收敛慢,学习率过大可能导致无法收敛。 通常通过三倍放大来考虑学习率的设置,比如:0.01,0.03,0.1,0.3,1,3,10……。 4.5特征和多项式回归 比如一个二次模型: 或者三次模型: 可以通过创建新特征(即令): 从而将模型转换成线性模型。 4.6正规方程 前提:对于某些线性回归问题,使用正规方程求解一步到位(导数为零等式求解)。如下所示 直接令 。 参数的解直接为: (X包含x0=1)。 梯度下降与正规方程的比较: 4.7正规方程及不可逆性: (1)特征之间互相不独立时不可逆; (2)样本数少于特征数时不可逆。
3.1矩阵和向量 几行几列即为矩阵。Aij表示第i行第j列。 只有一行或者一列的称为向量,向量是一种特殊矩阵。一般向量指的是列向量。 3.2加法和标量乘法 加法:元素对应相加。 标量乘法:标量和矩阵每一个元素相乘。 3.3矩阵向量乘法 3.4矩阵乘法 要求:第一个矩阵的列数等于第二个矩阵的行数,如m x n矩阵与nx 1矩阵相乘,结果为第一个矩阵的行数乘以第二个矩阵的列数。 结果Cij是第一个矩阵第i行和第二个矩阵第j列对应元素相乘求和的值。 3.5矩阵乘法的性质 不满足交换律:AxB != B x A。 满足结合律:(A x B) x C=A x (B x C)。 单位矩阵I:是对角线为1,其他都为零的方阵。任何矩阵于单位矩阵相乘,矩阵保持不变。 3.6逆、转置 如果矩阵A的逆矩阵存在,则AA-1=A-1A=I。 如果A的转置矩阵是B,则A矩阵第i行第j列元素与B矩阵第j行第i列元素相等。记AT=B。 转置矩阵的一些性质: (A±B)T=(AT±BT)。 (AxB)T=BTx AT。 (AT)T=A。 (KA)T=KAT。
2.1模型表示 (1)监督学习中的回归问题案例房价预测 (2)监督算法的工作方式 案例中:m表示训练集的数量,x代表特征/输入变量,y代表目标变量/输出变量,(x,y)代表实例,(x(i),y(i))代表第i个观察实例,h代表假设/函数/输入到输出的映射。 (3)房价预测的一种表达方式:h(Θ)=Θ0+Θ1x,只有一个变量,所以成为当变量线性回归问题。 2.2代价函数 (1)对于回归问题常用的代价函数是平方误差代价函数: 我们的目标选取合适的参数Θ使得误差函数最小,即直线最逼近真实情况。 2.3代价函数的直观理解I 2.4代价函数的直观理解II 2.5梯度下降 需要注意:参数是要同时更新的;不要算出一个倒数更新一个倒数,再用更新后的式子去计算其他倒数,这样是不对的。 其中α叫学习率,表示沿着下降程度最大的方向迈出的步子大小。 2.6梯度下降的直观理解 (1)梯度下降法可以最小化任何代价函数,而不仅仅局限于线性回归中的代价函数。 (2)当越来越靠近局部最小值时,梯度值会变小,所以即使学习率不变,参数变化的幅度也会随之减小。 (3)学习率过小时参数变化慢,到达最优点的时间长,学习率大时,可能导致代价函数无法收敛,甚至发散。 (4)梯度就是某一点的斜率。 2.7梯度下降的线性回归
1.1欢迎 1.2机器学习是什么 (1)一种机器学习的定义:一个程序被认为能从经验E中学习,解决任务T,达到性能指标度量值P,当且仅当,有了经验E后,经过P评判,程序在处理T时的性能有所提升。 (2)机器学习算法主要分为监督学习和非监督学习。监督学习是我们将教计算机如何去完成任务,非监督学习是我们打算让计算机它自己去学习。此外还有强化学习和推荐系统等其他机器学习。 (3)本课程主要内容是监督学习、非监督学习、了解应用机器学习算法的实用建议。 1.3监督学习 (1)监督学习基本思想是我们数据集中的每个样本都有相应的“正确答案”(有标签)。再根据这些样本做出预测,像房子和肿瘤的例子。 (2)监督学习分为回归问题和分类问题,前者如房价的预测,将房价的一系列实数值看成是连续的,后者如肿瘤预测,分为良性和恶性两种类别,其取值看成是离散的。 回归问题 分类问题 1.4无监督学习 (1)无监督学习样本没有标签(“无正确答案”),无监督学习算法可能会把没有标签的数据分成不同的簇,这种算法较聚类算法。 (2)一些常见的聚类算法应用:新闻分类、基因学的理解应用、组织大型计算机集群、社交网络分析、市场分割、天文数据分析等。
论文全名:Detecting Text in Natural Image with Connectionist Text Proposal Network 1.摘要 (1)本文提出新型网络CTPN,用于自然图像中的文本行定位。CTPN直接在卷积特征映射中的一系列细粒度文本提议中检测文本行。(创新一)开发了一个垂直锚点机制,联合预测每个固定宽度提议的位置和文本、非文本的分数。(创新二)序列提议通过循环神经网络自然连接起来,该网络无缝的结合到卷积网络中,从而形成可训练的端到端模型。 2.引言 (1)图像文字检测的应用:图像OCR、多语言翻译、图像检索等。包括检测和识别两个任务,本文聚焦检测任务。由于文本模式的大变化以及背景的高度杂乱,使得检测任务一般比文字识别任务难度更大。 (2)传统使用自下而上的方式,从低级别字符和笔画检测开始,步骤繁琐,现在普遍被神经网络所代替,无需自行查找特征。 (3)目前主流的方法Faster-RCNN虽然用于一般目标检测效果良好,但是用在文本检测上并不令人满意。第一:主要由于文本的长度往往都是难以固定,不像一般物体一般都是有相对较固定额边界框;第二:一般物体IOU>0.5可能就可以识别出物体的种类,而文字识别需要更精确的IOU,因为仅仅大于0.5可能根本无法识别出文字。 3.贡献 图一:(a)连接文本提议网络(CTPN)的架构。首先通过VGG16的最后一个卷积映射(conv5)密集的滑动3*3空间窗口。每行的序列窗口通过双向LSTM(BLSTM)循环连接,其中每个窗口的卷积特征(3*3*C)被用作256维的BLSTM(包括两个128维的LSTM)的输入。RNN层连接到512维的全连接层,接着是输出层,联合预测k个锚点的文本、非文本分数,y轴坐标坐标(包括坐标和高度)和边缘调整偏移。(b)CTPN输出连续的固定宽度细粒度文本提议。每个框的颜色表示文本/非文本的分数。只显示文本框正例的分数。 (1)贡献一:开发了一个垂直锚点机制,联合预测每个固定宽度提议的位置和文本、非文本的分数。 (2)贡献二:序列提议通过循环神经网络自然连接起来,该网络无缝的结合到卷积网络中,从而形成可训练的端到端模型。 在ICDAR2013,2015数据集上都取得了很好的成绩。 4.相关工作 (1)文本检测:过去都是使用自下而上的方法为主,粗略分为连接组件(CC)和基于滑动窗口的方法。特征手动设计,鲁棒性差,设计特征本身往往也十分困难,另外滑动窗口的方法在计算上也十分昂贵。 (2)目标检测:从选择性搜索的RCNN发展到了RPN网络提供候选框的Faster-RCNN,RPN提议不具有判别性,需要通过额外得成本高昂的CNN模型进一步细化和分类。更重要的是,文本和一般目标检测很大的不同,因此很难直接将通用的目标检测系统应用到这个高度领域化的任务中。 5.连接文本提议网络 本节详细介绍网络的细节,它包括三个关键的贡献,使文本定位可靠和准确:检测细粒度提议文本,循环连接文本提议和边缘细化。 (1)在细粒度提议中检测文本 输入的图像任意大小,VGG网络架构决定了总步长和感受野固定为16个和228个像素。而本文锚点的宽度恰好固定为16,刚好各个框互相挨着且不重叠。 文中k个锚点框,k设置成10,其高度从11个像素到273个像素(每次÷0.7),位置通过高度和y中心坐标度量。如下所示: 其中V={Vc,Vh},V*={V*c,V*h}分别是相对的预测坐标和相对的实际坐标,Cya,ha分别是锚点框的y轴中心高度,Cy,h是输入图片中预测的y轴坐标和高度,C*y,h*是输入图片的实际坐标和高度。 检测到的文本提议是从>0.7(具有非极大值抑制)的文本/非文本分数的锚点生成的。 (2)循环连接文本提议 RNN类型:BLSTM(双向LSTM),每个LSTM有128个隐含层。 RNN输入:每个滑动窗口的3*3*C的特征(可以拉成一列),同一行的窗口的特征形成一个序列。 RNN输出:每个窗口对应256维特征。 整个感受野理论上可以覆盖228*width. (3)边缘细化 文本行的构建规则。后面详细补充。 与y中心坐标预测类似,下面是x坐标的相对偏移: 文中每个锚点都预测了x坐标的偏移(这个步骤不是后处理计算的),如图一所示,但最终只使用了文本行边缘的提议。即左右两边。 (4)模型输出和损失函数 提出的CTPN有三个输出共同连接到最后的FC层,如图一所示,这个三个输出同时预测文本/非文本分数,垂直坐标(v={Vc,Vh})和边缘细化偏移(o).,探索k个锚点来预测他们在conv5中的每个空间位置,从而在输出层分别得到2k,2k和k个参数。 其中每一个锚点都是一个训练样本,其中每个锚点都是一个训练样本,ii是一个小批量数据中一个锚点的索引。 未完。
1.知识点 (1)指针可以指向任何类型,也可以指向函数。每个函数在内存中都占用一段存储单元,这段存储单元的首地址称为函数的入口地址,指向之歌函数入口地址的指针称为函数指针。 (2)函数基本用法如下: 1 int max(int a, int b) { 2 return a > b ? a : b; 3 } 4 int (*p)(int, int) = max;//定义函数指针时必须指明函数返回的类型和参数列表 5 int x = 10, y = 20; 6 int z = p(x, y);//可以直接把函数指针当做函数名来用,也可以用int z=(*p)(x,y); 注意:(2.1)函数名等价于函数的入口地址;(2.2)定义函数指针时()不能少,如果少了int *p(int,int)变成是一个函数,返回值是int *的函数,而不是指针了;(2.3)函数指针的类型务必要和函数的返回值以及参数列表都要匹配。 2.面试题 2.1通过函数指针实现四则运算 指出下面程序的不足,并提出更好的设计方案。 1 int add(int a, int b) { 2 return a + b; 3 } 4 int minu(int a, int b) { 5 return a - b; 6 } 7 int multi(int a, int b) { 8 return a * b; 9 } 10 int process(int a, int b, char operation) { 11 switch (operation) 12 { 13 case '+':return add(a, b); 14 case '-':return minu(a, b); 15 case '*':return multi(a, b); 16 default: 17 return 0; 18 } 19 } 20 int main(int argc, char argv[]) { 21 int a = 10, b = 20; 22 int res1 = process(a, b, '+'); 23 int res2 = process(a, b, '-'); 24 int res3 = process(a, b, '*'); 25 cout << res1 << " " << res2 << " " << res3 << endl; 26 27 getchar(); 28 return 0; 29 } 问题:扩展性很差,比如需要添加一个除法运算,这时不仅需要编写除法运算函数,还要去修改process函数,添加除法的判断分支。解决办法:可通过函数指针来提高扩展性(由于此处加减乘除的返回值和参数列表都是相同的),具体方式如下: 1 int add(int a, int b) { 2 return a + b; 3 } 4 int minu(int a, int b) { 5 return a - b; 6 } 7 int multi(int a, int b) { 8 return a * b; 9 } 10 int divide(int a, int b) { 11 if (b == 0) { 12 return -1; 13 } 14 return a / b; 15 } 16 17 int process1(int a, int b, int(*func)(int, int)) { 18 return func(a, b); 19 } 20 int main(int argc, char argv[]) { 21 int a = 10, b = 20; 22 int res1 = process1(a, b, add); 23 int res2 = process1(a, b, minu); 24 int res3 = process1(a, b, multi); 25 int res4 = process1(a, b, divide); 26 cout << res1 << " " << res2 << " " << res3 << " " << res4 << endl; 27 getchar(); 28 return 0; 29 } 2.2简化超长的函数指针类型 请解释process函数中四个参数的作用,其中第四个参数int (* fun) (int,int,int)似乎不太美观,有什么办法让它看起来更加简洁? 1 int maxof3(int a,int b,int c); 2 int minof3(int a,int b,int c); 3 int avgof3(int a,int b,int c); 4 int process(int a,int b,int c,int (*func)(int,int,int)){return func(a,b,c);} 答案:前三个是整型变量,第四个是函数指针;使用typedef来给类型起别名,用法如下: 1 typedef int(* Pfunc)(int ,int, int);//使用Pfunc来指代int (*)(int,int,int) 2 int process(int a,int b,int c,Pfunc func);
1.知识点 (1)在程序中可以声明指向任何数据类型的指针,指针也可以指向指针类型,成为指向指针的指针。下面是一个使用例子 1 int a=10,b=20; 2 int *q=&a; 3 int **p=&q; 4 **p=30; (2)如果想通过指针在被调函数中修改主调函数的变量,必须将主调函变量(务必确定该变量的类型,有时候可能变量本身就是指针,这时候形参就需要是指针的指针了)的地址作为参数,在被调函数中修改主调函数指向的内容。 2.面试题 2.1指针作为参数的常见错误 1 int find(char *s, char ch, char *sub) { 2 for (int i = 0; *(s + i) != '\n'; i++) { 3 if (*(s + i) == ch) { 4 sub = s + i + 1; 5 return i; 6 } 7 } 8 return 0; 9 } 10 11 int main(int argc, char *argv[]) { 12 char fullName[] = {"Jordan#Michale"}; 13 char *givenName=fullName; 14 15 int cnt = find(fullName, '#', givenName); 16 cout << givenName << "has a" << cnt << "characters 'family name" << endl; 17 18 getchar(); 19 return 0; 20 } 重点知识点:(1)如果想通过指针在被调函数中修改主调函数的变量,必须将主调函变量的地址作为参数,在被调函数中修改主调函数指向的内容。 (2)通过指针传递参数时,最大的忌讳就是以为只要参数是指针就万事大吉了。实际上,应该首先确定要修改的变量的类型,然后再将其地址作为参数。如果要修改的变量本身就是指针,就应该将该指针的地址作为参数,此时的形参类型是指向指针的指针。 以上第三个参数的指向的改变并不能带来实参的改变,正确答案如下: 1 int find(char *s, char ch, char **sub) { 2 for (int i = 0; *(s + i) != '\n'; i++) { 3 if (*(s + i) == ch) { 4 *sub = s + i + 1; 5 return i; 6 } 7 } 8 return 0; 9 } 10 11 int main(int argc, char *argv[]) { 12 13 char fullName[] = {"Jordan#Michale"}; 14 char *givenName=NULL; 15 16 int cnt = find(fullName, '#', &givenName); 17 cout << givenName << " has a " << cnt << " characters 'family name" << endl; 18 19 getchar(); 20 return 0; 21 } 2.2指向指针的指针和二维数组的区别 1 int main(){ 2 int a[2][3]={{1,2,3},{4,5,6}}; 3 int **p=a; 4 cout<<**p<<endl; 5 getchar(); 6 return 0; 7 } 解析:p是指向指针的指针(类型是int **),a<=>&a[0],a[0]是一个一维数组(相当于对一维数组名a[0]取地址,它应该赋值给int *[3])。所以左边是一个指向指针的指针,右边是一个指向数组的指针,两边类型不同。如果想编译通过,应该定义一个指向数组的指针,修改如下: 1 int (*p)[3]=a; 小技巧:判断变量的类型: 变量的类型在声明之初就已经确定了,在程序中只要将声明语句中变量名去掉,剩下的部分就是变量的类型。如下: 1 int **p的类型是:int** 2 int a[2][3]的类型是:int[2][3] 3 const int(const *p)[3]的类型是:const int(cosnt *)[3]
说明:主要考虑深度学习的方法,传统的方法不在考虑范围之内。 1.文字识别步骤 1.1detection:找到有文字的区域(proposal)。 1.2classification:识别区域中的文字。 2.文字检测 文字检测主要有两条线,两步法和一步法。 2.1两步法:faster-rcnn. 2.2一步法:yolo。相比于两步法,一步法速度更快,但是accuracy有损失。 文字检测按照文字的角度分。 2.1水平文字检测:四个自由度,类似于物体检测。水平文字检测比较好的算法是2016ECCV乔宇老师团队的CTPN。 2.2倾斜文字检测:文本框是不规则的四边形,八个自由度。倾斜文字检测个人比较喜欢的方法是2017CVPR的EAST和Seglink。套路:检测文本框->用radon hough变换等方法进行文本矫正->通过投影直方图分割出单行的文本的图片->最后对单行OCR。 3.文字识别 只考虑了不需要对文字进行分割。 3.1定长的,各个字符之间看成是独立的:multi-digit number。 3.2不定长的:RNN/LSTM/GRU+CTC。白翔老师团队的CRNN写的比较清楚。 3.3不定长的attention-mechanism(CNN+RNN+Attention):分为hard attention(直接给出hard location,不能直接暴力pb)、soft attention(可以暴力pb)、gradient-base attention。 参考:https://www.zhihu.com/question/20191727
题目:编程实现输入某年某月某日,计算这一天是这一年的第几天: 1 #include<iostream> 2 3 4 using namespace std; 5 6 int getDays(int year, int month, int day) { 7 int days_of_month[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };//一月份加的天数为0 8 //判断平年闰年,平年二月28天,闰年多一天 9 if ((year % 4 == 0) && (year & 100 != 0) || (year % 400 == 0)) { 10 days_of_month[2] = 29; 11 } 12 else { 13 days_of_month[2] = 28; 14 } 15 16 //参数有效性检查 17 if (month < 1 || month>12 || day<0 || day>days_of_month[month]) { 18 return -1; 19 } 20 21 int Days=0; 22 for (int i = 0; i < month; i++) {//不能int i=1,这样在一月时也会进行一次循环 23 Days += days_of_month[i]; 24 } 25 Days += day; //加上当月的天数 26 return Days; 27 } 28 int main(int argc, char *argv[]) { 29 cout << getDays(2015, 12, 2); 30 getchar(); 31 return 0; 32 } 注意:不要忘了参数检查。
1 #include<iostream> 2 3 using namespace std; 4 5 bool isPrime(int n) { 6 for (int i = 2; i < n; i++) { 7 if (n%i == 0) { //能被2到把自身小1的数整除的都不是素数 8 return false; 9 } 10 } 11 return true; 12 } 13 14 int getPrimePair(int n) { 15 if ((n %2== 0) && (n < 2)) { //不符合题目输入要求返回0,表示失败 16 return 0; 17 } 18 for (int i = 2; i < n / 2; i++) { 19 if (isPrime(i) && isPrime(n - i)) { 20 cout << n << " = " << i << " + " << n - i << endl; 21 } 22 } 23 return 1; //返回1表示成功 24 } 25 26 int main(int argc, char *argv[]) { 27 getPrimePair(28); 28 getchar(); 29 return 0; 30 } 思路:将n分为i和n-i,编写判断素数函数,看i和n-i是否都是素数,是的话打印出来。
题目:10个互不相等的整数,求其中的第2大的数字,要求数组不能用排序,设计的算法效率越高越好。 1 #include<iostream> 2 3 using namespace std; 4 5 int max_second(int *arr,int n) { 6 int max_first = arr[0], max_second = arr[0]; 7 for (int i=1; i < n; i++) { 8 if (arr[i] > max_first) { //当大于最大数的时候 9 max_second = max_first; 10 max_first = arr[i]; 11 } 12 else if (arr[i] > max_second) { //当小于最大数,大于次大数的时候 13 max_second = arr[i]; 14 } 15 } 16 return max_second; 17 } 18 19 int main(int argc, char *argv[]) { 20 int array[] = { 17,3,2,5,6,4,10,7,19,16 }; 21 cout << max_second(array, sizeof(array)/sizeof(int)); 22 getchar(); 23 return 0; 24 }
斐波那契数列是一个常识性的知识,它指的是这样的一个数列,它的第一项是1,第二项是1,后面每一项都是它前面两项的和,如:1,1,2,3,5,8,13,21,34,55,89,144,233…… 说明:由于通过递推方式效率低,系统开销大,空间复杂度高,故不考虑。 1 /*斐波那契数列:第一项和第二项为1,后面各项是其前面两项之和*/ 2 /*编写一个函数,输入整数n,求该项的值*/ 3 4 #include<iostream> 5 6 using namespace std; 7 8 int fibonacci(int n) { 9 if (n < 0){ 10 return -1; 11 } 12 if ((n == 1 )||( n ==2)) { 13 return 1; 14 } 15 else { 16 int fib_1 = 1,fib_2=1,fib_3; 17 for (int i = 3; i <= n; i++) { 18 fib_3 = fib_1 + fib_2; 19 fib_1 = fib_2; 20 fib_2 = fib_3; 21 } 22 return fib_3; 23 } 24 } 25 26 int main(int argc, char *argv[]) { 27 cout << "please input n" << endl; 28 int n; 29 cin >> n; 30 cout << "The reslut: " << fibonacci(n) << endl; 31 32 getchar(); 33 getchar(); 34 return 0; 35 }
1.知识点 1.1指针数组——存放指针的数组 (1)指针数组本质上是一个数组,指针是数组中的内容,表示数组中的每个元素都是指针,因此指针数组就是存放指针的数组。下面是指针数组的用法: 1 int a = 10, b = 20; 2 int *p[3]; 3 p[0] = &a; 4 p[2] = &b; (2)指针数组的定义可以抽象为:指向变量类型 * 数组名称[数组长度]。 (3)[]的优先级高于*,所以[]与p先结合,说明p是一个数组,长度为3,其数组元素的类型是int *。 1.2数组指针——指向数组的指针 (1)数组指针本质上是一个指针,数组是指针指向的类型,表示指针指向一个数组,因此数组指针就是指向数组的指针。下面是数组指针的用法: 1 int a[3] = { 1,2,3 }; 2 int (*p)[3]; 3 p = &a; (2)数组指针的定义可以抽象为:数组元素类型 (* 指针名称)[数组长度]。 (3)()使得*与p先结合,所以p是一个指针,盖掉(*p)剩下的就是指针指向的内容了即int[3]。 (4)数组指针指向整个数组a(取地址&a),而非数组a的首元素(取地址a),虽然数组a和数组a中首元素的地址相同。 2.面试题 2.1简述数组指针与二维数组的区别 请写出下面程序的输出结果(假设数组a的地址&a为0x001DF720) 1 int main(int argc, char *argv[]) { 2 //数组指针和二维数组的区别 3 int a[2][5] = { {1,2,3,4,5},{6,7,8,9,10} }; 4 int(*p)[5] = a; 5 cout << p << endl; 6 cout << p + 1 << endl; 7 8 cout << *p << endl; 9 cout << *(p + 1) << endl; 10 cout << *p + 1 << endl; 11 12 cout << **p << endl; 13 cout << **(p + 1) << endl; 14 cout << *(*p + 1) << endl; 15 16 getchar(); 17 return 0; 18 } 知识点提示:(1)数组名始终等价于数组首元素的地址:a<=>&a[0]。 (2)解引用的次数等于数组的维度时,才能得到数组的元素值,如二维数组必须经过两次解引用才能得到数组的元素值。 (3)数组首元素地址和数组的地址虽然位置一样,但是二者完全不是一回事(通过解引用来理解),数组首元素地址&a[0]解引用之后为*(&a[0])=a[0],是首元素的值;而数组地址&a解引用之后为*(&a)=a=&a[0];通过以上说明,数组地址一次解引用之后得到数组首元素的地址,再解引用之后得到首元素的值(在一维数组的情况下)。 (4)二维数组a[n][m],可以把a[n]看成是内层[m]的数组名。 按照以上的知识和规则认真分析可以得出如下答案: 1 //数组a的地址&a为0x001DF720 2 int main(int argc, char *argv[]) { 3 int a[2][5] = { {1,2,3,4,5},{6,7,8,9,10} }; 4 int(*p)[5] = a;//a==&a[0],左右类型相同,p指向一个元素为5的一维数组 5 cout << p << endl;//两次解引用是地址,p=&a[0],0x001DF720 6 cout << p + 1 << endl;//两次解引用是地址,p+1=&a[1]=0x001DF720+4*5=0x001DF734 7 8 cout << *p << endl;//一次解引用是地址,*p=a[0]=&a[0][0],0x001DF720 9 cout << *(p + 1) << endl;//一次解引用是地址,*(p+1)=a[1]=&a[1][0],0x001DF734 10 cout << *p + 1 << endl;//一次解引用是地址,*p+1=&a[0][1]=0x001DF724 11 12 cout << **p << endl;//二次解引用是元素值,**p=a[0][0]=1 13 cout << **(p + 1) << endl;//二次解引用是元素值,**(p+1)=a[1][0]=6 14 cout << *(*p + 1) << endl;//二次解引用是元素值,*(*p+1)=a[0][1]=2 15 16 getchar(); 17 return 0; 18 } 2.2简述数组地址和数组首地址的区别 指出下面程序中的错误: 1 int main(int argc, char *argv[]) { 2 int b[6] = { 1,2,3,4,5,6 }; 3 int *p = &b; //左右类型不匹配 4 cout << p << endl; 5 6 getchar(); 7 return 0; 8 } 知识点:数组首元素地址和数组的地址虽然位置一样,但是二者完全不是一回事(通过解引用来理解),数组首元素地址&a[0]解引用之后为*(&a[0])=a[0],是首元素的值;而数组地址&a解引用之后为*(&a)=a=&a[0];通过以上说明,数组地址一次解引用之后得到数组首元素的地址,再解引用之后得到首元素的值(在一维数组的情况下)。 修改方式以下二选其一: 1 //方式一 2 int (*p)[6]=&b; 3 //方式二 4 int *p=b;//或者:int *p=&b[0]; 2.3简述指针数组与指向指针的指针的区别 写出指针数组str中四个指针元素的值。 由于题目有问题,未正确解答,故略。
1.知识点 1.1指针常量——指针类型的常量 (1)指针常量本质是常量,指针用来说明常量的类型,表示该常量是一个指针类型的常量。 (2)在指针常量中,指针的值不可变,即始终指向同一个地址。 (3)但指针所指向的地址的值是可以通过*p来改变的。 用法如下: 1 //技巧: 2 //(1)读法:从左到右读,前面的是修饰词,后面的是主语 3 //如先出现指针(*),后出现常量(const),所以是指针常量 4 //(2)哪个东西不可变:直接看const后边是什么就什么不可变 5 //const后边是指针(p),所以是指针本身的值不可变。 6 int a = 10, b = 20; 7 int * const p = &a; 8 *p = 30; 1.2常量指针——指向常量的指针 (1)常量指针本质上是一个指针,常量表示指针指向的内容,说明指针指向的是一个“常量”。 (2)在常量指针中,指针指向的内容是不可变的,不可以通过*p来改变,但变量(指针指向的地址)可以通过自身赋值改变。 (3)另指针指向的地址是可以变的,即指向其他地址。 用法如下: 1 //技巧: 2 //(1)读法:从左到右读,前面的是修饰词,后面的是主语 3 //如下先出现常量(const),然后出现指针(*),所以是常量指针 4 //(2)哪个东西不可变:直接看const后边是什么就什么不可变 5 //如下const后面是*p,所以是指针指向的地址的值不可变(只限定了不可以通过*p来改变) 6 int a = 10, b = 20; 7 int const *p = &a;//不可以通过*p改变a的值 8 p = &b;//指针的地址是可以变的 9 a = 30;//也可以通过变量本身改变值 1.3const int 与int consts是等价的 const int *p==int const *p 2.面试题 2.1指针常量和常量指针的常见错误 1 int main(int argc, char *argv[]) { 2 int m = 10; 3 const int n = 20; 4 5 const int *ptr1 = &m; 6 int * const ptr2 = &m; 7 8 ptr1 = &n; 9 ptr2 = &n; 10 11 *ptr1 = 3; 12 *ptr2 = 4; 13 14 int *ptr3 = &n; 15 const int *ptr4 = &n; 16 17 int *const ptr5; 18 ptr5 = &m; 19 20 const int * const ptr6 = &m; 21 *ptr6 = 5; 22 ptr6 = &n; 23 24 getchar(); 25 return 0; 26 } 答案解析: 1 int main(int argc, char *argv[]) { 2 int m = 10; //正确,变量 3 const int n = 20; //正确,常量 4 5 const int *ptr1 = &m; //正确,常量指针,指向的地址的内容不可变 6 int * const ptr2 = &m; //正确,指针常量,指针指向不可变,是常量所以必须赋初值 7 8 ptr1 = &n;//正确 9 ptr2 = &n;//错误,指针常量,指针的指向不可变 10 11 *ptr1 = 3;//错误,常量指针,指针指向的地址的内容不可变 12 *ptr2 = 4;//正确 13 14 int *ptr3 = &n;//错误,不能将常量的地址赋值给普通指针,常量地址只能赋值给常量指针 15 const int *ptr4 = &n;//正确 16 17 int *const ptr5;//错误,指针常量是常量,所有常量在定义时都必须赋初值 18 ptr5 = &m;//错误,应该定义时直接赋初值 19 20 const int * const ptr6 = &m;//正确,同时包含指针常量和常量指针的性质 21 *ptr6 = 5;//错误,不符合常量指针的性质 22 ptr6 = &n;//错误,不符合指针常量的性质 23 getchar(); 24 return 0; 25 } 2.2指针常量用作函数参数 写出程序的输出结果,并说明在函数exchange2中将参数声明为const的意义,是否可以将const修饰符放在*之前。 1 void exchange1(int a, int b) { 2 int temp = a; 3 a = b; 4 b = temp; 5 } 6 void exchange2(int * const a, int * const b) { 7 int temp = *a; 8 *a = *b; 9 *b = temp; 10 } 11 int main(int argc, char *argv[]) { 12 int m = 10, n = 20; 13 exchange1(m, n); 14 cout << "m=" << m << ";n=" << n << endl; //输出:m=10;n=20 15 exchange2(&m, &n); 16 cout << "m=" << m << ";n=" << n << endl; //输出:m=20,n=10 17 getchar(); 18 return 0; 19 } 答案:第一行输出的是:m=10,n=20;第二行输出是:m=20,n=10. 解析:exchange1按值传递,函数中a,b值交换与m,n无任何联系,故无法改变m,n的值;exchange2中传入m,n的地址,通过解引用可以在函数内部改变m,n的值。在函数exhange2的参数中,const的作用是将指针声明为指针常量,防止指针a和b在使用过程中意外发生改变(改变将会报错)。如果将const放在*之前,指针a和b变成常量指针,无法修改指针指向的内容,从而无法实现交换m和n的目的。 2.3指针常量和字符串常量的冲突 请写出下面程序的运行结果: 1 int main(){ 2 char *const str="apple"; 3 *str="orange"; 4 cout<<str<<endl; 5 getchar(); 6 return 0; 7 } 答案1(其实有问题,看下面说明,): int main(){ char *const str="apple"; //错误,为了修改str的值应该去掉const *str="orange";//错误,根据字符串赋值规则应该改为str="orange" cout<<str<<endl; getchar(); return 0; } 说明:(1)一个字符串返回的是首字母的地址,故*str的值为'a'. (2)字符串常量是存储在常量区,而常量的值不可以修改,所以在VC编译器中下面的语句是编译不过去的 char *str = "apple"; 最正确的语句应该是(因为字符串常量本身是常量,不应该赋值给一个普通指针,这意味着通过指针可以改变常量的值,前后矛盾,上面语句在一些地方能通过编译,其实是由于历史遗留问题): const char *str = "apple"; (3)字符串正确赋值给变量应该是给str,因为字符串返回的其实是字符串的首地址,所以应该赋值给指针str,而不是*str,该指针指向首地址。 综上最正确的答案应该如下: 1 int main(){ 2 const char *str = "apple";//常量指针,指向的内容不可变 3 str = "oragne";//指针的指向是可变的 4 cout<<str<<endl; 5 getchar(); 6 return 0; 7 }
1.知识点 三步走:申请,释放,指针置空。 1.1malloc、free函数 在C语言中内存malloc函数申请动态空间,以下展示其基本用法: 1 int *p = NULL; 2 p = (int *)malloc(sizeof(int) * 10);//申请 3 free(p);//释放,否则会造成内存泄漏 4 p = NULL;//指针置空,否则成为野指针 (1)动态分配的空间来自队空间,而指针本身作为局部变量存储在栈空间中。 (2)malloc有时候也可能申请空间失败,这时返回NULL,故需要对其进行判断。 (3)通过malloc动态申请的空间必须通过free函数释放,这两个函数成对出现。否则可用空间会越来越少。 (4)在通过free函数释放之后,最好将指针置空。 (5)malloc/free函数申请释放的过程其实就是可用空间链表不断在更新。 1.2new、delete函数 (1)new和delete运算符既可以应用于基本类型,也可以用于自定义类型,new操作符不仅申请了空间,然后还根据提供的参数进行构造函数初始化,delete在释放内存空间之前还会调用对象的析构函数,这些事new/delete比malloc/free更为丰富的地方。 2.面试题 2.1malloc和free的常识性问题 以下说法正确的是(D)。 (A)free会将指针置为空 //需要手动置空 (B)malloc函数的返回指针移动后,free函数会自动找到首地址并释放 //不能失去对首地址的控制,否则无法释放 (C)malloc函数一次申请N个int空间,使用后需要循环N次逐一调用free释放 //malloc和free成对出现 (D)malloc申请的空间位于堆上 2.2返回一个64整数倍的地址 编写两个函数,align64malloc和align64free,分别用于申请空间和释放空间,并要求申请空间返回的地址必须是64的整数倍。 解析:在所需空间前面再加上64个字节,可保证其中肯定有一个地址是64的倍数,再在这64个字节空间的前面再加上4个字节保证有地方可以存储返回的首地址。如下表所示 A 4个字节 B 64个 字节 C N个字节 1 void * align64malloc(int size) { 2 void *ptr = malloc(sizeof(int)*size + 64 + sizeof(void *)); 3 if (!ptr) { 4 return NULL; 5 } 6 ptr = (char *)(ptr)+sizeof(void *); //在最前面预留出来了存放首地址的存储空间 7 //接下来一步需要将首地址空间放入到64整数倍前面的空间中去 8 *((int *)(((int)ptr+64-(int)ptr%64)-sizeof(void *)))=(int)ptr - sizeof(void *);//等式右边为首地址,void*是不可以进行加减运算 9 return (void *)((int)ptr + 64 - (int)ptr % 64); 10 } 11 12 void algin64free(void * ptr) { 13 if (ptr) { 14 free((void *)(*((void **)ptr - 1)));//void *不能进行加减,转换成指针的指针之后可以进行加减 15 } 16 } 2.3简述malloc/free和new/delete的区别 (1)malloc/free是C语言提供的库函数,通过函数调用访问,需要传递参数并接收返回值;而new/delete是C++提供的操作符,有自己的一套语法规则和运算方式。 (2)malloc/free只能用于基本的数据类型,而new/delete不但能用于基本数据类型,还可以用于面向对象中的自定义类型。 (3)malloc函数返回的是void*类型,程序需要显示的转换成所需要的指针类型,new操作符后面直接指明了类型,不涉及类型转换问题。 (4)malloc只负责申请空间,并返回首地址;new运算符除了申请空间,还会调用构造函数初始化指针指向的内容;free韩式只负责 释放空间,并标识这段空间为可用空间;delete运算符除了释放空间,还会调用对象的析构函数。 (5)事实上,后者覆盖了前者的全部功能,之所以在C++中还保留malloc/free函数,主要是为了解决兼容性问题,防止C++中调用包含malloc/free的C函数时出现错误。 2.4简述delete和delete[]的区别 答案(1)当new[]中数组的元素是基本类型时,通过delete和delete[]都可以释放数组空间; (2)当new[]中的数组元素是自定义的类型时,只能通过delete[]释放数组空间(因为用delete只调用第一个元素的析构函数)。 强烈建议申请和释放空间是采用完全配对的方式:new和delete成对使用,new[]和delete[]成对使用。 以下两个例子说明: 1 //基本类型时二者都可以 2 //A 3 int *i = new int[5]; 4 delete i; 5 //B 6 int *i = new int[5]; 7 delete[] i; 1 //自定义类型new[]/delete[]必须成对 2 class Test { 3 private: 4 char *text; 5 public: 6 Test(int lenght = 100) { 7 text = new char[lenght]; 8 } 9 ~Test() { 10 delete text; 11 cout << "A destructor" << endl; 12 } 13 }; 14 15 Test *a = new Test[5]; 16 delete[] a;//使用delete会出错
1.知识点 (1)sizeof是一个单目运算发,而不是一个函数,其用于获取操作数所占内存空间的字节数。 (2)sizeof的操作数可以使类型名,也可以是表达式,如果是类型名则直接获得该类型所占字节数,如果是表达式,则先分析表达式结果的类型,再根据类型确定所占字节数,并不对表达式进行实际计算。 1 int a = 1; 2 double b = 1.5; 3 sizeof(int);//结果为4 4 sizeof(a); //结果为4 5 sizeof(b); //结果为8 (3)sizeof很少单独使用,而是和内存分配或者计算法数组长度等需求进行配合使用。 1 //与内存空间分配配合使用 2 int *ptr = (int *)malloc(sizeof(int) * 20); 3 //与计算数组长度配合使用 4 int count = sizeof(darray) / sizeof(double); (4)数组名作为操作数时,将获得整个数组所占空间,当数组名作为实参传递给子函数时,此时数组名已经成为了指针,其计算结果将是指针的所占空间。 void subfunc(double darray[]) { cout << sizeof(darray) / sizeof(double) << endl; //输出4/8=0 } int main(int argc, char *argv[]) { double darray[20]; cout << sizeof(darray) / sizeof(double) << endl; //输出16/8=20 subfunc(darray); getchar(); return 0; } (5)在计算字符串长度时要用strlen。sizeof是一个运算符,用于获取类型或表达式的所占内存大小;strlen是一个函数,用于计算字符串中字符的个数,其中字符串结尾符\0不会被计算在内。 2.面试题 2.1不能使用sizeof计算的表达式 1 struct baby 2 { 3 unsigned int gender : 1; 4 unsigned int weight : 5; 5 unsigned int week : 7; 6 unsigned int blood : 3; 7 unsigned int height : 8; 8 }; 9 int triple(int number) { 10 return 3 * number; 11 } 12 int main(int argc, char *argv[]) { 13 sizeof(baby); //A 14 sizeof(baby.gender);//B 15 sizeof(triple);//C 16 sizeof(triple(3));//D 17 sizeof(show());//E 18 19 getchar(); 20 return 0; 21 } 答案:正确:A,D;错误:B,C,E. (1)sizeof计算结构体是结果是结构体的所占内存空间,结构体中使用了位域定义成员,要求一个位域成员不能横跨两个字节,故可以算出该结构体的字节数为4,A正确。 (2)sizeof计算的结果是字节数,但是不允许计算结构体中某个位域成员的所占空间,当然如果不是位域定义的是可以计算的。故B错。 (3)不允许计算函数名的所占字节数大小,有点莫名其妙,C错。 (4)允许计算表达函数表达式,其结果是函数返回值的类型的字节数,即该题的int大小,D正确。 (5)函数返回值类型不确定使,不允许通过sizeof计算,故E错误。 2.2sizeof计算结构体是的内存对其问题 写出下面两个结构体的sizeof计算结果 struct s1{ char a;//1 short b;//4 int c;//8 double d;//16,16刚好是double大小的整数倍 }; struct s2 { char a;//1 short b;//4 double c;//16(从9开始排) int d;//20(该结果不是double的整数倍,故需要加到24) }; 数据对其总结:(1)每个成员在内存中的起始位置都必须是自身大小的整数倍。 (2)最终结构体计算结果必须是占字节数最大的成员的整数倍。 答案:16,24。 2.3结构体嵌套时的sizeof计算 1 struct s1 { 2 int a; 3 }; 4 struct s2 { 5 char a[4]; 6 }; 7 struct s3{ 8 char a[4]; 9 char b; 10 }; 11 struct s4 { 12 s1 a; 13 char b; 14 }; 15 struct s5{ 16 s2 a; 17 char b; 18 }; 含复杂结构体的数据对其总结:在数据对其时,要以结构体中最深层的基本数据类型为准。例如:如果结构体成员是一个数组,应以数组的元素的类型作为数据对其的标准,如果结构体成员是其他结构体,应以内层结构体中的基本元素类型成员作为外层结构体数据对齐的标准。故答案如下 1 sizeof(struct s1)=4; 2 sizeof(struct s2)=4; 3 sizeof(struct s3)=5; 4 sizeof(struct s4)=8; 5 sizeof(struct s5)=5;
1.知识点 1.1宏定义 (1)不带参数的宏定义 1 #define ERROR_MESSAGE -100 2 #define SECONDS_PER_DAY 60*60*60 (2)带参数宏定义,这种形式称为宏函数,但其实并不是函数 #define OUTPUTINT(x) cout<<"INT:"<<x<<endl #define OUTPUTCHAR cout<<"CHAR:"<<x<<endl 1.2内联函数 宏定义是在预处理阶段进行宏展开的,但是经常会出现一些意想不到的错误,故出现内联函数,内联函数既发挥了宏定义的优势,又弥补了其缺点。 内联函数是在定义时在函数最前面加上inline,或者将函数声明的同时进行定义(这种方式不推荐)。 下面是一个内联函数的例子: 1 class Rectangle { 2 public: 3 Rectangle(int, int); 4 int getSquare(); 5 int getGirth() { return 2 * (length, width); } //直接在声明时定义函数,形成内联函数 6 private: 7 int length; 8 int width; 9 }; 10 11 Rectangle::Rectangle(int l,int w):length(l),width(w){} 12 inline int Rectangle::getSquare() { //在函数定义时使用inline形成内联函数 13 return length * width; 14 } 2.面试题 2.1简述内联函数和宏定义的区别 相同点:二者都能够节省频繁的函数调用过程中所产生的时间和空间消耗,提高执行的效率;二者都是哦谈过将函数调用替换成完整的函数体,二者的实现也类似。 区别:(1)二者的根本区别在于宏定义仅仅是字符串的替换,并不是函数,而内联函数是函数。 (2)二者的代码展开发生在不同阶段,宏定义是在预处理阶段展开的,而内联函数是在编译阶段展开的。 (3)内联函数作为类的成员函数时,可以访问类的所有成员,包括公有、私有、保护成员,隐式使用this指针,而宏定义无法实现这些功能。 (4)内联函数可以完全替代宏定义,故尽量少使用宏定义。 (5)另外在使用内联函数时要注意代码膨胀问题,内联函数应该尽量简短(另外现在编译器一般都有优化功能,当检测到内联函数代码很长时,不会进行内联,即使使用了内联函数)。 2.2宏定义的宏展开错误 指出下面程序中宏定义的错误并修改 1 #define MAX(a,b) a>b?a:b 2 #define MUL(a,b) a*b 3 int main(int argc, char *argv[]) { 4 int x = 4, y = 2; 5 int max = MAX(x, y); 6 int product = MUL(x, y); 7 cout << "the max is " << max << endl; 8 cout << "the product is " << product << endl; 9 getchar(); 10 return 0; 11 } 知识点:宏定义自身缺陷主要是宏展开之后,由于运算符的优先级等原因,使得宏定义展开后的语义和预想的发生偏差。 以下时两个宏展开出错的例子 1 int product=MUL(x,y+3) 2 int max=MAX(x,y)+2 3 4 //本意是 5 int product=x*(y+3) 6 int max=(x>y?x:y)+2 7 //实际宏展开变成了 8 int product=x*y+3 9 int max=x>y?x:y+2 解决办法包括以下两点: (1)给参数自身加上括号 (2)给整个宏定义加上括号 其修改结果如下: #define MAX(a,b) ((a)>(b)?(a):(b)) #define MUL(a,b) ((a)*(b)) 2.3内联函数的常识性问题 下列关于内联函数描述错误的是 (A)内联函数可以被重载; (B)构造函数可以被定义成内联函数; (C)内联函数能够减少函数调用的开销; (D)内联函数应该在函数声明时使用inline关键字 答案:D,一定要在定义时使用inline,在声明时使用不会起到任何作用。
注:读《程序员面试笔记》笔记总结 1.知识点 1.1条件语句 (1)if……;(2)if……else……;(3)if……else if……;(4)switch(){case ():break;case():break;default:}。 关于switch的两点说明,第一是case后面结束后必须加break,否则将在执行某个case之后的所有case语句都会执行,第二是default可以省略。 1.2循环语句 (1)for(init;condition of continue circular;variables update);(2)while(condition)。 关于while的一点说明:当while(1)时一般在内部会有break来终止程序结束,否则进入死循环。 2.面试题 2.1.不使用break的switch语句 公司年底给员工发一条关于年终奖的短信,奖品根据员工年度绩效考评结果而定,具体见下表,请编写一个函数,输入为员工年度考评的结果,输出为短信的内容,短信中需要罗列员工所获得的所有奖品。 考评结果 年终奖品 优秀 A 美国 或英国十日游,五千元超市卡,两千元亚马逊卡,一个月奖金 良好B 五千元超市卡,两千元亚马逊卡,一个月奖金 及格C 两千元亚马逊卡,一个月奖金 未达标D 一个月奖金 1 string getMessage(char mark) { 2 string message = ""; 3 switch (mark) { 4 case 'A'://注意此处使用单引号表示字符,双引号表示字符串 5 message.append("美国或英国十日游,"); 6 case 'B': 7 message.append("五千元超市卡,"); 8 case 'C': 9 message.append("两千元亚马逊卡,"); 10 case 'D': 11 message.append("一个月奖金"); 12 default: 13 break; 14 } 15 return message; 16 } 17 //注意#include<string>来重载cout,才能够输出string类型的数据 2.2.for循环的三要素 写出下面程序的输出结果: 1 bool foo(char c) { 2 cout << c; 3 return true; 4 } 5 int main(int argc, char *argv[]) { 6 int i = 0; 7 for (foo('A'); foo('B') && (i++ < 2); foo('C')) { 8 foo('D'); 9 } 10 getchar(); 11 return 0; 12 } 答案:ABDCBDCB 2.3巧打乘法口诀表 编写一个函数,接受一个整形参数n表示输出的规模。要求只用一重循环输出乘法口诀表的全部内容,并且程序中不能使用任何条件语句。 1 void print(int n) { 2 int row = 1, column = 1; 3 char flag[] = " \n";//当列数等于行数时为flag[1]换行 4 while (row<=n) 5 { 6 cout << row << " * " << column << " = " << row * column << flag[column / row]; 7 int tem = column % row + 1;//当列数等于行数时,tem跳回1 8 row = column / row + row;//当列数等于行数时,行数加一 9 column = tem; 10 } 11 } 总结:(1)列号的变化规律符合取模运算,这种不断回到起点的数字排列特征符合取模运算的性质; 下一项列号=当前列号%当前行号+1 (2)对于行号来说,当列号等于行号时,行号加1,当列号等于行号时,行号不变。行号的变化规律符合整数除法的性质,当被除数小于除数时结果为零,当二者相等时结果为1。 下一项行号=当前列号/当前行号+1
注:总结来自黄海广博士。
注:总结来自黄海广博士。
注:总结来自黄海广博士。 错误修正:9.微分中值定理,T2(罗尔定理)中缺了条件:a=b。