-
Notifications
You must be signed in to change notification settings - Fork 0
/
content.json
1 lines (1 loc) · 116 KB
/
content.json
1
{"pages":[{"title":"","text":"呀,一些碎碎念做一个自己的网站很酷呀,不过做完就去找工作了。(找工作倒是写了很多笔记,可以记录上来哈哈)但是也耽误了博客的更新,想起来可以记录些自己的想法,见闻,技术内容也挺好的。我一定好好更新嘻~~ 关于我:一个兜兜转转到CV的非典型程序媛 专业相关:目前在做计算机视觉相关的项目和研究,主要目标检测和语义分割做的多一些,目前在研究基于语义分割的领域自适应工作,争取出些成果(加油~) 业余爱好:喜欢各种运动(羽毛球,网球,足球,篮球,都很喜欢);日日离不开耳机;一看美剧就人间消失;看展看演出积极分子; 经历:焊板子,单片机编程,matlab仿真,计算机视觉都做过哈哈,虽然前几项已经忘得差不多了。","link":"/about/index.html"}],"posts":[{"title":"【Accelerated C++】 -- 01/16 开始学习C++","text":"本章开始,通过阅读《Accelerated C++》开始记录笔记并学习学习要点: 标准库和其代表的名字空间 表达式:被操作数和运算符组成了一个表达式,其中运算符有左结合/右结合的性质,被操作数则是由其类型决定表达式的结果 作用域:学习了两种作用域的生成方式,分别是花括号和名字空间 12345678#include <iostream>//这是一个简单的cpp程序int main(){ std::cout << \"Hello, World!\" <<std::endl; return 0;} 在这个简单的程序中,我们将学习到表达式,作用域,运算符,作用数等一系列的概念 1. 注释:可以使用 // 进行单行注释,也可以使用/ / 来进行多行注释(每次跨行需要行首加上 * ),当使用// 时,其优先级会高于多行注释 2. include:使用 include 语句来包含不属于语言核心的标准库来增加对额外的指出 3. main 函数:每一个 cpp 程序都需要包含且只能包含一个 main 函数,编译完成后,程序自行调用 main 函数,main 函数通常返回 0 来表示成功。 函数的返回值类型:函数定义的初始制定函数返回值的类型,每个返回值都需要满足这个类型要求 函数的参数:在函数名后的小括号中可以传入函数的参数,其中 main 函数也可以传入参数 花括号:函数的花括号代表了函数体,其中所有语句当成一个单元来处理 4. 标准库输入输出:1std::cout << \"Hello World\" << std::endl; 这里使用了输出运算符 << 来将字符串直接量和控制器 std::endl 写入到 std::cout 标准输出流中,这是一种链式输出。 std 名字空间:是所有标准库中变量所在的空间,通过作用域运算符 ::,我们就可以在名字空间中找到对应的变量,而这个变量的作用域就是这个名字空间 输出运算符号 <<: 用于将变量不断写入输出流 控制器:控制器 std::cout 用于控制数据流,并返回流作为其结果 5. 表达式:表达式由操作数和运算符组成 操作数:每一个操作数都有一个类型,代表了数据结构和对这个数据结构合理的操作,表达式的结果取决于草所数的类型。 运算符:用于运算被操作数的实体,其中 << 输出运算符是左结合的,也就是运算符左边的操作数由尽可能多地结合左边的表达式生成,右边的操作数则是尽可能结合少的表达式的结果,也就是会产生从左到右依次执行的结果 6. 作用域:变量的作用域代表变量只有在这个函数部分中才有含义,这里有两种定义域: 名字空间: 通过作用域运算符,我们可以在指定名字空间中找到我们想要的变量,防止了名字的冲突 花括号:定义函数主体,一般在函数中定义的变量,作用域在于函数体中","link":"/2020/09/12/CPP/01_%E5%BC%80%E5%A7%8B%E5%AD%A6%E4%B9%A0cpp/"},{"title":"【Accelerated C++】 -- 06/16 使用顺序容器和分析字符串","text":"学习要点: 容器和迭代器 list 表和 vector 向量的对比 顺序容器和字符串的成员和创建方法 字符串的有用方法和cctype文件中对字符的判定 1234567891011121314151617181920//目标:实现将满足条件的iter从student中抽取,并将原来的元素进行删除// 我们使用原地删除减少空间复杂度// 使用了表这种结构减少了删除元素时间复杂度bool fgrade(const Student_info& s){ return grade(s) < 60;}list<Student_info> extract_fais(list<Student_info>& students){ list<Student_info> fail; list<Student_info>::iterator iter = students.begin(); while (iter != students.end()){ if (fgrade(*iter)){ fail.push_back(*iter); iter = students.erase(iter); } else ++iter; } return fail;} 1. 容器和迭代器在标准库中,对不同的容器,vector,list等都有着相似的操作和语义,因而有统一的操作,接下来我们就介绍这些统一操作的一个:迭代器 迭代器: 优势 便于优化:作为容器的成员函数,帮助库了解我们在顺序遍历容器,基于顺序遍历库可以做出很多优化。 代码优雅:替换了使用索引这样可能是用于随机存取的索引,代码更加简洁 定义:迭代器在顺序容器中都有定义,其中 iterator 可以直接转换为 const_iterator 12container-type::const_iterator iter = student.begin(); //一个只能读的迭代器,指向容器第一个元素container-type::iterator iter = student.end(); //一个可以读写的迭代器,指向容器最后一个元素的后面一个位置 操作:包含间接引用对象,取出对象元素,移动到下一位等 123456789//间接引用对象:vector<Student_info>::iterator iter = student.begin();Student_info s = *iter;// 取出对象元素:double infor = (*iter).infor;double infor = iter->infor;//移动到下一位:自加运算符重载,代表移动到迭代器的下一位的意思++iter;iter++; 2. list 表和 vector 向量的对比二者都是顺序容器,但是 vector 想要实现快速随机存取,即使用索引;而list实现在任意位置插入和删除,但不支持索引 vector 针对在向量末端添加和删除元素,以及根据索引随机访问进行了优化; 删除时间很慢,O(N)的删除时间,因为需要移动删除元素后面所有元素; 迭代器失效问题,需要避免对于可能无效的迭代器的复制。 迭代器失效:在 student.erase(iter) 删除迭代器之后,迭代器后面所有元素都会移动位置,从而指向后面所有元素的迭代器都会失去意义123// 不过erase函数会返回一个迭代器指向删除元素的后面一个元素iter = student.erase(iter) student.resize(n) // 裁剪或者增加空元素 随机访问迭代器 list 是对删除插入任意位置元素的优化;是大规模数据任意删除元素的首选 不支持索引 不存在迭代器失效,只有删除的迭代器会失效 不支持完全随机访问,需要另外的 sort 函数12345list<Student_info> student;student.sort(cmp); //list 自带排序vector<Student_info> student;sort(student.begin(), student.end(), cmp); // 全局排序 3. 顺序容器和字符串的成员和方法1234567891011121314container<T>::iteratorcontainer<T>::const_iteratorcontainer<T>::size_typec.begin()c.end()c.rbegin() // 指向最后一个元素c.rend() // 第一个元素之前的迭代器container<T> c(c2); //复制才 c2 作为 c 的定义container<T> c(n); //初始化一个长度为n的ccontainer<T> c(b,e); //c复制了位于迭代器[b,e)的所有元素c.empty() //c.insert(d,b,e) //在 d 之前插入[b,e)之间的内容c.erase(iter)c.erase(b,e) // 擦除[b,e)之间的内容 4. 字符串的有用方法和cctype文件中对字符的判定123456789s.substr(b,j) // 复制[b,b+j)之间内容getline(is,s) //读一行到 s 中#include<cctype> //使用这个头文件包含对单个字符的判断isspace(c)isalpha(c)isdigit(c)isupper(c)toupper(c)","link":"/2020/11/05/CPP/06_%E4%BD%BF%E7%94%A8%E9%A1%BA%E5%BA%8F%E5%AE%B9%E5%99%A8%E5%92%8C%E5%88%86%E6%9E%90%E5%AD%97%E7%AC%A6%E4%B8%B2/"},{"title":"乐理知识","text":"根据 doyoudo 的教程整理的笔记 第一节:节拍 拍:描述音乐节奏的基本单位,一拍就是一次击打 速度:beat per minute 每分钟有多少拍 小节: 根据一定数量的拍就能确定一个小节 音符时值:每个音符占据一个小节的长度,占据全部长度的是全音符,占据1/2长度的是半音符,其他四分音符,八分音符则以此类推 4/4 节拍描述:分子表示,每小节拍的数量(四拍),分母表示每一拍的音符时值——以四分音符为一拍(为什么感觉上下描述的是一个意思/捂脸) 第二节:键盘 基础认知:钢琴键盘上左边的音低,右边的音高,一共由88个键组成,每个键之间的音调相差半个音,即黑键和白键之间,白键和白键之间都相差半个音 键盘的组合方式:在键盘上,会以下图为 pattern 进行重复,每一个 pattern 代表一组音,从C到B。因此由此可以重复组成钢琴的88个键其中白键从C开始命名,黑键则以和白键之间的关系来命名,其中一个黑键因为相对位置可以有两个命名 音的分组:最高音是小字五组的$C^5$,依次向下为小字四组,小字三组,小字二组,小字一组,小字组,大字组,大字一组,大字二组。其中中央C为小字一组的C,也称为C5 第三节:调 调的组成:哪些音是属于这个调的, 一个调代表了一种模式(音区的迁移不会对调产影响);调中哪些音是稳定的,哪些是不稳定的;音的倾向性 调的模式—C大调->大调式(全全半全全全半):基于钢琴之间相邻的键之间相差半音,那么我们从C开始,就可以以大调式(如下图所示)从一个组走到另外一个组。这样从C开始的一组音称为C大调,C大调全部由白键组成,而由此生成的D大调则由白键和黑键交替生成,虽然看上去不同,但是模式都是一样的,那么调也是一样的。其中升号调中标志音符要用黑键的升号名称#,降号调中黑键则用flat降号名称表示。 升号调有:G D A E B F# C# 降号调有:F Bf Ef Af Df Gf Cf 调中音的稳定性: 音的倾向性:不稳定的音倾向运动到稳定的音 第四节:音程定义:音程是描述两个音之间的距离,我们按照把白键区分成度,从一个白键走到另外一个白键闭区间包含了几个音就是几度。但是我们可以发现,不同的区间可能包含的白键数目相同,但是黑键数目不相同,这样这两个区间的名字也会不同。因此,根据每个区间中包含的半音个数,我们可以根据下图中的表将其区分为不同的度。 大小度:举例:大二度和小二度,其中二度代表这个区间中包含了两个音。大二度包含的半音个数有两个,比小二度包含的半音个数一个要多,所以分别称为大二度和小二度。 纯度:有句口诀是一四五八无大小,即一四五八度没有大小之分,即这些度中包含的半音数都相同,其中一度代表只有自己,八度代表跨了一个音区,四五度其实也并不是没有大小,在包含六个半音的时候,既可能是四度,也可能是五度 音程的转位:类似于从4到7和由7到4,称为音程的转位规律: 度数相加等于九:转位前后的音程相加之和有9度 大小增减是互换:大六度转位之后变成小三度 纯音程不变:纯的还是纯的 协和音程:协和度排序 第五节:和弦就是根据不同的音程组合成的几个音的组合。 一般是相隔三度的三个音组成三和弦,在三和弦的基础上加上大七度或者小七度的音就可以组成七和弦。除此之外还可以相隔其他的度组成和弦 三个音的和弦结构: 大三和弦:由根音出发,依次由大三度和再隔小三度的音组成的和弦称为大三和弦,如以根音C出发,则写为C 小三和弦:由根音出发,依次由小三度和再隔大三度的音组成的和弦称为小三和弦,如以根音D出发,则写为Dm 减三和弦:由根音出发,依次由小三度和再隔小三度的音组成的和弦称为减三和弦,如以根音B出发,则写为BdimC 大调中,可以组成以下几个和弦 四个音的和弦:通过在三和弦的基础上叠加和第一个音相差7度的音,就组成和七和弦 大大七和弦:在大三和弦的基础上叠加大七度的音 小七和弦:在小三和弦的基础上叠加小七度的音 属七和弦:在大三和弦的基础上叠加大七度的音 小大七和弦:在小三和弦的基础上叠加大七度的音 和弦转位在和弦中,通过改变三音和五音的位置,我们就可以获得转位的和弦,转位的和弦以(/根音)来表示转的哪一位,如下图所示为三和弦转位的结果。七和弦也可以进行转位,获得不同的和弦 五度循环圈用于告诉升号调和降号调","link":"/2020/09/27/Music/%E4%B9%90%E7%90%86/"},{"title":"【Accelerated C++】 -- 04/16 使用批量数据","text":"学习要点: 批量数据的读取和保存,使用vector double 和 float 类型辨析,变量类型的转换和隐式初始化 使用流控制器作为判断条件 格式化输出:使用 streamsize 读取 iostream 精度并用流控制器 setprecision 函数设置 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950#include <iostream>#include \"algorithm\"#include \"iomanip\" //include setprecision#include \"string\"#include \"vector\"#include \"ios\"using std::cout; using std::endl;using std::cin; using std::sort;using std::string; using std::vector;using std::streamsize; using std::setprecision;int calGrades(){ cout << \"please enter your first name: \"; string name; cin >> name; cout << \"Hello\" << name << \"!\" << endl; cout << \"please enter your midterm and final grades:\"; double midterm, final; cin >> midterm >> final; cout << \"enter all of your Homework grades,\" \" followed by end of file\"; vector<double> homework; double x; while (cin >> x){ homework.push_back(x); } typedef vector<double>::size_type vec_sz; vec_sz size = homework.size(); if (size == 0){ cout << endl << \"you should enter your grades\" << endl; return 1; } sort(homework.begin(),homework.end()); vec_sz mid = size/2; //result in integers double median; median = size%2 == 0 ? (homework[mid] + homework[mid-1])/2 : homework[mid]; streamsize prec = cout.precision(); cout << \"your final grade is\" << setprecision(3) << 0.2 * midterm + 0.4*final+0.2*median << setprecision(prec)<<endl; return 0;} 1. 批量数据的读取和保存,使用vector对于批量数据,我们往往不知道长度,并需要对其进行快速的索引操作等。因此需要使用一个特殊的数据结构进行保存,这小节用到的则是 vector 向量。良好的数据结构来保存批量数据统一了数据之间的关系,例如索引-数值,名字-数值等,帮助我们优化程序的书写。 1.1 vector 向量基本操作 模板类初始化:在建立新的 vector 时,使用 <> 括入 vector 内变量的类型,一个 vector 内的变量类型都是一致的 123#include \"vector\";using std::vector;vector<double> homework; // vector<string> 可以用来保存字符串 增加新变量: push_back 是vector 类中的成员函数,会增加一个新的元素到队尾,长度加一,并完成扩充的操作 12double x;homework.push_back(x); 获取属性:使用.begin; .end;.size;可以用于获取开始,结束和长度等基本属性 1234#include \"algorithm\";using std::sort;sort(homework.begin(), homework.end()); // sort 函数两个参数,代表排序向量的开始元素和结束位置,// 函数执行结果仅仅调换了被排序向量中的数字,原地排序为非递减序列 为了确保我们的向量长度可以正确保存,并独立于不同的系统之外都能正确运行,使用 vector 的内建变量进行尺寸的保存;这个类型是无符号整数类型, 默认不会存储负数,因此使用时要格外小心不要让其保存负数结果 12typedef vec_sz vector::size_type; // typedef 和其他名称会有相同的作用域vec_sz size = homework.size() // 两个都是size,但是一个是成员函数,一个是自定义变量,因为定义域的不同,我们不至于混淆 索引操作:直接根据 index 索引即可 123mid = size/2 //默认为截取除法median = size%2 == 0 ? (homework[mid] + homework[mid-1])/2 : homework[mid]; //条件语句,根据截取除法的结果获得 median 的结果 2. double 和 float 类型辨析,变量类型的转换和隐式初始化12double sum = 0; // 0 是整数,但是为了初始化双精度浮点,进行了隐式的类型转换int cnt = 0; 计算浮点数的时候,有float 和 double 类型都可以保存,分别是单精度和双精度浮点,现在从运行速度和精度的角度考虑,我们都应该采用双精度浮点,单精度浮点是为内存较少时服务。 当我们不显示地初始化变量时,就会初始化一个随机数值,这样的操作之后只能进行赋值,否则会带来错误的运行结果1double ssum; 3. 使用流控制器作为判断条件cin流控制器在条件判断中被转换成立一个bool值,这个值的结果取决于最近一次读取数据是否成功 12double X;while(cin >> x){ STATEMENT} 我们使用流控制器作为判读语句,当我们正确地读取了结果之后就会产生三个效果,否则则跳过。 cin 读数据到x 返回 cin 条件判断为真,进入块语句执行 什么情况会导致读取失败呢? 遇到了 end-of-file 符号,或者在这个例子中,读取了非 double 类型的变量 4. 格式化输出:使用 streamsize 读取 iostream 精度并用流控制器 setprecision 函数设置在我们输出的时候,为了能设置精度,则会采用 setprecision 流控制器 控制输出的有效位数1234streamsize prec = cout.precision(); //读取cout当前精度cout << \"your final grade is\" << setprecision(3) // 流控制器,控制后续输出有效位数为3 << 0.2 * midterm + 0.4*final+0.2*median << setprecision(prec)<<endl; // setprecision 流控制器,回复输出精度为之前的有效数字","link":"/2020/10/15/CPP/04_%E4%BD%BF%E7%94%A8%E6%89%B9%E9%87%8F%E6%95%B0%E6%8D%AE/"},{"title":"【Accelerated C++】 -- 02/16 使用字符串","text":"这一节,利用字符串读取和输出的程序,了解字符串的相关操作,常量,和变量的声明以及初始化等概念。 学习要点: 变量和对象的区分 字符的基本操作和其他操作 缓存区刷新,运算符重载等 1234567891011121314151617181920212223#include <iostream>#include <string>int main(){ std::cout << \"please enter your first name: \"; std::string name; std::cin >> name; const std::string greetings = \" Hello, \" + name + \"! \"; const std::string spaces(greetings.size(),' '); const std::string second = \"*\" + spaces + \"*\"; const std::string first(greetings.size()+2,'*'); std::cout << std::endl; std::cout << first << std::endl; std::cout << second << std::endl; std::cout << '*' << greetings << '*' << std::endl; std::cout << second << std::endl; std::cout << first << std::endl; return 0;} 变量和对象,字符变量在使用 std::cin 读取输入之前,我们使用了标准库种的 std::string 类来声明了一个字符串变量,用于存储 std::cin 的内容。 1. 什么是变量? 变量和对象?变量是有名称的一个对象,对象则是计算机内具有类型的一段存储空间,对象的类型决定了可以对对象进行的操作和操作的结果。变量和对象之间通过两种方式进行区分: 变量是一种有名称的对象,这样可以用来被编译器检测是否存在命名错误 变量存在事件有限:在花括号中声明的变量的存活时间限于},之后就被销毁和回收,这就是一种局部变量 除了区分点,变量继承了指定对象接口中的所有操作,如我们下面列举的 std::string 类型的操作就可以被任何 std::string 类型的变量使用。 2. 字符变量的声明和操作字符变量的声明有四种形式: 直接声明,并隐式初始化为空字符串,对于其他类型则会初始化为其他的值 1std::string name; // name = '' 声明的同时赋值初始化 1std::string name = \"a string\"; // name = \"a string' 使用方法来重复字符直接量来构造字符:系统环境会根据表达式构造一个变量。其中字符直接量的类型是内建类型char,字符串的内建类型则复杂得多 1std::string spaces(name.size(),' '); // spaces = ' ' 使用const 关键字,建立常量变量,其在变量存在的时间内不会改变数值,并且需要在创建时就赋值初始化,否则之后不会有改变的机会;初始化常量时,我们也可以使用变量进行初始化 1const std::string name = \"I am a const\" + name; 3. 字符变量的其他操作处理初始化操作之外,我们还可以对为std::string 类型的变量进行一些其他的操作: 输入字符到当前变量:输入字符时会忽略输入头部的空格,tab,换行符等,并持续读入非空白输入,直到遇到下一个空格,接下来的输入归入到下一次输入,并返回std::cin,因此我们可以使用其进行链式输入 1234std::string name1;std::string name2;std::cin >> name1;std::cin >> name1 >> name2; // 链式输入 将结果写入到输出流:返回输出流 12std::string name;std::cout << name; 将字符写入输出流时,其实我们是将其写入了缓冲区,缓冲区的设计是为了优化多次频繁输出开销很多的设计。 在运行一个时间复杂度较高的程序时,我们需要时常刷新缓存区来获得及时的输出,刷新缓存区的三种方法: 读取输入:在读取输入时,缓存区内容就必须先输出然后再读取输入 控制器刷新:使用std::endl可以进行缓存区的刷新 缓存区满:当缓存区满了会自动刷新 获取size: 可以读取字符串中字符的个数,这里只包含非空字符的个数 12std::string name;int size = name.size() 使用“+”和字符串或者字符串直接量相连接:其中+字符和在3+4表达式中有不同的含义,这样的情况我们就称为运算符重载 12std::string name = 'string';std::string = \"a\" + name;","link":"/2020/09/16/CPP/02_%E4%BD%BF%E7%94%A8%E5%AD%97%E7%AC%A6%E4%B8%B2/"},{"title":"【Accelerated C++】 -- 03/16 循环和计数","text":"学习要点:op 针对之前的打印框架进行改进,从而减少存储并把输出结构的变化更加灵活 掌握 while 循环的循环不变式 三种精简程序的方法: 预定义 std 变量,for 循环精简,逻辑压缩精简 从零开始和从1开始计数的差异 其他:使用类中的类型来作为变量的类型 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051/* 打印形如***************** ** Hello, daisy ** *****************的greetings */ #include <iostream>#include <string>using std::cin; using std::endl;using std::cout;using std::string;int main(){ cout << \"please input your name\"; string name; cin >> name; cout << \"please input the pad size you want\"; int pad = 0; cin >> pad; const string greetings = \"Hello, \" + name + \"!\"; const int rows = pad*2 + 3; const string::size_type cols = greetings.size() + pad*2 + 2; cout << endl; // 循环不变式:已经打印了r行 for(int r = 0; r!=rows; ++r){ string::size_type c = 0; while (c!=cols){ if(r==pad+1 && c == pad+1){ cout << greetings; c += greetings.size(); }else{ if(c == 0 || c == cols-1 || r == 0 || r == rows -1) cout << \"*\"; else cout << \" \"; ++c; } } cout << endl; } return 0;} 1. 更加灵活的打印框架由于我们使用的打印框架有固定格式—在周围包‘*’号,中间打印greetings, 并在之间用空格包围, 我们可以直接通过行和列的位置计算出打印的符号。通过这样的思想我们可以相较于之前: 节省对于每一行结果的保存 灵活调节空格 2. 使用 while 语句 形式: 12while (condition) statement //statement 可以是由{}框起来的块语句,也可以是一个普通的语句 由于循环的概念相对熟悉,我们仅仅讲述如何设计 while 语句,也就是确保循环不变式的成立 循环不变式: 循环不变式是在每次 while 语句测试条件的时候,这个条件一定为真,从而这样能确定程序的运行和我们的意图相符在示例程序中,循环不变式是已经打印了 r 行。也就是通过初始化和循环体的执行,我们循环不变式成立,因此循环结束,就打印了rows行 3. 精简程序 预先定义namespace: 在程序中,我们有很多使用 std::name 的场景,我们可以对其进行一定的精简,即: 12using std::cout; //声明我们的cout 是std 中的,这个声明随着我们使用位置的不同,也会有不同的定义域using namespace std; // 引入std 中所有的命名,这个和 import * 类似,一次引入了太多的变量,并有可能带来模糊(不同定义的同样名称变量) 使用 for 循环代替初始化和累加操作:每次使用while 循环,一般都需要初始化控制变量,并每次累加,我们使用精简的 for 循环也可以实现 1234567891011for(init_statement condition; expression) // init_statement 只会执行一次,并且退出for 循环之后,这个变量不再存在 statment//等效于下面{ init_statement while (condition){ statement expression }} 压缩检测: 程序中多次出现的语句,有时可以通过逻辑进行优化 4. 从零开始和从1开始计数的差异 从零开始计数可以直接计算出 [m,n) 区间中包含n-m个元素; 表示空区间时更方便:从 0 开始是 [n,n), 而从 1 开始是 [n, n-1],末尾比开头小可能会有很多麻烦 循环表达式更加好表达和理解: 从零开始: 已经输出 r 行 从1开始:已经输出 r-1 行 程序状态更好理解:从零开始能保证输出了 row 行,而从 1 开始的r<=row 只能保证 r >row,因此不确定最终输出了多少行。 5. 其他: 使用类中的类型来作为变量的类型: 因为对于输入字符长度的不确定性,我们可以使用string 中的类型来定义变量。 当我们需要定义一个变量来保存特定数据结构的大小,就应该养成使用库位特殊用途而定义的类型的好习惯 12using std::string;string::size_type cols = greetings.size() + 2; // greetings 是输入的字符,可能很长,这个长度 int 不一定可以包容 累加符号:++r; 由于变成后续数值和重新计算任意数值的概念形成鲜明对比,因此这个概念时很有意义的 运算符的优先级:在书写表达式时,其结果会随运算符的结合性和优先级产生不同的结果,其中只有赋值运算符和单运算符时右结合的。这里,运算符的优先级和 python 相差不大 只需注意,++x 返回 x + 1, x++ 返回 x即可","link":"/2020/10/14/CPP/03_%E5%BE%AA%E7%8E%AF%E5%92%8C%E8%AE%A1%E6%95%B0/"},{"title":"五线谱","text":"根据 doyoudo的教程整理的笔记 第一节:识谱 乐符:五线谱中的基本组成元素为乐符,其中符号由下往上分为符头,符干和符尾,这三者用来区分不同的音符时值,只有一个空心符头则称为全音符,下面是各种不同时值音符的表示方式 谱表:谱表有五根线,四个间,每条间和线可以放一个白键的音 乐符在谱表上:当乐符在谱表上方时,符干朝下,符头朝右;当乐符在谱表下方时,符干朝上,符头朝左;当有符尾时,无论在上方还是下方,一直都朝右 高音符表:下面这张图代表了高音符表,这个表上从下到上每根线代表的是: e g b d f 第一个音符代表小字一组的e 低音符表:下面这张图代表了低音符表,这个表上从下到上每根线代表的是: G B d f a 第一个音符代表大字组的G 中央C:可以在高音符表和低音符表下方和上方,加一根线,表示中央C,如下图所示 调号的辨认和书写 调号的定义:调号用于辨认五线谱中的全部半音,如果单个音符是半音的话,我们可以将调号写在音符前面,代表他的音符变化,如果将调号写在五线谱的前面,那么就代表在谱中对应符号的调全部要发生变化。同时,在调号中全部都是升号,或者全部都是降号,没有升号和降号混合的情况 调号的辨认和对应:下面的图代表了不同升号和降号数量下的大调。既然调号代表谱中的每个特定音都要升/降,那么怎么知道是那些音升/降呢? 我们可以通过下图的对应方式找到调号对应的调,再根据调中哪个音升降了,判断升/降的音。 例如:有一个降号的是F大调,根据按全全半全全全半的方式,从F音开始,得到的结果为 F G A Bb C D E,也就是谱中所有的 B 都是 降b 我们也可以使用五度循环圈和升/降号的个数得到调号和对应的升/降音:首先,我们可以根据升降号的个数找到对应的调,升降的音就是根据个数走过右边的循环圈即可。 例如:观察谱中有一个降号,因此在降号调中对应到F大调, 然后1个降号,就是降的B。 快速找主音:可以根据下面的口诀,快速找到调号中的主音,主音的确定也可以帮助我们迅速找到哪个是升/降音升:比最后一个升号高半音;降: 倒数第二个降号 等音调有几个调在五度循环圈中在相同位置,代表他们的音是相同的,称作等音调 附点音符单附点:时值延长一半复附点: 时值延长3/4 休止符不同休止符有不同的样子 这是全休止符,在最上方 从右到左是二分,四分八分,十六分休止符 音值组合法 定义:按照几个K分音符为一拍的方式,将1拍的音符组合成一个音符。当采用四分音符为一拍时,下面两种表示是等效的:当我们使用四分音符为一拍的情况下,十六分音符,八分音符,四分音符分别代表1,2,4。同时16分音符有两个短横线,八分音符用横杠连起来,四分音符就是本身。这样的组合,会更加的和谐 注意事项:第一拍和第三拍要立即看清楚,如果需要在第二拍分割的地方有超过第二拍的音符,可以加一个延音线,使得我们可以获得音符又不模糊长度。延音线是将两个或者多个同样音高的音符连接起来的一条线,只发第一个音符的音","link":"/2020/09/27/Music/%E4%BA%94%E7%BA%BF%E8%B0%B1/"},{"title":"弹琴姿势","text":"坐姿:坐下弹琴时,上身直立,坐琴凳三分之一,坐在琴键的正中间位置。方便自己可以更好的使用到钢琴所有琴键 手部姿势 不要晃压手腕:手掌不要下压,也不要过度抬起,正常平放 抬手和收手:开始时,手轻轻抬起,远离键面;结束时,手正常收回 连续按键:按下一个键时,其他手指可以抬起;在按下下一个键之后,再松开之前的键,从而形成连续而不松散的发音 指型:手指直立按键,不要折指,也不要过于内扣,大拇指使用指尖进行弹奏 手臂:在固定键位弹奏时,注意手部不要晃动。只有在真正放松的状态下才能演奏出优美的音色。","link":"/2020/09/27/Music/%E5%BC%B9%E7%90%B4/"},{"title":"思考","text":"","link":"/2020/09/09/%E6%97%A5%E6%9C%89%E6%89%80%E6%80%9D/post/"},{"title":"见闻","text":"","link":"/2020/09/09/%E6%89%80%E8%A7%81%E6%89%80%E9%97%BB/post/"},{"title":"Domain adaptation - CAG_UDA","text":"Category Anchor-Guided Unsupervised Domain Adaptation for Semantic Segmentation中心思想:核心思想为基于源域类别 Anchor 的分布对齐,实现两个域之间类内距离减小,类间距离增大的目的,更加利于生成分界面,同时使用对目标与分配伪标签的方式促使分界面不从数据中心穿过,也减少分类器对源域的偏爱。 类别层次的特征对齐:基于源域和目标域相同类别的特征向量在特征空间中距离较近的假设,把源域的每个类别上计算类别的平均值当成是类别中心,并促使源域的同一类别特征向量和目标域的激活特征向量向类别中心靠拢。 提升模型泛化能力:基于源域 Anchor 给激活的目标域特征分配伪标签,分类器使用伪标签进行训练,促使分类边界也根据目标域的标签进行相应的调整。 这个方法可以解决全局特征对齐带来的类别没有对齐的问题,通过向激活向量,即在目标域中原有特征空间中和源域某一个类别中心较为接近,和其他类别中心的距离均超过一个设置的门限的特征向量,分配对应 cluster 的伪标签,实现两个域类内距离减少。 主要步骤: 建立源域类别Anchor(CAC): 1.由于源域的图片都具有标签,那么,在fD之后的特征向量上,筛选出相同类别的特征向量(维度:num_channel 1 1),并计算均值得到类别Anchor—fcs 根据类别Anchor明确目标域中的active target instance(ATI): 1.active target instance的选择方式即目标域中的fD之后的特征向量和某个fsc的距离较近,和其他fsc的距离的差值都超过了一个设定的门限(这个门限需要人为设定优点僵硬,其实可以换成比例这样的形式?) 给active target instance分配伪标签(PLA): 1.active target instance的标签就是距离最近的那个类别 建立距离和分割损失约束,进行分布对齐和分类边界优化。 首先,需要建立一个适应源域特征的好的分割模型,从而基于源域的信息迁移到目标域。即先在源域上对分类器上进行训练,这里涉及了源域的分割损失: 对源域和目标域的分布进行一个整体对齐的操作:可以有多种多样选择,判别器对抗训练,输入风格迁移等 采用stage的方式训练。每个stage: 约束源域的sample都尽量靠近对应类别的anchor: 约束分类边界和源域贴合 约束目标域的active sample贴合源域的Anchor,从而目标域细节分布靠近源域 约束目标域伪标签下的分割损失,从而分类边界依据目标域对齐后的active sample进行调整 优势: 清晰明了的方式说明如何进行类内对齐并通过判别器损失提升类间距离,整体流程很自然。 使用anchor的形式,相较于使用分类器的预测结果获得伪标签而言,减少了分类器的bias,可以对比由概率生成的伪标签,可以看出其能更加正确生成行人等标签,是一种互补的形式 在目标域通过伪标签的形式加强了监督,提升了效果 Limitations: 目标域和源域的数据分布假设强烈:源域和目标域相同类别的特征向量在特征空间中距离较近的假设过于强烈,尤其是对于源域和目标域原本相差较大的情况下,上述对齐方法如作者在文中提到的实际上会得到更差的结果。因此需要先对源域和目标域进行一个基础对齐工作,才能进行类内对齐,即需要一种coarse-to-fine的特征对齐方式。 非激活向量的处理:文中仅仅对激活向量进行了操作,而没有考虑目标域中的非激活向量,尽管作者说非激活向量会随着激活向量对齐的同时同样地对齐,但是说服力较低,也没有一个严谨地阐述,因此,我们需要对非激活向量进一步操作,例如进行熵最小话这样的操作。 像素代表实体:作者使用一个基于像素点的均值来代表全体像素点,但是像素的信息相对于一个语义实体来说,有点过于省略,也许我们可以使用多个聚类中心来代表语义实体,来促使anchor更加具有代表行。另外还可以思考如何利用大大小小类别的结构信息, 类别不均衡:由于距离损失和交叉熵损失中还是以像素为单位进行累计,因此还是可能会存在类别不均衡问题","link":"/2020/09/15/%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0/CAG_UDA/"},{"title":"【Accelerated C++】 -- 05/16 组织程序和数据","text":"学习要点: 根据逻辑组织数据和程序分块:结构体和头文件(函数声明和定义,函数的重载和预处理) 不同传参类型的对比:传参数,传引用,传常量引用 异常处理:捕捉和报错 读取输入时的注意事项: 存值变量清空,确保输入字符流的正确,重置输入流 库工具: 比较字符串,设置宽度 1. 根据逻辑组织数据和程序分块:结构体和头文件(函数声明和定义,函数的重载和预处理) 结构体:将不同数据关联起来的一种形式,可以通过自己定义获得各种关联,类似 python 的类12345struct Student_info{ std::string name; double midterm, final; std::vector<double> homework;}; //分号一定要有 头文件:用于存放数据结构的定义和使用这个数据结构,或逻辑独立的其他函数的声明;需要对应有源文件,对应的源文件中包含对应头文件,并给出头文件中声明的函数的定义。 1.作用:通过不同的头文件执行程序中不同的逻辑分块,并保持头文件之间的逻辑独立和不假设性,保证了最终主程序的简洁和子程序的功能模块化 2.避免重复的检查:当给定头文件可能由于递归被多次包含在主程序中时,采用下面的方式防止同一函数从重复定义和声明 123#ifndef ACCLERATEDCPP_STUDENT_INFO_H //放置在头文件的第一行,用于检查函数中的声明是否重复定义#define ACCLERATEDCPP_STUDENT_INFO_H#endif //ACCLERATEDCPP_STUDENT_INFO_H 3.头文件的简洁性:因为头文件会被用户重复使用,因此我们不能在头文件中使用不需要的声明,因为可能这种声明并不是用户需要的,而是对于每个变量采用 std::var 的形式 函数的重载:我们可以使用相同的名字来命名不同的函数,这个就是函数的重载,但只要不同的函数在返回值,参数个数,参数类型中有一个不同,就会被认为值不同的函数 2. 不同传参类型的对比:传参数,传引用,传常量引用12345678910111213//传参数:对原参数进行复制,创造局部变量,不影响其他参数int fun(vector<double> v){ statement}//传引用:将原变量重命名传入函数,函数内对参数的修改会传递到函数外,可以用于返回多变量,且不复制,减少内存// 传入的引用必须为左值,即非临时变量,因为函数内的影响需要反应到函数外。int fun(vector<double>& v){ statement}//传常量引用:传入不复制的原参数,但是是常量,因此不能修改,也可以减少内存消耗int fun(const vector<double>& v){ statement} 3. 异常处理:捕捉和报错12345678try{// statement that could fail }catch(error) cout << error.what() //e.what 返回一个报告问题所在的值if(s.size() == 0) throw e // throw error when there is a logic error domain_error: 数据类型不匹配注意:异常之后的语句将不会被执行;一个可能引发异常的语句不应该再有其他副作用 4. 读取输入时的注意事项: 存值变量清空,确保输入字符流的正确,重置输入流12345678910111213141516171819istream& read_hw(istream& in, vector<double>& hw){ if (in){ //以防预先已经出错 hw.clear(); // 清楚之前的数据 double x; while (in >> x) hw.push_back(x); // 最后重置输入流的状态,清除之前的错误标记,从而输入可以继续 in.clear(); } return in;}istream& read(istream& is,Student_info& s){ is >> s.name >> s.midterm >> s.final; read_hw(is,s.homework); return is;} 5. 库工具: 比较字符串,设置宽度 比较字符串:字符直接><=比较,得到字典排序法的比较结果。用于自定义 compare 参数并传入 sort 函数 排序函数的谓词: 通过排序函数的谓词函数,即一个产生真值的比较函数,用于定制化的比较。 1234sort(students.begin(),students.end(), compare);bool compare(const Student_info& x, const Student_info& y){ return x.name<y.name;} 设置宽度: s.width:将流 s 的宽度设置为 n,输出的右边被填充成给定的长度,之后会使用s.width(0)重置 setw(n):返回streamsize的值,作用于输出流时,和s.width一样,作用效果只有一次1setw(2) << student.name","link":"/2020/10/17/CPP/05_%E7%BB%84%E7%BB%87%E7%A8%8B%E5%BA%8F%E5%92%8C%E6%95%B0%E6%8D%AE/"},{"title":"注意力机制","text":"1. 简介2. SEnet (Squeeze-excitation network)3. SKNET, CBAM 等1. 简介定义:通过一定方式使得学习过程中仅仅关注部分信息的的手段都可以称作注意力机制,其可以让网络仅仅关注某些有用的信息,获取了关键信息就可以使用更加少的参数获得更加好的效果。 1.1 在 CV 中的应用方式: 我们知道图像中的信息就是特征图,那么对信息筛选就是对特征图进行筛选,这里的筛选可以在空间层面和维度层面,筛选的手段就是进行加权。 空间层面(Where ?):使用维度为(BS,1,H,W)的加权向量,对不同空间位置的信息进行筛选 维度层面(What ?): 通常我们认为不同维度代表了不同卷积核的提取的特征,那么同一维度都是同一个特征提取器获得的,也就是输入特征图中所有有这个特征的位置。对不同维度的信息进行筛选就是采用维度为(BS,channels,1,1)的向量为每一特征图进行加权。 1.2 如何确定筛选权值: 目前我们知道注意力机制是通过加权的方式在空间和维度层面对特征图进行筛选,那么怎么确定权值呢?权值代表哪些信息重要,哪些信息不太关键,那么怎么来决定呢?这里显然需要根据任务来决定,当使用哪些信息我们获得更好的最终结果,哪些信息就是更关键的信息。 流程: 特征图生成权重:由于我们需要根据特征图判定重要性,那么我们建立一个以特征图为输入的浅层神经网络,其输出当作权值 权重作用于特征图:上一步生成的权值通过 broadcast 加权到特征图上,得到加权特征图 权重反馈:网络根据加权的特征图进行结果的预测,得到损失,进行反向传播,这里的损失就能让我们确定第一步生成的权重是否正确以及如何调整权重 循环1-3步就能获得越来越好的权重。 2. SEnet (Squeeze-excitation network)接下来我们来介绍一下基于 channel 维度进行注意力机制的 SEnet,这个视频讲解的比较清楚。 SEnet的流程主要分为三个部分: 压缩:对上一步的特征图进行全局池化,获得包含有全局感受野的特征向量。 激励:我们将这个特征向量传入两层 FC 的浅层神经网络获得权值 乘积:将权值乘到之前的每个特征图上就获得有不同注意力特征图 由于注意力机制是通过额外的神经网络进行支撑,因此我们可以将其加到任何的神经网络中,例如加到 Inception 和 Residual net的效果则如下图所示: 3. SKNET, CBAM 等见 SKnet 和 CBAM","link":"/2020/09/09/%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0/%E6%B3%A8%E6%84%8F%E5%8A%9B%E6%9C%BA%E5%88%B6/"},{"title":"Domain adaptation - Bidirectional Learning for Domain Adaptation of Semantic Segmentation","text":"主要思想在图像变换和分割网络中融入perception loss 来减少不同特征对分割网络的影响,随后使用双向学习和自监督学习提升网络的泛化能力,并使得两阶段网络不断互相促进。其中双向学习使得图像变换网络和分割网络不断迭代更新,相互促进优化,自监督学习使用分类器输出结果给目标域图片分配伪标签来约束分割网络 流程步骤 建立三大损失来约束 Cycle GAN: 变换结果不影响语义分割的感知:将S和T分别变换之后经过分割网络获取的特征用于计算感知loss,即迁移的结果在分割网络的特征提取下在感知loss衡量下相近,即特征相近 GANloss:迁移后S’和T的分布相近 reconloss:迁移后再迁移回来,信息没有损失 使用cycle GAN 进行源域利用风格迁移到目标域,并用分割损失和对抗损失进行模型约束和特征对齐。 模型约束:迁移后的图片使用分割网络利用标签信息进行分割 特征对齐:同时增加对抗损失促使变换后的源域特征和目标域的一致。 使用分割网络的概率结果,生成自监督学习伪标签 模型约束:分割模型在输出较大概率的当成是图像的伪标签,从而使用这个伪标签进行图像约束,在对模型的约束上增加了一层 由前三步组成两层循环进行训练 外层循环主要训练好变换网络,并用源域信息和特征对齐损失监督分割模型 内层循环使用不断优化的分割网络来生成更多的伪标签来进行模型的监督。伪标签的生成使用门限进行筛选,概率超过门限的才计入损失当中 讨论: 作者通过消融实验证明,不断迭代进行多stage 的训练有利于精度的提升 同时引入SSL 能对精度有较大的提升。7%左右 在分配伪标签时,门限对于标签的影响较大,一开始较高的门槛有助于筛选不正确的标签,提升效果,随后更高的门槛则会减少有利的信息,导致网络难以学习。 优势: 每周期重置了伪标签,防止了错误的累计 双向学习有利于互相促进 Limitations: 目标域和源域特征相似,同时源域变换前后的特征也一致:特征对齐中对特征的相似性要求过多,同时对抗损失,即源域和目标域的特征对齐上没有精细到类别层次 网络结构太大,不利于反向传播","link":"/2020/09/15/%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0/BDL/"},{"title":"线性回归","text":"线性回归(基于神经网络+梯度下降)定义:基于特征和标签之间的线性函数关系约束,线性回归通过建立单层神经网络,将神经网络中每一个神经元当成是函数关系中的一个参数,通过利用初始输出和目标输出建立损失,并优化损失最小的方式使得神经元的数值和真实函数参数数值最相近,从而通过网络训练得到最符合数据分布的函数关系。 实施步骤: 初始通过随机化线性函数的参数,通过输入的x,会得到一系列y_h 输出的y_h和真实值y之间因为神经元参数不正确产生差距,为了y_h和y能尽量地逼近,我们通过平方误差损失函数(MSE Loss)来描述这种误差。 类似于通过求导得到损失函数最优解的方式,我们通过梯度下降法将这种误差传递到参数,通过调整参数使误差达到最小 通过几轮的训练,我们得到的最小的损失值对应的神经元数值,就是描述输入输出的线性关系的最好的参数。 要点: 确定输入输出之间一定满足线性关系,这一点可以通过对x,y画图看出,只有线性关系才能使用线性回归训练得到 由于线性关系唯一由神经元个数决定,不同的参数个数唯一决定了这种线性关系,因此需要选择适合的特征用于线性回归 最小二乘法(基于数据计算得到解析解的线性回归)参考shuhuai的视频 定义:最小二乘法是希望对 n 维平面的线性数据进行拟合得到输入输出的线性函数,其思想是建立一个线性模型 y = w_{1}x + ... + w_{k}x + b进行预测并使得预测的损失最小,其中损失函数为 L = \\sum_{i}(y_{i}-y_{i}) = \\sum_i(y_{i}-w^{T}x_{i})我们可以采用多种方式对这个损失函数进行优化 1. 矩阵推导出最小二乘解析解将损失函数化成矩阵表示,之后令损失函数对w求导得0,求得最优解,过程如下: 2. 利用函数求导方式求得最小二乘估计解析解除了表示成矩阵的形式,我们也可以直接对损失函数进行化简,求得使得损失函数最小的参数值,这个部分更容易推导 离差是一个凸函数 对凸函数里面的参数求导得到全局损失最小值对应的参数 3. 最小二乘估计的集合解释 最小二乘法实际上第一种形象的解释就是求出最能拟合数据点的直线,而最能拟合则使用数据到直线的离差最小的方式来表示 第二种集合解释则是从线性代数的角度出发: 将 N * p维的数据 X 想象成一个 p 维子空间的的基, 由于 y 不能完全由这 p 维数据线性表出,因此 y 则在这个 p 维子空间的外面, 而我们求得的 $y^{hat}$ 则是这个 p 维子空间里离 y 最近的向量,即 y 在子空间中的投影, 而损失则是 y 和投影之间的差距,即投影的法向量, 这样使得法向量最小,可以求得和矩阵表达求解析解里面相同的结果。 4. 概率角度的最小二乘估计当使用 y = w^{\\top}x + \\epsilon\\epsilon \\sim N(0,\\sigma^{2})表达输入输出的关系,并进一步利用此关系进行最大似然估计时,我们发现可以得到和上述相同的损失的表达式,这也就说明,当使用最大二乘估计时,其概率视角就是在噪声为高斯分布的基础上进行最大似然估计的结果。","link":"/2020/09/09/%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0/%E7%BA%BF%E6%80%A7%E5%9B%9E%E5%BD%92/"},{"title":"Change Detection - Fully Convolution Siamese Network for Change Detection","text":"主要思想设计三种略有差异的网络,对OSCD 使用基于 Unet 的结构进行变化检测 流程步骤 建立三种不同的网络结构,分别为 FC-EF, FC-Siam-conc, FC-Siam-diff 分别将输入直接连接成不同通道,在传入两个相同网络之后在卷积层结合和在传入卷积层之后将原数据和差进行结合 将数据传入网络进行训练得到效果: 讨论: 数据集OSCD介绍,包含了 2015-2018 年的 24 对数据集,每对数据集具有几百到上千的不等边长,具有一定的参考价值 使用了 Unet 的跳接结构,并在第三种网络结构中获得了较好的结果,但是根据结果图标可知,较好的结果都在少量数据集上获得,而在数据量较大时,F1 score 只有 50 不到 综上,采用了几种不同的网络结构进行实验,但其本质差异不大,同时最终结果并不佳,需要根据数据进一步分析原因,并查看更新的文章。","link":"/2020/11/10/%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0/ChangeDetection_FullyConvolutionSiameseNetworkforChangeDetection/"},{"title":"Domain adaptation - review","text":"温故知新篇 — 领域自适应中的多种方法和内部含义浅析本文的行文结构如下,首先从定义出发描述,继而通过现有问题和数学模型开始理解什么是领域自适应问题。基于理解,我们罗列现有的几大主流领域自适应方法,并使用我们的数学模型辅助理解,提出笔者的一些思考。 领域自适应定义:现有深度学习模型使用源域的知识,并运用一系列领域自适应方法,提升其在目标域上的表现。基于研究领域自适应问题本质为研究泛化问题,其有众多的应用方向,包括但不限于分类,目标检测,语义分割等。 如何理解领域自适应: 首先,我们可以来看一下,什么样的情况会导致领域不适应;接着,我们来使用自己建立的数学模型来理解领域不适应时到底发生了什么;最后我们引出,基于这个数学模型,我们如何来理解现有的多种领域自适应方法。 领域不适应的情形:领域不适应,即神经网络的泛化性能较差,其在以下情形容易发生: 神经网络模型对于训练集分布过拟合:网络在训练集上的指标和测试集上指标差异达到5%及以上,并且随着训练的进行差异越来越大。 测试集分布和训练集分布差异较大:我们发现,在生成图像上训练的模型,在真实图片上训练效果很差。 这时,我们会说神经网络在测试集上的表现性能差,我们称这个神经网络模型的泛化性能差。那么,如何从数学模型的角度来理解泛化的问题呢? 数学模型解释泛化: 我们不妨先假设我们在进行分类问题,同时我们将数据的分布降为两维,而不是图像的千万维,那么分类问题即在二维平面上寻找一条分界线,将两个不同的标记的类别进行区分。 神经网络对于训练集过拟合:这个情形我们应当很熟悉了,此时分界线在训练集的不同类别之间交叉穿过,但是由于测试集的分布和训练集不同,因此产生了大量的错误分类。这时,我们的问题在于分界线的复杂度太高,超过了数据本身真实分布需要的分界线,或者可以说模型的复杂度和真实分界线的复杂度之间有维度差距。 测试集和训练集分布差异大:这时我们可以想象一个适当复杂度的分界线对训练集的不同类别进行分类,但是测试集和训练集的分布有差异。同样的类别,但是产生了分布偏移,甚至和原来的类别差异较远。说明当前用于训练神经网络模型的数据分布不能反映真实数据的分布。从而生成的分界线不能正确分割真实数据。 从以上分析我们可以看出,领域自适应的问题,本质就是数据分布和分界线的问题,一个良好的领域自适应方法应当同时兼顾对齐源域和目标域的数据分布,同时对分界线进行约束。从而源域和目标域的数据分布对齐良好,同时模型的分界边界远离数据分布并能泛化到其他相同分布数据。 现有的多种解决领域自适应的方法: 基于A review of domain adaptation without target labels [1] 这篇综述,我们按照下面三个方向进行分类: 基于采样的方法: 给予不同样本不同的权重,促使源域中分布和目标域不一致的部分尽量少地影响神经网络。这类方法不能对某一类数据的分布进行变换(平移,聚集等),从而不能优化源域和目标域数据同一类别特征有较大差异带来的影响,而只能通过改变分布的存在与否来间接地影响分类边界。 基于特征对齐的方法: 将源域和目标域的神经网络特征分布进行对齐,从而同样类别的物体能在特征分布空间中尽量地接近。这类方法应当是最灵活的,但是我们需要找到一个合适的方式来实现特征对齐。 基于推理的方法:通过在优化过程中加上额外限制的方式,从而估计神经网络的模型参数,促使神经网络学习的分界线满足限制。这类方法可以看成是对分界线的变换下面我们通过独立的模块,细致地描述这些方法 如何做到领域自适应做到领域自适应,关键在于如何基于源域数据上学习到的分界线信息,加上源域和目标域之间的差异信息,促使数据差异减小,分界线向更适应目标域的方向移动。 1. 基于采样的方法:基于采样的方法目的在于消除采集源域数据时的偏差带给神经网络的影响,我们希望采样的数据分布和真实的数据分布尽量一致,从而提升神经网络对真实数据的适应性。现代的数据方法往往通过预估真实分布中每个实例出现的比例来进行采样后分布的校正,但是在真实场景的数据往往没那么好估计。 数据层次重要性权重 类别层次重要性权重2. 基于特征对齐的方法: 子空间映射 最佳传输 域不变空间匹配 深度特征匹配 协同学习(Correspondence learning) 参考文献: Kouw, W.M., & Loog, M. (2019). A review of single-source unsupervised domain adaptation. ArXiv, abs/1901.05335.","link":"/2020/05/06/%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0/DomainAdaptation/"},{"title":"Change Detection - End-to-End Change Detection for High Resolution Satellite Images Using Improved UNet++","text":"主要思想基于 Unet++ 的网络结构,设计了多输出采用Sigmoid 融合的方式提升不同层次信息的利用,并使用 dice loss 减少变化检测中样本不均衡的影响 Unet++:和 Unet 同样采用了即在上采样下采样的方式,增加感受野提取更多信息,随后采用跳接的方式将抽象的信息和对应的低层的信息融合,不同的是,在 Unet++ 中,跳接采用了类似 densenet 的想法,将上采样相同大小的输出特征和之前的 concatenate,而之前层次是下采样到更浅尺度就上采样的结果,因此在另一个角度实现了多层信息融合,并累加的短路连接促使梯度更方便传播和网络训练的收敛。 MSOF:基于之前提到了多尺度下上采样,我们会获得多个最终特征,作者将多个特征经过Sigmoid得到的结果 concatenate 并最后使用 Sigmoid 融合,一定程度上提升了效果。 Dice Loss: dice loss 仅仅关注每个像素真值和对应输出概率的相似性,而不考虑样本的正负性,从而减少了变化检测中样本不均衡的影响 流程步骤 在给定数据集如下图上进行数据增广,包含旋转,平移,翻转,尺度变化等,将数量缺乏的变化检测数据集扩充,并获得了较大的提升 将数据传入 Unet++,并使用MSOF 得到结果 使用加权的二值交叉熵损失和 Dice Loss 计算结果: L = \\sum_{i=1}^{5}w_{i} L_{side}^{i} L_{side}^{i} =L_{bce}^{i}+\\lambda L_{dice}^{i} L_{dice}^{i} =1 - \\frac{2*gt*pr}{gt+pr}最终结果如下: 讨论: 从结果上来说,相比没有使用 Unet++ 和 MSOF 的 FC-Siam-diff 网络有最高 3.8% 的 F1 score 提升,不过,我们之前提到在 FC-Siam-diff 在 OSCD 上的效果不佳。因此,当前变化检测算法可能也会在不同数据集上有不同的表现 根据 MSOF 的提升效果,可以看出不使用 MSOF 的 Unet++ 相比之前的 FC-Siam-diff 可能并没有多少效果提升 目前的变化检测网络均使用是在分割的网络结构,这样说来,也许可以直接利用 SOTA 的分割网络进行训练,主要需要注意的是,变化检测数据集较小,因此需要减少网络规模,相比 20 对 1k 左右的数据,10M 以内的网络参数应该是比较适宜的。","link":"/2020/11/11/%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0/ChangeDetection_Unet++/"},{"title":"【Accelerated C++】 -- 07/16 使用库算法","text":"学习要点: 类型修饰符 static 类型 void 迭代器适配器 back_inserter, front_inseter, inseter 库算法: 求和,查找,复制,批量作用,分隔(满足条件/不满足条件分开) 讨论:库算法设计中的分离思想 — 灵活,高复用度;函数作为参数时,重载带来的困难 其他:使用迭代器构造vector/string;如果容器支持索引,那么它的迭代器也支持;向量的成员函数 empty — 比统计大小更有效率 1. 类型修饰符 static作用于局部变量,此变量在程序运行到其有效作用域时,会使用上一次该程序结束后保留的值,因此有 static 修饰符的变量只需要进行一次初始化 2. 类型 void当函数没有返回值时,使用 void 开头,此时可以不使用返回语句,或者 12void main() {return;} 3. 迭代器适配器 back_inserter, front_inseter, inseter传入有迭代器的变量,从而产生迭代器的函数,生成的迭代器可以让关联的容器动态地增长,从而作为复制算法的安全的目的地定义于 123back_inserter(c) // c 需要支持链表,向量和字符串支持的 push_back 操作front_inserter(c) // c 需要支持链表支持的 push_front 操作inserter(c, it) // c 需要支持链表,向量和字符串支持的 push_back 操作, 其在迭代器 it 之前插入元素 4. 库算法: 求和,查找,复制,批量作用,分隔(满足条件/不满足条件分开)通过对所有的容器(包含字符串)定义迭代器,同时库算法对迭代器使用同样的接口,保证了不同库算法对多种容器的通用性。这些库算法也被称为泛型算法,即调用它之前不知道他的参数和返回类型是而什么的算法现在我们来介绍这些库算法,除特殊说明,均定义在 中。 求和:accumulate(定义在) 1accumulate(b,e,t) // 把 [b,e) 的元素 +t 的综合存储在 t 里 查找:find, findif, search, equal 1234find(b,e,t) // 在 [b,e) 之间找t,并返回第一个位置的迭代器,否则返回 e find_if(b,e,p) // 在 [b,e) 之间找满足谓词 p 的元素,并返回第一个位置的迭代器,否则返回 esearch(b,e,b2,e2) // 在 [b,e) 之间找 [b2,e2) 指示的序列,并返回第一个找到位置的迭代器,否则返回 eequal(b,e,b1) // 判断 b 和 b1 迭代器开头的序列成员是否相同,由于默认长度相等,因此不需要结尾迭代器,其在相同时返回 True 复制: copy, remove_copy, remove_copy_if, remove_if, remove 123456789copy(begin,end,out); // 复制 begin 到 end 部分到 out 中copy(ret.begin(),ret.end(),back_inserter(ret)); // 复制算法中,使用了复制和拓展分离的思想,从而最大化函数的复用,使用更加灵活remove_copy(ret.begin(),ret.end(), back_inserter(nonzero), 0); // 将 ret 中不等于 0 的元素放入 nonzeroremove_copy_if(ret.begin(),ret.end(), back_inserter(result), function) // 将 function 返回为 true 的放入 result 中remove_if(ret.begin(),ret.end(), function); // 将所有不满足谓词的元素移入开头,并返回指向最后一个不被删除的后面一个元素的迭代器// student.erase(remove_if(...), student.end()); 配合 erase 使用remove(ret.begin(),ret.end(), t); // 和 remove_if 相同,但是检测那些元素不等于 t。 批量作用: transform对于每一个成员,传入 function, 并将结果放入 ret 中。 1transform(student.begin(),student.end(),back_inserter(ret), function) 分割: partition, stable_partition 12partition(student.begin(),student.end(), function)stable_partition(student.begin(),student.end(), function) // 稳定的分组版本 5. 讨论 库算法设计中的分离思想 — 灵活,高复用度: 12ret.insert(ret.end(),bot.begin(),bot.end());copy(bot.begin(),bot.end(), back_inserter(ret))); 上述例子实现在 ret 中插入bot,一种方式是调用容器相关的 insert 方法;另一种则是采用库算法进行复制。 库算法中则将复制和拓展进行了分离,拓展的部分则使用了迭代适配器,这样我们不光可以实现复制,也可以将迭代适配器应用于只增加元素的场合,提高了灵活度和复用性。 函数作为参数时,重载带来的困难 1234567891011121314151617double grade_aux(const Student_infor& s) // 在新函数中,除了指定重载版本,我们还可以捕捉一些可能的异常{ try { return grade(s); } catch (domain_error) { return 0.2*s.midterm+0.4*s.final; }}double median_analysis(const vector<Student_infor>& s){ vector<double> grades; transform(s.begin(),s.end(), back_inserter(grades), grade_aux); return mmedian(grades);} 在 cpp 中,编译器可以通过函数传入参数的不同而区分重载的函数,但是在将这些重载函数作为参数时,我们传入函数相应的参数,从而我们无法判断使用哪个版本的重载函数。 这时,我们使用自行创建的新函数,这里是 grade_aux,在新函数中调用指定版本的重载函数,从而指定版本。 6. 其他 使用迭代器构造vector/string 123string spaces(name.size(), ' '); // spaces = ' 'string s = string(ret.begin(),ret.end()); vector<double> ret = fail(ret.begin(),ret.end()); // 也可以用来构造任意向量 类似第一节中使用 string 中的 spaces 构造空的字符串,我们使用迭代器传入 string 的方法来构造对原来字符的复制。 如果容器支持索引,那么它的迭代器也支持 1234vector<double> retbegin = ret.begin();begin[-1] // == *(begin - 1)begin[sep.size()] // == *(i+sep.size()) 向量的成员函数 empty — 比统计大小更有效率 123vector<double> retif(ret.empty()) cout << 'ret is empty' << endl;","link":"/2020/11/15/CPP/07_%E4%BD%BF%E7%94%A8%E5%BA%93%E7%AE%97%E6%B3%95/"},{"title":"【Accelerated C++】 -- 08/16 使用关联容器","text":"学习要点: 关联容器:依赖元素本身值进行排序的容器—快速元素定位 常用的关联数组—映射表map 计算单词数 建立交叉引用表 生成句子 1. 关联容器:依赖元素本身值进行排序的容器—快速元素定位当我们需要建立查找某个元素的算法时,使用顺序容器需要一个非常高效的算法来提升时间效率,否则每次需要对每个元素进行查找。而关联容器则使用自调节平衡树来实现,从而实现比低级数据结构快,散列表稍慢却不依赖散列函数的查找性能。 2. 常用的关联数组—映射表map在映射表中,通过插入可以比较的索引(例如整数/字符串,从而我们可以对他们进行比较从而构树)以及对应的键值,我们可以快速根据键值检索和插入元素。但是我们不能随意来对容器索引进行修改,否则会破坏关联容器内部的顺序,从而使其无效。 12345map<K,V> m; //创建新的空映射表,键类型为 const Kmap<K,V> m(cmp); //创建新的空映射表,使用 cmp 来确定元素顺序m[k] // 索引键值 k,在常量映射表中,由于不能修改键,因此这个操作是违法的m.begin(),m.end(); //跟映射表相关的迭代器m.find() // 在常量映射表中找到键,避免索引 3. 计算单词数问题:计算所有输入中,每个单词出现了几次 算法:通过建立 map counters,我们就可以通过对应每个输入进行索引(在索引时如果索引不在 map 中,会自动创建 pair, 对于 int 这样的简单类型,会将 v初始化为0),并把对应数值加一。 库类型pair:为了在重复访问映射表时找到同时接触到键-值,库提供了类型数对(pair)。pair 中包含了两个成员,first 和 second,分别指向键和值,我们可以像访问结构体成员一样访问它们,其中 first 默认是常量,不能被修改。当我们间接引用映射表迭代器时,就能获得这个映射表关联的数对。 123456789101112131415161718int coutfreq(){ string s; map<string, int> counters; int cnt = 0; while (cin >> s and cnt < 10) { cnt = cnt + 1; cout << cnt; ++counters[s]; } for(map<string,int>::const_iterator it= counters.begin(); it != counters.end(); ++it) { cout << it->first << \"\\t\" << it->second << endl; } return 0;} 4. 建立交叉引用表问题:查找我们关心的每个输入出现的位置,默认为查询每个单词出现的行数。算法:对于每个关心的输入记录行数,行数为值,使用 vector 保存。缺省参数:对于函数默认设置为split,和 python 类似。自建类型初始化:在 map 中没有对应元素索引时,会自动创建值,对于复杂的自建向量类型,会默认创建为空向量。 1234567891011121314151617map<string, vector<int> > xref(istream& in, vector<string> find_words(const string&) = split){ string line; int line_num = 0; map<string, vector<int> > ret; while (getline(in,line) and line_num <10) { ++line_num; vector<string> words = find_words(line); for(auto it = words.begin();it != words.end(); ++it) ret[*it].push_back(line_num); } return ret;} 5. 生成句子问题:根据给定的语法,生成随机的句子算法:随机选择一种句式规则,并根据语法使用关联容器进行对应。[0,n)范围的随机数:使用中的 rand 函数,会返回[0,RAND_MAX)中的随机整数,结合将区间分成长度相等的存储桶,并判断生成的数在哪一个桶中生成随机数 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485Grammar read_grammar(istream& in){ Grammar ret; string line; int line_num = 0; while (getline(in,line) && line_num<14) { line_num += 1; vector<string> entry = split(line); if (!entry.empty()) ret[entry[0]].push_back(Rule(entry.begin()+1, entry.end())); } return ret;}bool bracketed(const string& s){ return s.size()>1 && s[0] == '<' && s[s.size()-1] == '>';}// 将RAND_MAX 分成 n 个区间,随机抽取整数,看随机数处于哪个位置,从而得到小于n的结果int nrand(int n){ if (n <= 0|| n> RAND_MAX) throw domain_error(\"Argument ti nrand is out of range\"); const int bucket_size = RAND_MAX/n; int r; do r = rand()/bucket_size; while (r>=n); return r;}void gen_aux(const Grammar& g, const string& word, vector<string>& ret){ if (!bracketed(word)) { ret.push_back(word); } else { auto it = g.find(word); //找到 sentence 开头的规则 if (it == g.end()) { cout << \"word\" << word << endl; throw logic_error(\"empty rule\"); } const Rule_collection c = it->second; // 重定向所有规则 const Rule r = c[nrand(c.size())]; //随机采用一个规则 for (auto i = r.begin(); i!= r.end();++i) gen_aux(g, *i, ret); }}vector<string> gen_sen(const Grammar& g){ vector<string> ret; gen_aux(g, \"<sentence>\", ret); return ret;}int main(){ vector<string> sentence = gen_sen(read_grammar(cin)); auto it = sentence.begin(); if (!sentence.empty()) { cout << *it; ++it; } while (it != sentence.end()) { cout<< \" \" << *it ; ++it; } cout << endl; return 0;}","link":"/2020/11/18/CPP/08_%E4%BD%BF%E7%94%A8%E5%85%B3%E8%81%94%E5%AE%B9%E5%99%A8/"},{"title":"【Accelerated C++】 -- 09/16 编写泛型函数","text":"学习要点: 泛型函数:定义和示例1.1 泛型函数:模板实例化的要求1.2 泛型函数:泛型函数对参数的约束 迭代器实现数据结构的独立性2.1 五种迭代器2.2 泛型函数加迭代器生成多种输出—split 函数示例 迭代器其他:3.1 输入输出迭代器3.2 迭代器区间和越界值 1. 泛型函数:定义和示例 定义:在使用之前并不知道参数类型的函数,这里的参数类型可以包含输入参数和输出类型。这样,我们将不同类型的相同行为特性使用同一个程序来描述,提升了函数和数据结构之间的独立性,使得函数的功能更加强大。为了能让程序的调用者明白这个函数的支持的类型,一方面我们可以观察函数内部对这个未知类型的参数的使用来判断,例如 x+y 就要求这个参数支持算数运算;另一方面,我们可以观察这个参数是哪一类的迭代器。 示例:未知类型的中值:之前的 median 对 vector 计算中值,我们可以将其改造成模板函数,从而提升对各种类型 vector 的适应 1234567891011121314template <class T> //声明我们定义的是模板函数T median(vector<T> v) //调用函数时,编译器将其判定的 median 输入的类型赋给 T{ typedef typename vector<T>::size_type vec_sz; // 依据模板类型建立新类型时,需要进行提前声明 vec_sz size = v.size(); if (size == 0) throw domain_error(\"median of an empty vector\"); sort(v.begin(), v.end()); vec_sz mid = size / 2; return size %2 ==0 ? (v[mid] + v[mid+1]) / 2 : v[mid]; // 输入 median 的参数的元素需要支持算术运算,且输出类型和输入类型相同} 1.1 泛型函数:模板实例化的要求c++ 标准没有说明系统环境如何处理模板示例化,因此我们需要记住以下两点: 对于使用了边集-编译-连接模式的系统环境来说,实例化动作是在连接期间发生的, 因此我们可能在连接期间看到哪些因为模板实例化不正确发货是哪个的错误 在编写自己的模板时,我们模板所在源文件和头儿年间需要是系统可以访问的,一般通过 include 的方式包含进文件 1.2 泛型函数:泛型函数对参数的约束在定义泛型函数时,我们没有定义输入的具体类型,因此我们所有的操作都需要对所有需要的类型的变量都适用,包含对变量的操作和类型之间的运算有定义,同时考虑传入函数的其他参数对该参数和输出结果的影响,即我们需要精确理解模板的“适当的类型”之间的相互作用。 其他参数对输出结果的影响: 当我们输入不同的其他参数也会影响我们输出结果的精度,从而影响我们想要得到的结果 12accumulate(v.begin(),v.end(),0)accumulate(v.begin(),v.end(),0.0) 其他参数对未知参数的要求: 当采用比较函数时,我们往往要求两个不同输入有相同的类型,因此其他参数可能以及对我们的未知参数产生了约束 12string::size_type max_size = 0;maxlen = max(maxlen, name.size()); //两个输入都是 size_type 对同一个未知类型的参数,输入参数类型需要一致 12345template <class T>T max(const T& left, const T& right){ return left > right ? left : right;} 2. 迭代器实现数据结构的独立性c++ 中使用不同的迭代器满足不同的访问需求,对于参数包含迭代器的函数,其依赖于迭代器类型而不是特定数据结构实现算法,从而提升了数据结构和算法之间的独立性。从而: 我们不需要对每种数据结构都实现类似功能的成员函数; 可以对数值的一个区间而不是固定的全部使用某个算法; 可以通过迭代器建立某些特殊意义的元素用于算法的实现,例如 vector.end() 2.1 五种迭代器不同的迭代器对应了不同的操作集合 输入迭代器:顺序只读 操作:++, ==, !=, *, -> 1234567template <class In, class X>In find(In begin,In end,const X& x){ while(begin!=end && *begin!=x) ++begin; return begin;} 输出迭代器:顺序只写—单次不跳过,不重复写入 操作:++, ==, !=, *, ->, = 12345678template <class In, class Out>Out copy(In begin,In end,const Out dest){ while(begin!=end) *dest++ = *begin++; return dest;} 正向迭代器:顺序读写—不访问前面一个元素每次顺序访问向量,并对元素进行读写。所有标准库容器都满足正向迭代器的要求 操作:++, ==, !=, *, ->, = 12345678910template <class For, class X>void replace(For begin, For end, const X& x, const X& y){ while (begin !=end) { if (*begin ==x) *begin = y; ++begin; }} 双向迭代器:正向迭代器+可逆访问 操作:++, ==, !=, *, ->, =, — 123456789101112template <class Bi>void reverse(Bi begin, Bi end){ vector<pair<const int, string> > a; while (begin!=end) { --end; if (begin != end) swap(*begin++, *end); }} 随机访问:相减为整数类型,随机访问,整数相加得到新的迭代器(移位),用于折半查找等。 操作(p,q 为迭代器,n 为整数):++, ==, !=, *, ->, =, —, p+n, p-n, n+p, p-q, p[n],pq 字符串迭代器和向量都是随机访问迭代器,而链表则为双向迭代器,只能通过顺序查看 1234567891011121314template <class Ran, class X>bool binary_search(Ran begin, Ran end, const X& x){ while (begin<end) { Ran mid = begin +(end-begin)/2; if (x<*mid) end = mid; else if (*mid <x) begin = mid +1; else return true; } return false;} 2.2 泛型函数加迭代器生成多种输出—split 函数示例通过模板和迭代器,我们为 split 函数创造多种输出的情形,即我们首先定义模板参数,随后将结果赋值给迭代器,并将其顺序移位,这一操作只需要一个输出迭代器就能完成 123456789101112131415161718192021222324252627template <class Out>void split_iter(const string& str, Out os){ typedef string::const_iterator iter; vector<string> ret; iter i = str.begin(); // 字符串可以有迭代器,但是字符数组不可以 while (i!= str.end()){ i = find_if(i,str.end(),not_space); iter j = find_if(i, str.end(),space); if (i!= str.end()) *os++ = string(i,j); i = j; }}#include <iterator>int main(){ string str; while (getline(cin,str)) { split_iter(str, ostream_iterator<string>(cout, \"\\n\")); // 将结果直接输出 }} 3. 迭代器其他:3.1 输入输出迭代器既然所有的容器的迭代器都是正向迭代器,那么为什么还需要输入输出迭代器呢? 不是所有迭代器都和容器相关,例如对于支持push_back的容器,我们就可以定义 back_inserter(c), 其就是一个输出迭代器 可以和流相关联,形成流迭代器,从流中读取,并将结果输出到流,在使用流迭代器时,需要指定输入输出的类型。 123vector<int> v;copy(istream_iterator<int>(cin),istream_iterator<int>(), back_inserter(v)); // 读入除了文件末尾的输入到 vcopy(v.begin(),v.end(), ostream_iterator<int>(cout, \" \")) 3.2 迭代器区间和越界值在指定区间时,我们往往使用迭代器指定左闭右开区间,而我们有三个原因来进行这样的处理: 可以良好表示没有元素的区间,左闭右闭则无法表示 在循环时只需要定义相等最后一个迭代器就可以结束迭代,而不需要判断迭代器是否小于最后一个迭代器来结束循环。 良好指示没有找到的输出:当我们没有找到,就返回最后一个元素代表“区间之外”","link":"/2020/11/19/CPP/09_%E7%BC%96%E5%86%99%E6%B3%9B%E5%9E%8B%E5%87%BD%E6%95%B0/"},{"title":"Domain adaptation - SpcL","text":"1. 问题背景: 解决行人重识别任务中的领域自适应问题,在源域行人重识别数据集中学习的知识,在新的数据集下应用。即给定数据集 A 下行人/车辆的标签,在没有标签的数据集 B 的测试集中进行检索的测试—给定一个监控行人图像,检索跨设备下的该行人图像。注意到,数据集 A 和数据集 B 中往往是不同的行人,即源域和目标域的标签不重叠 对比方法: 使用源域数据进行模型预训练,随后使用目标域聚类生成的伪标签来促使模型分类边界适应目标域。 对比方法缺点: 1. 只有预训练没有充分利用源域信息; 2. 目标域聚类中最后的 outlier 难样本被抛弃,削弱了目标域中有用的信息 2. 中心思想 核心思想是分别通过对比学习促进同类特征拉近,基于可靠的特征进一步进行基于自步学习的聚类,由此聚类生成的可靠的伪标签和源域标签一起来训练网络,获得在源域和目标域上都表现良好的模型,与其他域适应模型不同,该模型甚至可以获得在源域上更好的效果,这一点应当归功于对比学习生成的更有效的特征。 使用对比学习,拉近同一类别类内距离,增加类间距离,生成更加可靠的特征。 使用基于自步学习的聚类方式生成更加可靠的 cluster,用于监督模型的预测结果,这一点被证明尤其有效 使用 Hybid Memory 来提供所有样本,包含难样本的监督信息,进一步利用目标域的信息 3. 基于 Hybrid Memory 的对比学习:聚集类内特征,增强特征可靠性 为了提升模型提取的特征可靠性,作者分别利用源域标签和目标域聚类中心,将不同特征归类到不同集合,并进一步地使用对比损失,促使这些特征更加贴近集合中心。即利用对比损失更新 encoder 从而提取的特征更加靠近上一步得到的集合中心;随后这些特征进一步用于更新聚类中心。 对比损失:特征和本类原型特征的距离和所有距离的比值,我们想减少这个损失即缩小类内距离,提升类间距离 L_{f} = - log\\frac{exp(\\frac{}{\\tau})}{\\sum_{k=1}^{n^s}exp(\\frac{}{\\tau}) + \\sum_{k=1}^{n_c^t}exp(\\frac{}{\\tau}) + \\sum_{k=1}^{n_o^t}exp(\\frac{}{\\tau})}其中 z+ 是当前特征所在类别的原型,f 是特征,n^s 为源域集合的个数,n_c^t 是目标域聚类的个数,n_o^t 则是目标域 outlier 的个数。 Hybrid Memory:恰当的方式得到不同集合中的原型 为了得到合适的中心传入对比损失,作者构建了 Hybrid Memory 来存储必要的特征中心/原型 初始化:源域的同一标签下的特征平均,目标域所有 instance 在当前 encoder 下的特征。基于这些,我们就可以得到传入对比损失的原型。 更新: 源域中心:每个 mini-batch 中属于同一类别的源域特征的平均都用于动量更新对应的Wk: \\omega_k \\leftarrow m^s\\omega_k + (1-m^s)\\frac{1}{|B_k|}\\sum_{f_i^s\\in B_k)}f_i^s 目标域 instance:每个目标域特征都用于动量更新其对应的目标域特征 v_i \\leftarrow m^t v_i + (1-m^t)f_i^t 4. 自步学习: 缓解噪声标签影响,增强学习特征的鲁棒性 只考虑可靠的 cluster,否则,将其拆回到 instances,避免不是同一类的 clusters 相互拉近。获得可靠的cluster之后,通过对比学习不断学习到更聚集的特征,相应的 cluster 评价分数也会越高,越来越多的特征能参与到准确的cluster进行计算,从而减少噪声标签的影响,并提升模型的鲁棒性。 衡量可靠性 cluster 的标准 — 改变聚类算法的参数,使其分别获得更加紧凑/松散的 cluster: 独立性:描述 cluster 扩展之后和原来 cluster 中元素个数的差距,越独立的 cluster 独立性分数越高 R_{indep}(f_i^t) = \\frac{I(f_i^t)\\cap I_{loose}(f_i^t)}{I(f_i^t)\\cup I_{loose}(f_i^t)} \\in [0,1] 紧凑性:描述 cluster 缩紧之后和原来 cluster 中元素个数的差距,越紧凑的 cluster 紧凑性分数越高 R_{compac}(f_i^t) = \\frac{I(f_i^t)\\cap I_{tight}(f_i^t)}{I(f_i^t)\\cup I_{tight}(f_i^t)} \\in [0,1] 对于独立性分数和紧凑性分数大于给定门限的则为可靠的 cluster,用于作为 cluster 代表参与到对比损失的计算,否则拆回到instance参与对比损失的计算。 5. 整合训练步骤 基于特征,对目标域数据进行聚类,聚类成 cluster 和未聚类实例 使用聚类生成的标签和源域的标签生成的 Hybid Memory监督,以及对比损失,训练特征提取器。 重复1,2步不断学习 6. 消融实验Oracle:使用目标域标签训练 Src.class: 只使用源域标签训练 Src.class+tgt.instance: 传统对比学习,将每个 instance 当成一类训练。 Src.class + tgt.cluster: 现有基础上抛弃难样本,取消自步学习 Src. class + tgt. cluster (w/ self-paced):没有难样本,同时不更新难样本 Ours (full):上述描述算法 体现了自步学习和全对比学习的必要性 7. Limitations 基于目标域和源域之间没有标签重叠,本文没有使用目标域和源域之间的关联性,只间接使用共享模型作为沟通特征的桥梁,缺少了对信息的利用,同时对比学习会拉远目标域和源域的特征,不一定适用于目标域和源域标签重叠的情况。标签重叠时也许需要增强源域和目标域之间对应类别的配对。 在使用特征时考虑全局平均的pooling 而没有结构性信息。","link":"/2020/11/27/%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0/NIPs_SpcL/"},{"title":"【Accelerated C++】 -- 10/16 自定义新类","text":"学习要点: 新类型 新类型的封装和数据保护1.1 成员函数、存取器1.2 private/public 新类型的初始化 0. 新类型C语言中,我们使用结构体定义我们自己的类型并将多种关联数据绑定起来,之前使用的 Student_infor 就像这样定义 123456struct Student_info{ std::string name; double midterm, final; std::vector<double> homework;};但是这样定义的结构体没有达成这样几条约定: 内部数据一开始需要有定义,否则基于局部内部数据的操作因为缺省初始化带来的未知参数,会导致程序不正确; 内部数据的访问权限要有良好定义,我们不希望用户能直接修改数据,而是将我们读进来的数据进行只读操作 使用这个新建类型的所有函数要保存在一个头文件中,避免接口的分散。 1. 新类型的封装和保护为了减少用户对数据的接触,我们基于: 类内数据定义成员函数 将成员函数使用 public 关键字公开,其他用 private 关键字隐藏 进行数据保护和方法公开。 1.1 成员函数定义:在类内部声明/定义的函数,是函数本身的方法,不需要传递类就可以直接访问到类/结构体中的变量。常量成员函数:通过给成员函数加上 const 关键字使之成为常量成员函数,从而其不能改变操作对象的状态。常量成员函数的存在适用于成员函数不传入原对象参数,但是又需要给原对象加 const 关键字的情况 下面是带有成员函数的类的定义: 123456789101112class Student_info_dnc{ std::string n; double midterm, final; std::vector<double> homework; std::istream& read(std::istream&); // 封装: 不用访问内部数据就可以赋值 double grade() const; // 封装: const 常量成员函数,计算分数但不改变值 std::string name() const {return n;} // 封装: 存取器控制只读访问 bool valid() const {return !homework.empty();} // 封装: 减少用户对数据访问情况下,判断初始化的合理性 Student_info_dnc(): midterm(0),final(0) {}; // 初始化:任意初始化 Student_info_dnc(std::istream& in) {read(in);}; // 初始化:读 io 流初始化}; 成员函数类似与 python 中的方法,在类外部定义时需要加上类和表作用域符号;在类内部定义时使用内联子过程,减少函数调用的开销。 在外部定义: 使用 Student_info_dnc::read 而不是 read,:: 是作用域运算符,用于定义和访问类内的成员函数 1234double Student_info_dnc::grade() const{ return ::calgrade(midterm,final,homework); //::放在名称之前,说明我们使用的函数不能是任何事物的成员,用于区分函数} 在内部定义: 合并程内联子过程 123456789class Student_info_dnc{ std::string n; double midterm, final; std::vector<double> homework; std::istream& read(std::istream&); double grade() const {return ::calgrade(midterm,final,homework);}; std::string name() const {return n;} }; 1.2 数据隐藏机制:public 保护标识符公开, private 保护标识符隐藏数据隐藏机制: 通过在类/结构体内指定不同的保护标识符,我们就可以控制用户能访问的成员。private 保护标识符之后的成员在非成员函数中的访问是违法的。 12345678910class Student_info_dnc{private: std::string n; double midterm, final; std::vector<double> homework;public: std::istream& read(std::istream&); double grade() const;}; class 和 Struct 的区别: class 倾向保护,而 struct 倾向公开。体现在 struct { 和第一个保护标识符之间的内同都是公开的。而 class 则是私有,倾向于控制对成员的访问。 2. 新类型的初始化:构造函数:用于对自建类型进行合理的初始化。没有构造函数的局部变量会被缺省初始化,这样 double 等类型的数据会初始化成无意义信息导致程序运行的混乱和崩溃。数值初始化的几种情况:在以下情况下,会通过数值初始化为0,减少变量的不确定性: 1. 对象用于初始化容器元素 2. 对象为映射表添加元素后的副作用 3. 对象是有特定长度容器的元素 变量的初始化:1. 有构造函数的变量采用构造函数初始化 2. 内建类型根据情况缺省初始化或者数值初始化 3. 自建类型递归后,根据情况缺省初始化或者数值初始化 下面的代码通过两种方式定义了缺省构造函数: 在:和{ 之间是告诉编译器初始化特定的成员,其他成员按照情况初始化;我们还可以在构造函数内部,通过外部数值初始化 12Student_info_dnc(): midterm(0),final(0) {}; //在:和{ 之间是告诉编译器初始化特定的成员,Student_info_dnc(std::istream& in) {read(in);}; // 我们还可以在构造函数内部,通过外部数值初始化 3. 总结定义新类型时,我们希望满足: 内部数据一开始需要有定义,否则基于局部内部数据的操作因为缺省初始化带来的未知参数,会导致程序不正确; 内部数据的访问权限要有良好定义,我们不希望用户能直接修改数据,而是将我们读进来的数据进行只读操作 使用这个新建类型的所有函数要保存在一个头文件中,避免接口的分散。 三条约定,从而是的新类型数据得到良好的定义和保护,整体有组织性。我们使用了构造函数来良好的初始化,并使用成员函数和保护标识符来保证新类型内部数据的访问权限。","link":"/2020/12/14/CPP/10_%E5%AE%9A%E4%B9%89%E6%96%B0%E7%B1%BB%E5%9E%8B/"},{"title":"【Accelerated C++】 -- 11/16 管理内存和低级数据结构","text":"学习要点: 指针和数组 字符串常量—常量字符数组 1.1 字符串指针数组 1.2 指向字符指针的指针作为 main 函数参数 1.3 字符指针初始化文件输入输出流 三种内存分配方法 0.指针和数组0.1 指针定义:指针是一种随机存取的迭代器,其用于存放一个对象的地址的值,对对象使用求址算符 & 就可以获得其地址。指针类型是内部类型,会被缺省初始化为没有意义的数值,因此我们可以使用 0 或者 nullptr 对其进行初始化,0是唯一一个可以转化成指针类型的整形。 123456789// 指针的初始化int* p; //和 int *p 有同样的意义int* p = nullptr; //初始化为空指针int* p, q; //声明 p 指向整形的指针变量,q 为整形变量//指针指向位置和对象在同一地址,因此可以直接修改指针的值int x = 5;int*p = &x;*p = 6 指向函数的指针:函数在程序中只有调用和获取函数地址两个作用,因此当我们传函数作为参数时,我们实际上时传入了函数的地址,或者说传入了指向函数的指针、在定义和初始化指向函数的指针时,我们需要声明函数的返回类型和传入参数,并用指针的间接银行用替换函数名所在位置。 123456// 函数名就是地址,调用函数指针就是调用函数fp = &function;fp = function; // 等价上一行i= (*fp)(q)i= fp(q) // 等价上一行 函数指针的声明: 我们需要先预定义一个函数指针,随后用这个预定义的类型用于函数的定义中。123456// 声明函数指针double analysis(const vector<double>&) // 一个函数typedef double(*analysis_fp)(const vector<double>&) // typedef 定义指针类型analysis_fp get_analysis_ptr() //这个函数就能返回指向 返回为都double,以const vector<double>& 为参数的函数指向函数的指针往往用于传递给另外一个函数,下面 Pred 就是指向 is_neg 函数的指针类型。这样使用泛型可以传入任意类型的函数。1234567891011121314template <class In, class Pred>In find_if(In begin, In end, Pred f){ while (begin != end && !f(*begin)) ++begin; return begin;}bool is_neg(int n){ return n<0;}vector<int>::iterator i = find_if(v.begin(), v.end(), is_neg); 0.2 数组定义:数组是预先确定尺寸的用于存储一个或几个同类型对象的低层数据结构,其没有成员函数和成员变量,并使用在 头文件中定义的 size_t 来表示一个数组的大小。特点: 数组的初始化:可以直接写出元素从而避免显式地写出大小。 数组的访问: 使用数组名表示指向数组首元素的指针,对其间接引用可以获得数组首元素 使用加减的方式可以随机访问数组中元素,如果 p 指向数组中第 m 个元素,那么(p+n) 指向数组中第 p+n 个元素,即便数组没有这个元素,但是这个定义还是有效的。同时我们使用 ptrdiff_t 这个类型来定义两个指针之间的间距 p-q。 索引: 使用指向数组的指针就可以对数组元素进行索引,等效于使用数组名称。 数组指针的使用:地址可以类似迭代器一样传入其他库函数中,甚至用于初始化 vector。1234567891011121314// 数组初始化: 不用显式初始化大小int month[] = {1,2,3,4,5,6,7,8,9,10,11,12};// 数组访问int a = *month; // a = 1int b = *(month+2) // b = 3int c = month[3] // c = 3//用于传入到库函数 copyvector<double> v;copy(coords, coors+3, back_inserter(v));// 用于初始化 vectorvector<double> c(coords, coords+3); 1. 字符串常量—字符常量数组字符串常量定义:表现为 “Strings” 的常量,它和字符常量数组等价,可以用定义在 里的 strlen 来求长度。同时 strlen 还可以用来求以空字符结束的数组大小,也说明了二者的统一性。123456const string a = \"a string\";const char asim[] = {'a',' ','s','t','r','i','n','g'}; //a 的等价形式size_t alen = strlen(asim);string s(asim) //asim 代表字符常量数组的首元素地址,传入s的构造函数,用于创建 asim 的复件string s(asim, asim+len) // 也可以传入首尾指针构造,类似于传入首尾迭代器构造 1.1 字符串指针数组我们使用一个字符串指针数组的例子,来说明 static 和 sizeof 的应用。下面这个程序是将分数转换成对应的等级分数,这里面我们使用了字符指针数组 letters。 注意,字符指针数组和字符数组相同,其数组代表的方式都是字符名,在这里就是 letters,而不是*letters。12345678910111213141516171819202122232425262728293031323334353637string letter_grade(double grade){ // static 关键字(程序存在期间只创建一次)用于减少初始化次数,节省时间复杂度。 static const double numbers[] = {97,94,90,87,84,80,77,74,70,60,0}; //之所以说 letters是字符指针数组,在于它的每一个元素都是一个常量字符数组,也就是指向第一个字符元素的指针 static const char* letters[] = {\"Ap\",\"An\",\"A-\",\"B+\",\"Bn\",\"B-\",\"C+\",\"Cn\",\"C-\",\"Dn\",\"Fn\"}; // sizeof 用于计算对象所占的字节数; //下两行的等效性说明 letters 才是代表数组// static const std::size_t ngrades = sizeof(numbers) / sizeof(*numbers); static const std::size_t ngrades = sizeof(letters) / sizeof(*letters); cout <<\"Letters\" << **letters << ngrades << endl; for (std::size_t i = 0; i < ngrades; ++i) { if (grade>=numbers[i]) return letters[i]; }; return \"?\\?\\?\";}int main(){ random_device rd; //随机种子 mt19937 mt(rd()); // 随机设备 uniform_int_distribution<int> dist(0,100); // [0,100] 均匀分布 for(int i=0; i<30; ++i) { int grade = dist(mt); cout << \"The grade is: \" << grade << \", And the letter grade is: \" << letter_grade(grade) << endl; } return 0 ;} 1.2 指向字符指针的指针作为 main 函数参数main 函数的参数: argc,argv:main 函数有两个参数,第一个整形参数 argc 代表第二个参数中指向对象的元素的个数;第二个参数 argv 是一个指向字符指针的指针。123456789101112int passparamsTomain(int argc, char** argv) // 指针数组的指针{ if (argc>1) { int i; // argv[i] 是 char*,相当于字符串的首元素地址,交给输出流就可以输出字符串 for(i=1; i< argc-1; ++i ) cout << argv[i] << \" \"; cout << argv[i] << endl; } return 0;}下面的图示代表了第二个参数的含义: 1.3 字符指针初始化文件输入输出流文件输入输出流:除了使用标准输出流进行输入输出之外,我们还希望能使用文件进行输入输出,此使就可以使用文件输入输出流,其可以直接替换标准输入输出流,我们用字符指针,也就是文件所在位置的字符串常量/常量字符数组来初始化它。标准错误流 cerr、clog:在程序输出异常信息时,我们使用标准错误流来输出。其中 cerr 在出错时即时输出错误信息,clog 则是适当时候输出 1234567891011121314151617181920212223int copymultiin(int argc, char ** argv){ int failcnt = 0; ofstream outfile(R\"(E:\\Projects\\Accleratedcpp\\out.txt)\"); for (int i= 1; i<argc; ++i) { ifstream in(argv[i]); //不想使用字符串常量时,可以这样替换:用于替换变量名// string file; // ifstream infile(file.c_str()) if(in) { string s; while(getline(in, s)) outfile << s << endl; }else { cerr << \"cannot open file\" << argv[i] << endl; ++ failcnt; } } return failcnt;} 2. 三种内存分配方法 局部变量自动分配内存:作用域内分配内存,模块结束自动释放。如果使用指针指向被释放的局部变量,指针会变成无效指针 静态变量自动分配内内存:模块结束不会释放内存,程序结束会,同时由于静态变量初始化一次的特性,每次只能返回一个指针。 使用 new 动态分配内存:当使用 delete 删除指针之后才会释放,每次运行都可以创建新对象。 1234567891011121314151617181920//第一种分配内存int* invalid_ptr(){ int x; return &x; //无效指针返回}//第二种分配内存int* ptr_to_static(){ static int x; return &x; //有效指针返回}//第三种分配内存int* new_ptr(){ int* ptr = new int(0);// delete ptr; 执行这一步之后指针才会消失 return ptr; //有效指针返回} 3.1 为数组分配/释放内存数组分配内存:我们需要初始化数组,这也就涉及到数组每一个元素初始化,从而带来一定运行开销;在初始化空数组时,也会返回一个有效但无意义的 off-the-end 指针1234567891011121314151617181920const char* duplicate_chars(const char * p){ size_t length = strlen(p) + 1; char* result = new char[length]; // 创建一个字符数组 copy(p,p+length, result); //指针是迭代器 return result;}int testduplicate(){ const char letters[] = {'a','v','A','B','C','e','d','D','F','\\0'}; const char* p = duplicate_chars(letters); for(std::size_t i =0; i < sizeof(letters)/sizeof((*letters)); ++i) { cout << *(p+i)<< endl; } delete[] p; //删除一整个数组 return 0;} 总结 指针和数组相辅相成,指针可以看作是数组的迭代器,用于构建其他数组并传入库函数使用 字符串常量是常量字符输出,可以用于初始化文件输入输出流,字符指针还可以当作 main 函数的参数 我们有三种分配内存的方法:使用局部变量要防止野指针;使用 new、delete 可以分配释放对象,数组内存","link":"/2020/12/15/CPP/11_%E7%AE%A1%E7%90%86%E5%86%85%E5%AD%98%E5%92%8C%E4%BD%8E%E7%BA%A7%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"},{"title":"【Accelerated C++】 -- 12/16 定义抽象数据类型","text":"学习要点: Vec 抽象类的基本要素 三位一体—构造函数,析构函数,复制构造函数 1.1 复制构造函数 1.2 赋值运算符函数 1.3 析构函数 1.4 编译器合成构造函数等 类的内存管理 2.1 类的扩展 2.2 allocator 库 在第 10 部分,我们重新定义了自己的 Student_infor 类。但是我们在复制,赋值和销毁这个对象时,对其内部进行的操作却没有控制。我们希望能建立这些相关的函数,并进行相应的内存管理。 1. Vec 抽象类的基本要素建立需求:为了定义一个类,我们需要确定在类中需要定义什么接口,通过观察 vector 在程序中的使用,我们通常需要构造类的对象,使用类的迭代器和大小,并通过索引找到类中对象,因此我们围绕这几个需求建立一个基础的模板类。模板类是可以用于存储多种类型对象的类,在声明这个类的对象时,编译器就会实例化模板类的具体版本。类型定义:为了使用迭代器,获取大小,我们需要进行类型定义。从而用户能调用成员而不知道具体细节。 迭代器:包含指针并能使用 ++ 运算符获得下一个节点的变量。由于我们使用的是普通数组,我们可以使用普通指针作为迭代器。 size_type:使用可以装下任何数组大小的 size_t12345678910111213template<class T>class Vec{public: // interface typedef T* iterator; typedef const T* const_iterator; typedef size_t size_type; typedef T value_type;private: // implement iterator data; iterator limit; 构造函数定义:构造函数用于对象的初始化,我们通过对内部数组指针的管理来进行构造,也就是构造函数将申请内存,管理指针并初始化内存。我们建立了下面几个构造函数。 默认构造函数:使用 create 函数初始化指针为 0 即可 长度为 n ,初始值为默认初始值/自定义初始值构造函数: explicit 关键字代表,只有显式地使用构造函数时才会调用,在其他初始化情况下:例如函数声明,默认初始化,函数返回等 不会调用 123456789template<class T>class Vec{public: Vec() {create();}; //默认构造函数 explicit Vec(size_type n, const T& val=T()) {create(n,val);}; // 给定大小和初始值构造函数 private: T* data; T* limit; }; 索引函数:为了能使用索引运算符,我们需要基于 Vec 类对索引运算符进行重载,定义重载运算符函数只需要和普通函数一样:定义函数名,参数和返回类型。同时由于是重载运算符,我们需要加入 operator 关键字到运算符前。 参数个数:非成员运算符函数的参数和其操作数一样多,第一个参数为左操作数,第二个参数为右操作数;如果为成员函数,左操作数一定是对象,因此,只需要传入右操作数为参数。 参数类型:索引函数直接返回给定位置的对象,因此需要针对对象为常量、非常量做两个版本,确保返回的常量对象不被改变。12345678910111213template<class T>class Vec{public: // 重载的操作符:索引,赋值 T& operator[] (size_type i) {return data[i];}; //重载索引运算符;写成函数并返回 const T& operator[] (size_type i) const {return data[i];}; // 能重载是因为还隐式地传入了自身这个参数 Vec& operator= (const Vec&);private: // implement iterator data; iterator limit;}; 迭代器:迭代器为指针,由于返回了对象指针,也需要区分其为常量版本和非常量版本。1234567891011121314template<class T>class Vec{public: // 迭代器 iterator begin() {return data;}; const_iterator begin() const { return data;}; iterator end() {return limit;}; const_iterator end() const {return limit;};private: // implement iterator data; iterator limit;}; size 函数:基于ptrdiff_t类型,找到当前指针和开头指针的位置差;同时不能改变对象本身,因此声明其为 const 函数。12345678910template<class T>class Vec{public: size_type size() const {return limit - data; };private: // implement iterator data; iterator limit;}; 基于我们的基本需求,通过类型定义,和建立基本函数,我们完成 Vec 类的 基本元素定义,但是还是需要对赋值,初始化,删除等情况进行控制。初步版本如下。1234567891011121314151617181920212223242526272829303132333435template<class T>class Vec{public: // interface typedef T* iterator; typedef const T* const_iterator; typedef size_t size_type; typedef T value_type; // 迭代器 iterator begin() {return data;}; const_iterator begin() const { return data;}; iterator end() {return limit;}; const_iterator end() const {return limit;}; // 构造函数 Vec() {create();}; explicit Vec(size_type n, const T& val=T()) {create(n,val);}; size_type size() const {return limit - data; }; // 重载的操作符:索引,赋值 T& operator[] (size_type i) {return data[i];}; //重载索引运算符;写成函数并返回 const T& operator[] (size_type i) const {return data[i];}; // 能重载是因为还隐式地传入了自身这个参数private: // implement iterator data; iterator avail; iterator limit; void create(); void create(size_type, const T&); void create(const_iterator, const_iterator);}; 2. 三位一体 — 赋值函数,析构函数,复制构造函数在写一个管理资源(例如内存资源)的类时,应当注意对复制函数的控制:从而避免因为默认复制函数带来的复制不独立问题,默认析构函数只释放指针不释放对象的问题。为了避免这些问题,我们需要自行建立复制构造函数,赋值运算符函数,和析构函数三位一体的函数结构。 2.1 复制构造函数使用场景:当我们把一个对象作为参数进行值传递,或者函数返回一个对象时,我们就对对象进行了隐式的复制操作。在使用一个对象初始化另外一个对象时,我们就会显式地复制对象1234567891011//显式复制: 传入函数;函数返回复制vector<int> vi;double d;d = median(vi); // 传入函数string line;vector<string> words = split(line); // 函数返回复制// 隐式复制:初始化另一个对象vector<Student_info> vs;vector<Student_info> v2 = vs如何复制:当传入参数是对象地址时(因为我们需要基于指针进行操作),我们需要注意复制函数需要开辟新的内存并复制对象数值,而不是简单将地址进行复制,否则就会得不到一个新的独立对象。目前定义复制函数如下 123456789101112131415template<class T>class Vec{public: Vec(const Vec& v){create(v.begin(), v.end());}; // 复制构造函数private: T* data; T* limit; }; template<class T>void Vec<T>::create(const_iterator i, const_iterator j){ data = alloc.allocate(j-i); limit = avail = uninitialized_copy(i,j,data);} 2.2 赋值运算符定义:作为自定义类型的成员函数,赋值总是把已经存在的左操作数擦去,然后使用右操作数替代。这里我们需要特别注意赋值给自身的情况。12345678910template <class T>Vec<T>& Vec<T>::operator=(const Vec<T> & rhs){ if (&rhs!= this) // this 是成员函数的中的关键字,指向函数操作对象的指针,这里是判断左右操作数是否相同。 { uncreate(); // 不相同情况下,擦除左操作数,创建右操作数的副本。 create(rhs.begin(), rhs.end()); } return *this; // 由于左操作数生命周期大于赋值操作,我们可以确定返回对象的有效性}赋值不是初始化:赋值是将左操作数删除并用右操作数替换的过程,而初始化则是将没有对象的内存中初始设置值的过程,因此二者从定义上来看就是不同的。下面从几个不同的角度来看他们的差异 赋值时调用类中 operator= 函数赋予另一对象数值,初始化则调用构造函数赋予默认值 赋值只有在调用 = 时使用,而初始化则在声明变量,函数传参,函数返回函数值,构造初始化时作用。123string v; //声明初始化string c = \"abd\"; //调用 string 的带const char*的构造函数初始化vector<string> v = split(word) // 传参时初始化 word,之后返回值初始化 v。 赋值操作时先运行复制构造函数生成临时变量,构造函数对这个临时变量进行初始化;然后运行复制运算符函,将临时变量的值赋给左操作数。 2.3 析构函数定义:用于删除类的一个对象示例,使用~前缀定义,没有参数和返回值12345template <class T>class Vec{public: ~Vec(){uncreate();} //对于所有构造函数中分配了内存的对象,在析构函数中都要释放,一般可以通过data 到 avail 之间距离确定}; 2.4 编译器合成构造函数等当我们没有显式定义构造函数,赋值运算符或者析构函数的时候,编译器就会为我们合成相应的默认版本的函数,编译器递归地根据他们的类型进行复制,赋值,删除。编译器合成的这些函数可能有这样的问题: 复制函数当复制对象是指针时,不会产生单独的新对象,而是产生指向相同位置的另外一个指针 析构函数在删除一个指针时,不会删除这个指针指向的对象,因此可能产生内存泄露。 3. 类的内存管理内存管理出现在对类进行创建,赋值和删除的时候,上一节我们使用了 new T[n] 的方式来创建一个新的对象,但是使用 new 方法的有这样的缺陷: 我们需要 T 进行初始化,因此,需要 T 有默认的构造函数才能创建 T。 多一次初始化操作:结合构造函数的初始化和 new 函数的初始化,我们一共需要两次初始化操作。 因此,我们使用 allocator 库函数的内存分配类来代替 new 和 delete,使用这个库可以申请原始内存,并使用库函数中的方法进行初始化;这个库函数将通过管理数组指针来管理我们的数据,而数组指针和内存管理方法将是 private 的,那么用户将不能影响指针的变化。 3.1 动态的类对象 — 类的扩展当我们对数据进行扩充时,我们需要重新申请新的内存,从而重新复制原来元素到新的内存空间,如果每一个添加元素时都进行这样的操作的话,就会效率很低,因此就会给程序分配之前的两倍的内存。新的 push_back 函数如下所示,其中建立了 avail 成员来指向有效的元素的边界:123456void push_back(const T& val){ if (avail==limit) // 空间不够了,就重新分配 grow(); unchecked_append(val);} 3.2 allocator 库定义: 在 中提供了一个 allocator 的类,可以分配一块用来存储类型 T 但是没有被初始化的内存,并返回指向该内存头部的指针。在库里面,有这样一些我们会用到的函数:12345678910template<T>class allocator{public: T* allocate(size_t); // 分配长度为 size_t 的内存 void deallocate(T*, size_t); // 释放 allocate 生成的长度为 size_t 的内存 void construct(T*, T) // 在 allocate 的内存上初始化生成单个对象 void destroy(T*) // 删除 construct 创建的对象};void uninitialized_fill(T*,T*,const T&); // 在 allocate的内存块上初始化成一个指定的值T* uninitialized_copy(T*, T*, T*); // 前两个指针指向的位置的对象复制到第三个指针开头的区域最后的 Vec 类如下,对于一个有效的 Vec 类型对象,需要满足以下四个条件 如果有元素,data 指向首元素,否则为0 data<=avail<=limit 在[data,avail) 区间内的元素被初始化 在[avail, limit) 区间内的元素不被初始化只要成员函数不改变这个规律,这个规律就会一直成立。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118template<class T>class Vec{public: // interface typedef T* iterator; typedef const T* const_iterator; typedef size_t size_type; typedef T value_type; // 迭代器 iterator begin() {return data;}; const_iterator begin() const { return data;}; iterator end() {return limit;}; const_iterator end() const {return limit;}; // 构造函数 Vec() {create();}; explicit Vec(size_type n, const T& val=T()) {create(n,val);}; Vec(const Vec& v){create(v.begin(), v.end());}; // 复制构造函数 ~Vec() {uncreate();}; // 析构函数 size_type size() const {return limit - data; }; // 重载的操作符:索引,赋值 T& operator[] (size_type i) {return data[i];}; //重载索引运算符;写成函数并返回 const T& operator[] (size_type i) const {return data[i];}; // 能重载是因为还隐式地传入了自身这个参数 Vec& operator= (const Vec&); void push_back(const T& val) { if (avail==limit) grow(); unchecked_append(val); }private: // implement iterator data; iterator avail; iterator limit; allocator<T> alloc; void create(); void create(size_type, const T&); void create(const_iterator, const_iterator); void uncreate(); void grow(); void unchecked_append(const T&);};template <class T>Vec<T>& Vec<T>::operator=(const Vec<T> & rhs){ if (&rhs!= this) { uncreate(); create(rhs.begin(), rhs.end()); } return *this;}template <class T>void Vec<T>::create(){ data = avail = limit = 0;}template<class T>void Vec<T>::create(size_type n, const T& val){ data = alloc.allocate(n); limit = avail = data+n; uninitialized_fill(data,limit,val);}template<class T>void Vec<T>::create(const_iterator i, const_iterator j){ data = alloc.allocate(j-i); limit = avail = uninitialized_copy(i,j,data);}template<class T>void Vec<T>::uncreate(){ if (data) { iterator it = avail; while (it!=data) alloc.destroy(--it); alloc.deallocate(data,limit-data); } data = limit = avail = 0;}template<class T>void Vec<T>::grow(){ size_type new_size = max(2*(limit-data),std::ptrdiff_t(1)); iterator new_data = alloc.allocate(new_size); iterator new_avail = uninitialized_copy(data,avail,new_data); uncreate(); data = new_data; avail = new_avail; limit = data+new_size;}template<class T>void Vec<T>::unchecked_append(const T & val){ alloc.construct(avail++, val);}","link":"/2020/12/17/CPP/12_%E5%AE%9A%E4%B9%89%E6%8A%BD%E8%B1%A1%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B/"}],"tags":[{"name":"表达式","slug":"表达式","link":"/tags/%E8%A1%A8%E8%BE%BE%E5%BC%8F/"},{"name":"迭代器和顺序容器","slug":"迭代器和顺序容器","link":"/tags/%E8%BF%AD%E4%BB%A3%E5%99%A8%E5%92%8C%E9%A1%BA%E5%BA%8F%E5%AE%B9%E5%99%A8/"},{"name":"乐理","slug":"乐理","link":"/tags/%E4%B9%90%E7%90%86/"},{"name":"循环","slug":"循环","link":"/tags/%E5%BE%AA%E7%8E%AF/"},{"name":"字符串","slug":"字符串","link":"/tags/%E5%AD%97%E7%AC%A6%E4%B8%B2/"},{"name":"五线谱","slug":"五线谱","link":"/tags/%E4%BA%94%E7%BA%BF%E8%B0%B1/"},{"name":"键盘","slug":"键盘","link":"/tags/%E9%94%AE%E7%9B%98/"},{"name":"综述","slug":"综述","link":"/tags/%E7%BB%BC%E8%BF%B0/"},{"name":"论文阅读","slug":"论文阅读","link":"/tags/%E8%AE%BA%E6%96%87%E9%98%85%E8%AF%BB/"},{"name":"头文件书写","slug":"头文件书写","link":"/tags/%E5%A4%B4%E6%96%87%E4%BB%B6%E4%B9%A6%E5%86%99/"},{"name":"注意力机制","slug":"注意力机制","link":"/tags/%E6%B3%A8%E6%84%8F%E5%8A%9B%E6%9C%BA%E5%88%B6/"},{"name":"最小二乘法","slug":"最小二乘法","link":"/tags/%E6%9C%80%E5%B0%8F%E4%BA%8C%E4%B9%98%E6%B3%95/"},{"name":"孪生网络","slug":"孪生网络","link":"/tags/%E5%AD%AA%E7%94%9F%E7%BD%91%E7%BB%9C/"},{"name":"Unet++","slug":"Unet","link":"/tags/Unet/"},{"name":"库算法","slug":"库算法","link":"/tags/%E5%BA%93%E7%AE%97%E6%B3%95/"},{"name":"关联容器map","slug":"关联容器map","link":"/tags/%E5%85%B3%E8%81%94%E5%AE%B9%E5%99%A8map/"},{"name":"泛型函数","slug":"泛型函数","link":"/tags/%E6%B3%9B%E5%9E%8B%E5%87%BD%E6%95%B0/"},{"name":"新类型的数据初始化、封装和保护","slug":"新类型的数据初始化、封装和保护","link":"/tags/%E6%96%B0%E7%B1%BB%E5%9E%8B%E7%9A%84%E6%95%B0%E6%8D%AE%E5%88%9D%E5%A7%8B%E5%8C%96%E3%80%81%E5%B0%81%E8%A3%85%E5%92%8C%E4%BF%9D%E6%8A%A4/"},{"name":"数组、指针、分配内存","slug":"数组、指针、分配内存","link":"/tags/%E6%95%B0%E7%BB%84%E3%80%81%E6%8C%87%E9%92%88%E3%80%81%E5%88%86%E9%85%8D%E5%86%85%E5%AD%98/"},{"name":"抽象类,构造析构函数,内存管理","slug":"抽象类,构造析构函数,内存管理","link":"/tags/%E6%8A%BD%E8%B1%A1%E7%B1%BB%EF%BC%8C%E6%9E%84%E9%80%A0%E6%9E%90%E6%9E%84%E5%87%BD%E6%95%B0%EF%BC%8C%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86/"}],"categories":[{"name":"cpp","slug":"cpp","link":"/categories/cpp/"},{"name":"音乐","slug":"音乐","link":"/categories/%E9%9F%B3%E4%B9%90/"},{"name":"思考","slug":"思考","link":"/categories/%E6%80%9D%E8%80%83/"},{"name":"见闻","slug":"见闻","link":"/categories/%E8%A7%81%E9%97%BB/"},{"name":"领域自适应","slug":"领域自适应","link":"/categories/%E9%A2%86%E5%9F%9F%E8%87%AA%E9%80%82%E5%BA%94/"},{"name":"深度学习","slug":"深度学习","link":"/categories/%E6%B7%B1%E5%BA%A6%E5%AD%A6%E4%B9%A0/"},{"name":"机器学习","slug":"机器学习","link":"/categories/%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0/"},{"name":"变化检测","slug":"变化检测","link":"/categories/%E5%8F%98%E5%8C%96%E6%A3%80%E6%B5%8B/"}]}