├── C++基础 ├── 02_incluede后面使用双引号和尖括号的区别.md ├── 04_C++11常用新特性.md ├── 05_请说一下你理解的 C++ 中的四个智能指针.md ├── 11_new&delete与malloc&free 之间的关系与区别.md ├── 12_delete和delete[]的区别.md ├── 14_虚函数详解.md ├── 15_纯虚函数.md ├── 16_析构函数.md ├── 24_sizeof与strlen的区别.md ├── 25_strcpy_strncpy_memcpy的区别.md ├── 29_哈希冲突和解决方法.md ├── 31_c++中的左值引用与右值引用.md ├── 32_C++中的内存管理是怎样的.md ├── 35_内存泄漏是什么.md ├── 36_为什么要内存对齐.md ├── 37_全局变量和局部变量的区别.md ├── 46_深拷贝与浅拷贝的区别.md ├── 50_引用作为函数参数和返回值的好处.md ├── 52_虚函数可以声明为inline吗.md ├── 54_常量指针和指针常量的区别.md ├── 55_函数指针和指针函数的区别.md ├── 56_数组指针和指针数组的区别.md ├── 63_7种经典的排序算法原理及C++实现.md ├── 72_vector的扩容原理以及resize和reserve的区别.md ├── 74_sort自定义排序.md ├── 75_快速幂.md ├── 76_二叉树前中后层遍历代码模板.md └── 77_牛客网笔试处理各种输入输出的解决方法.md ├── PDF版 ├── C++基础 │ ├── 02_incluede后面使用双引号和尖括号的区别.pdf │ ├── 04_C++11常用新特性.pdf │ ├── 05_请说一下你理解的 C++ 中的四个智能指针.pdf │ ├── 11_new&delete与malloc&free 之间的关系与区别.pdf │ ├── 12_delete和delete[]的区别.pdf │ ├── 14_虚函数详解.pdf │ ├── 15_纯虚函数.pdf │ ├── 16_析构函数.pdf │ ├── 24_sizeof与strlen的区别.pdf │ ├── 25_strcpy_strncpy_memcpy的区别.pdf │ ├── 29_哈希冲突和解决方法.pdf │ ├── 31_c++中的左值引用与右值引用.pdf │ ├── 32_C++中的内存管理是怎样的.pdf │ ├── 35_内存泄漏是什么.pdf │ ├── 36_为什么要内存对齐.pdf │ ├── 37_全局变量和局部变量的区别.pdf │ ├── 46_深拷贝与浅拷贝的区别.pdf │ ├── 50_引用作为函数参数和返回值的好处.pdf │ ├── 52_虚函数可以声明为inline吗.pdf │ ├── 54_常量指针和指针常量的区别.pdf │ ├── 55_函数指针和指针函数的区别.pdf │ ├── 56_数组指针和指针数组的区别.pdf │ ├── 63_7种经典的排序算法原理及C++实现.pdf │ ├── 72_vector的扩容原理以及resize和reserve的区别.pdf │ ├── 74_sort自定义排序.pdf │ ├── 75_快速幂.pdf │ ├── 76_二叉树前中后层遍历代码模板.pdf │ └── 77_牛客网笔试处理各种输入输出的解决方法.pdf ├── 其他常考察问题 │ ├── 01_用天平找次品.pdf │ └── 02_TCP三次握手四次挥手的过程.pdf ├── 图像处理 │ ├── 01_LBP算法原理.pdf │ ├── 02_HOG算法原理.pdf │ ├── 03_FAST、BRIEF、ORB算法原理.pdf │ ├── 08_边缘检测算子有哪些以及它们之间的对比.pdf │ ├── 12_常见的三种图像插值方法.pdf │ ├── 14_开操作与闭操作.pdf │ ├── 15_Hough变换检测直线与圆的原理.pdf │ ├── 17_图像拼接原理介绍.pdf │ └── 18_图像配准.pdf ├── 机器学习 │ ├── 01_LR和SVM的比较.pdf │ ├── 11_三种主要集成学习思想简介.pdf │ ├── 12_Adaboost_GBDT_XGBoost算法原理.pdf │ ├── 17_bagging算法思想及与DNN中的dropout思想的一致性.pdf │ ├── 19_如何从偏差和方差的角度解释bagging和boosting的原理.pdf │ ├── 20_随机森林思想.pdf │ ├── 26_k-means算法原理.pdf │ ├── 29_k-means和GMM的区别与联系.pdf │ ├── 33_决策树的原理以及决策树如何生成.pdf │ ├── 34_ID3_C4.5_CART算法总结与对比.pdf │ ├── 44_PCA算法原理.pdf │ ├── 45_LDA算法原理.pdf │ ├── 46_PCA与LDA比较.pdf │ ├── 49_判别式模型和生成式模型.pdf │ ├── 57_机器学习中常用的损失函数一览.pdf │ ├── 58_线性回归损失函数为什么要用平方形式.pdf │ └── 59_逻辑回归与线性回归之间的异同.pdf └── 计算机视觉 │ ├── 02_过拟合和欠拟合的表现与解决方法.pdf │ ├── 03_代码实现卷积操作.pdf │ ├── 04_BN层的深入理解.pdf │ ├── 05_NMS详细工作机制及代码实现.pdf │ ├── 06_三种常见的激活函数.pdf │ ├── 07_ReLU函数在0处不可导,为什么还能用.pdf │ ├── 08_Pooling层的作用以及如何进行反向传播.pdf │ ├── 09_为什么输入网络前要对图像做归一化.pdf │ ├── 10_理清深度学习优化函数发展脉络.pdf │ ├── 11_梯度消失和爆炸以及解决方法.pdf │ ├── 14_CNN在图像上表现好的原因.pdf │ ├── 21_手动推导反向传播公式BP.pdf │ ├── 22_CNN网络各种层的FLOPs和参数量paras计算.pdf │ ├── 23_弄明白感受野大小的计算问题.pdf │ ├── 24_卷积网络中的卷积与互相关的那点事.pdf │ ├── 32_为什么用F1-score.pdf │ ├── 34_简述CNN分类网络的演变脉络及各自的贡献与特点.pdf │ ├── 38_了解全卷积网络FCN.pdf │ ├── 40_解释 ROI Pooling 和 ROI Align 的区别.pdf │ ├── 41_CascadeRCNN本质解决了什么问题.pdf │ ├── 44_特征融合concat和add的区别.pdf │ ├── 46_LSTM介绍理解.pdf │ ├── 48_各种卷积方式串讲.pdf │ └── 49_Faster_R_CNN和Mask_R_CNN的损失函数详解.pdf ├── README.md ├── 其他常考察问题 ├── 01_用天平找次品.md └── 02_TCP三次握手四次挥手的过程.md ├── 图像处理 ├── 01_LBP算法原理.assets │ ├── 20140221184735734 │ ├── 20150805105836031 │ ├── HAfcpDgYmkdnsK1.png │ ├── OkCFW9vpgAStKT1.png │ ├── bzk3tnEWw5GjrC9.png │ └── izH7Sl3AdDqWEUu.png ├── 01_LBP算法原理.md ├── 02_HOG算法原理.md ├── 03_FAST、BRIEF、ORB算法原理.md ├── 08_边缘检测算子有哪些以及它们之间的对比.md ├── 12_常见的三种图像插值方法.md ├── 14_开操作与闭操作.md ├── 15_Hough变换检测直线与圆的原理.md ├── 17_图像拼接原理介绍.md └── 18_图像配准.md ├── 机器学习 ├── 01_LR和SVM的比较.md ├── 11_三种主要集成学习思想简介.md ├── 12_Adaboost_GBDT_XGBoost算法原理.md ├── 17_bagging算法思想及与DNN中的dropout思想的一致性.md ├── 19_如何从偏差和方差的角度解释bagging和boosting的原理.md ├── 20_随机森林思想.md ├── 26_k-means算法原理.md ├── 29_k-means和GMM的区别与联系.md ├── 33_决策树的原理以及决策树如何生成.md ├── 34_ID3_C4.5_CART算法总结与对比.md ├── 44_PCA算法原理.md ├── 45_LDA算法原理.md ├── 46_PCA与LDA比较.md ├── 49_判别式模型和生成式模型.md ├── 57_机器学习中常用的损失函数一览.md ├── 58_线性回归损失函数为什么要用平方形式.md └── 59_逻辑回归与线性回归之间的异同.md └── 计算机视觉 ├── 02_过拟合和欠拟合的表现与解决方法.md ├── 03_代码实现卷积操作.md ├── 04_BN层的深入理解.md ├── 05_NMS详细工作机制及代码实现.md ├── 06_三种常见的激活函数.md ├── 07_ReLU函数在0处不可导,为什么还能用.md ├── 08_Pooling层的作用以及如何进行反向传播.md ├── 09_为什么输入网络前要对图像做归一化.md ├── 10_理清深度学习优化函数发展脉络.md ├── 11_梯度消失和爆炸以及解决方法.md ├── 14_CNN在图像上表现好的原因.md ├── 21_手动推导反向传播公式BP.md ├── 22_CNN网络各种层的FLOPs和参数量paras计算.md ├── 23_弄明白感受野大小的计算问题.md ├── 24_卷积网络中的卷积与互相关的那点事.md ├── 32_为什么用F1-score.md ├── 34_简述CNN分类网络的演变脉络及各自的贡献与特点.md ├── 38_了解全卷积网络FCN.md ├── 40_解释 ROI Pooling 和 ROI Align 的区别.md ├── 41_CascadeRCNN本质解决了什么问题.md ├── 44_特征融合concat和add的区别.md ├── 46_LSTM介绍理解.md ├── 48_各种卷积方式串讲.md └── 49_Faster_R_CNN和Mask_R_CNN的损失函数详解.md /C++基础/02_incluede后面使用双引号和尖括号的区别.md: -------------------------------------------------------------------------------- 1 | ## 问题 2 | 3 | incluede 后面使用双引号 " " 和尖括号 <> 的区别 4 | 5 | ## 解答 6 | 7 | 预处理器发现 `#include` 指令后,就会寻找后跟的文件名并把这个文件的内容包含到当前文件中。被包含文件中的文本将替换源代码文件中的`#include`指令,就像你把被包含文件中的全部内容键入到源文件中的这个位置一样。 8 | 9 | ```c++ 10 | #include // 文件名放在尖括号中 11 | #include “mystuff.h” //文件名放在双引号中 12 | ``` 13 | 14 | 尖括号`< > `括起来表明这个文件是一个工程或标准头文件。查找过程会检查预定义的目录,我们可以通过设置搜索路径环境变量或命令行选项来修改这些目录。 15 | 16 | 如果文件名用一对`" "`引号括起来则表明该文件是用户提供的头文件,查找该文件时将从当前文件目录(或文件名指定的其他目录)中寻找文件,然后再到标准位置寻找文件。 -------------------------------------------------------------------------------- /C++基础/04_C++11常用新特性.md: -------------------------------------------------------------------------------- 1 | ## 问题 2 | 3 | C++11新增了很多新特性,这也成为了面试中非常常见的问题,这里介绍一些常用的新特性。C++11新特性有很多,这里就简单整理几个很常见的,应该足以应对面试中的问题了。 4 | 5 | ## C++11新特性 6 | 7 | ### ● 初始化列表 8 | 9 | 初始化列表,即用花括号来进行初始化。C++11中可以直接在变量名后面跟上初始化列表来进行对象的初始化,使用起来更加方便,例如: 10 | 11 | ```C++ 12 | vector vec; //C++98/03给vector对象的初始化方式 13 | vec.push_back(1); 14 | vec.push_back(2); 15 | 16 | vector vec{1,2}; //C++11给vector对象的初始化方式 17 | vector vec = {1,2}; 18 | ``` 19 | 20 | 21 | 22 | ## ● auto关键字 23 | 24 | C++11之前,**在使用表达式给变量赋值的时候需要知道表达式的类型,如char、int等,然而有的时候要做到这一点并不容易**,因此,为了解决这个问题,C++11引入了`auto`关键字,编译器可以分析表达式的结果来进行类型推导。当然,直接定义变量的时候也可以使用`auto`来推导类型,可以理解为`auto`相当于一个占位符,在编译期间会自动推导出变量的类型。 25 | 26 | ```C++ 27 | auto a = 2; //推导出a为int类型 28 | auto b = 2.5; //推导出b为double类型 29 | auto c = &a; //推导出c为int*类型 30 | 31 | vector vec = {1,2,3,4}; 32 | vector::iterator it = vec.begin(); //初始化迭代器 33 | auto it = vec.begin(); //使用auto后更加方便 34 | ``` 35 | 36 | 使用`auto`时**必须对变量进行初始化**;另外,也可以使用`auto`定义多个变量,但**必须注意,多个变量推导的结果必须为相同类型**,如: 37 | 38 | ```C++ 39 | auto a; //错误,没有初始化 40 | int a = 2; 41 | auto *p = &a, b = 4; //正确,&a为int*类型,因此auto推导的结果是int类型,b也是int类型 42 | auto *p = &a, b = 4.5; //错误,auto推导的结果为int类型,而b推导为double类型,存在二义性 43 | ``` 44 | 45 | **`auto`使用的限制:** 46 | 47 | ①`auto`定义变量时必须初始化 48 | 49 | ②`auto`不能在函数的参数中使用 50 | 51 | ③`auto`不能定义数组,例如:`auto arr[] = "abc"`,(`auto arr = "abc"`这样是可以的,但arr不是数组,而是指针) 52 | 53 | ④`auto`不能用于类的非静态成员变量中 54 | 55 | 56 | 57 | ## ● decltype关键字 58 | 59 | 有时候会遇到这样的情况:希望从表达式的类型中推断出要定义的变量的类型,但是想用该表达式的值来初始化变量。C++11中引入了 `decltype`关键字来解决这个问题,编译器通过分析表达式的结果来返回相应的数据类型。 60 | 61 | 格式: 62 | 63 | ```C++ 64 | decltype(表达式) 变量名 [=初始值]; //[]表示可选,下面用exp来表示表达式 65 | ``` 66 | 67 | `decltype` 的使用遵循以下3条规则: 68 | 69 | ①若exp是一个不被括号`()`包围的表达式,或者是单独的变量,其推导的类型将和表达式本身的类型一致 70 | 71 | ②若exp是函数调用,则`decltype(exp)`的类型将和函数返回值类型一致 72 | 73 | ③若exp是一个左值,或者是一个被括号`()`包围的值,那么 `decltype(exp)`的类型将是exp的引用 74 | 75 | 具体示例: 76 | 77 | ```C++ 78 | class Base{ 79 | public: 80 | int m; 81 | }; 82 | int fun(int a, int b){ 83 | return a+b; 84 | } 85 | 86 | int main(){ 87 | int x = 2; 88 | decltype(x) y = x; //y的类型为int,上述规则1 89 | decltype(fun(x,y)) sum; //sum的类型为函数fun()的返回类型,上述规则2 90 | 91 | Base A; 92 | decltype(A.m) a = 0; //a的类型为int 93 | decltype((A.m)) b = a; //exp由括号包围,b的类型为int&,符合上述规则3 94 | 95 | decltype(x+y) c = 0; //c的类型为int 96 | decltype(x=x+y) d = c; //exp为左值,则d的类型为int&,符合上述规则3 97 | return 0; 98 | } 99 | ``` 100 | 101 | 102 | 103 | **`decltype`和`auto`的区别:** (两者都可以推导出变量的类型) 104 | 105 | ● `auto` 是根据等号右边的初始值推导出变量的类型,且变量必须初始化,`auto`的使用更加简洁 106 | 107 | ● `decltype` 是根据表达式推导出变量的类型,不要求初始化,`decltype`的使用更加灵活 108 | 109 | 110 | 111 | ## ● 范围for循环 112 | 113 | 类似于python中的for-in语句,使用格式及例子如下: 114 | 115 | ```C++ 116 | vector nums = {1,2,3,4}; 117 | //使用冒号(:)来表示从属关系,前者是后者中的一个元素,for循环依次遍历每个元素,auto自动推导为int类型 118 | for(auto num : nums){ 119 | cout << num << endl; 120 | } 121 | ``` 122 | 123 | 124 | 125 | ## ● nullptr关键字 126 | 127 | C++11使用`nullptr`代替了`NULL`,原因是`NULL`有时存在二义性,有的编译器可能将`NULL`定义为`((void*)0)`,有的则直接定义为0。 128 | 129 | ```C++ 130 | void fun(int x) { 131 | cout << x << endl; 132 | } 133 | void fun(int *p) { 134 | if (p != NULL) cout << *p << endl; 135 | } 136 | 137 | int main() { 138 | fun(0); //在C++98中编译失败,存在二义性,在C++11中编译为fun(int) 139 | return 0; 140 | } 141 | ``` 142 | 143 | `nullptr`是一种特殊类型的字面值,可以被转换成任意其他的指针类型,也可以初始化一个空指针。 144 | 145 | ```C++ 146 | int *p = nullptr; //等价于 int *p = 0; 147 | ``` 148 | 149 | 150 | 151 | ## ● lambda表达式 152 | 153 | lambda表达式定义了一个匿名函数,一个lambda具有一个返回类型、一个参数列表和一个函数体。与函数不同的是,lambda表达式可以定义在函数内部,其格式如下: 154 | 155 | ```C++ 156 | [capture list] (parameter list) -> return type { function body } 157 | //[捕获列表] (参数列表) -> 返回类型 { 函数体 } 158 | ``` 159 | 160 | ● capture list(捕获列表):定义局部变量的列表(通常为空) 161 | 162 | ● parameter list(参数列表)、return type(返回类型)、function body(函数体)和普通函数一样 163 | 164 | ● 可以忽略参数列表和返回类型,但**必须包括捕获列表和函数体** 165 | 166 | 示例: 167 | 168 | ```C++ 169 | auto sum = [](int a, int b) -> int { return a+b; }; //一个完整的lambda表达式 170 | cout << sum(1, 2) << endl; //输出3 171 | 172 | auto fun = [] { return 4; }; //省略参数列表和返回类型 173 | cout << fun() << endl; //打印结果为:4 174 | ``` 175 | 176 | lambda表达式可以定义在函数内: 177 | 178 | ```C++ 179 | //使用lambda表达式和sort排序自定义一个降序排序算法 180 | #include 181 | #include 182 | #include 183 | using namespace std; 184 | 185 | //bool cmp(const int a, const int b) { 186 | // return a > b; // 前者大于后者返回true,因此为降序排序 187 | //} 188 | 189 | int main() { 190 | vector nums{ 13, 5, 3, 7, 43 }; 191 | //sort(nums.begin(), nums.end(), cmp); // 1.使用函数来定义,需要自定义一个cmp函数来调用 192 | //2.直接使用lambda表达式 193 | sort(nums.begin(), nums.end(), [](int a, int b)-> int { return a > b; }); 194 | for (auto i : nums) { 195 | cout << i << " "; 196 | } 197 | cout << endl; 198 | system("pause"); 199 | return 0; 200 | } 201 | ``` 202 | 203 | 使用捕获列表: 204 | 205 | ● [] 不捕获任何变量 206 | 207 | ● [&] 捕获外部作用域中所有变量,并作为引用在函数体中使用(按引用捕获)。 208 | 209 | ● [=] 捕获外部作用域中所有变量,并作为副本在函数体中使用(按值捕获)。 210 | 211 | ● [=,&x] 按值捕获外部作用域中所有变量,并按引用捕获 x 变量。 212 | 213 | ● [x] 按值捕获 x 变量,同时不捕获其他变量。 214 | 215 | ```C++ 216 | //下面使用lambda表达式编写一个函数,从数组中找到第一个大于给定长度的字符串 217 | #include 218 | #include 219 | #include 220 | #include 221 | using namespace std; 222 | 223 | int main() { 224 | vector str = {"abcd", "hello", "hi", "hello world", "hello abcd"}; 225 | int len = 5; 226 | 227 | //使用lambda表达式,len为按值捕获的变量 228 | auto iter = find_if(str.begin(), str.end(), [len](const string &s) {return s.size() > len; }); 229 | 230 | cout<<"The length of first word longer than "<` 中,在C++11中已经被弃用了,因为它不够安全,而且可以被 `unique_ptr` 代替。那它为什么会被 `unique_ptr` 代替呢?先看下面这段代码: 20 | 21 | ```C++ 22 | #include 23 | #include 24 | #include 25 | using namespace std; 26 | 27 | int main() { 28 | auto_ptr p1(new string("hello world.")); 29 | auto_ptr p2; 30 | p2 = p1; //p2接管p1的所有权 31 | cout << *p2<< endl; //正确,输出: hello world. 32 | //cout << *p1 << endl; //程序运行到这里会报错 33 | 34 | //system("pause"); 35 | return 0; 36 | } 37 | 38 | ``` 39 | 40 | 由上面可以看到,当第9行代码执行赋值操作后,`p2` 接管了`p1`的所有权,但此时程序并不会报错,而且运行第10行代码时可以正确输出指向对象的内容。而第11行注释掉的代码则会报错,因为`p1`此时已经是空指针了,访问输出它会使程序崩溃。可见,`auto_ptr` 智能指针并不够安全,于是有了它的替代方案:即 `unique_ptr` 指针。 41 | 42 | 43 | 44 | ## 2. unique_ptr 45 | 46 | `unique_ptr` 同 `auto_ptr` 一样也是采用所有权模式,即同一时间只能有一个智能指针可以指向某个对象 ,但之所以说**使用 `unique_ptr` 智能指针更加安全**,是因为它相比于 `auto_ptr` 而言**禁止了拷贝操作**(`auto_ptr`支持拷贝赋值,前面代码示例就是个例子), `unique_ptr` 采用了移动赋值 `std::move()`函数来进行控制权的转移。例如: 47 | 48 | ```C++ 49 | #include 50 | #include 51 | #include 52 | using namespace std; 53 | 54 | int main() { 55 | unique_ptr p1(new string("hello world")); 56 | //unique_ptr p2(p1); //编译不通过,禁止拷贝操作 57 | //unique_ptr p2 = p1; //编译不通过 58 | unique_ptr p2 = std::move(p1); 59 | cout << *p2 << endl; 60 | //cout << *p1 < p1(new string("hello world")); 74 | unique_ptr p2(p1.release()); //p2获得控制权,p1被置为空 75 | unique_ptr p3; 76 | p3.reset(p2.release()); //p3获得p2的控制权,reset释放了p2原来的内存 77 | ``` 78 | 79 | 此外,**`unique_ptr`使用也很灵活,如果`unique_ptr`是个临时右值,编译器允许拷贝操作**。 80 | 81 | ```C++ 82 | unique_ptr p2; 83 | p2 = unique_ptr(new string("hello world")); //正确,临时右值可以进行拷贝赋值操作 84 | ``` 85 | 86 | 上面这种方式进行拷贝操作不会留下悬挂指针,因为它调用了`unique_ptr`的构造函数,该构造函数创建的临时对象在其所有权让给 `p2` 后就会被销毁。 87 | 88 | 89 | 90 | ## 3.share_ptr 91 | 92 | 共享指针 `share_ptr`是一种可以共享所有权的智能指针,定义在头文件`memory`中,它允许多个智能指针指向同一个对象,并使用引用计数的方式来管理指向对象的指针(成员函数use_count()可以获得引用计数),该对象和其相关资源会在“最后一个引用被销毁”时候释放。 93 | 94 | `share_ptr`是为了解决 `auto_ptr` 在对象所有权上的局限性(`auto_ptr` 是独占的),在使用引用计数的机制上提供了可以共享所有权的智能指针。 95 | 96 | > 引用计数变化的几种情况: 97 | > 98 | > ①在创建智能指针类的新对象时,初始化指针,引用计数设置为1; 99 | > 100 | > ②当智能指针类的对象作为另一个对象的副本时(即进行了拷贝操作),引用计数加1; 101 | > 102 | > ③当使用赋值操作符对一个智能指针类对象进行赋值时(如`p2=p1`),左操作数(即`p2`)引用先减1,因为它已经指向了别的地方,如果减1后引用计数为0,则释放指针所指对象的内存;然后右操作数(即`p1`)引用加1,因为此时左操作数指向了右操作数的对象。 103 | > 104 | > ④调用析构函数时,析构函数先使引用计数减1,若减至0则delete对象。 105 | 106 | #### `share_ptr` 与内存泄漏: 107 | 108 | 当两个对象分别使用一个共享指针`share_ptr` 指向对方时,会导致内存泄漏的问题。 109 | 110 | 可以这样来理解:智能指针是用来管理指针的,当它指向某个对象时,该对象上的引用计数会加1,当引用计数减到0时该对象会销毁,也就是说**智能指针使用引用计数机制来管理着它所指对象的生命周期**,因此若某个对象A的`share_ptr` 指向了对象B,那么对象A只能在对象B先销毁之后它才会销毁;同理,若对象B的`share_ptr` 也指向了对象A,则只有在对象A先销毁之后B才会被销毁。因此,当两个对象的`share_ptr` 相互指向对方时,两者的引用计数永远不会减至0,即两个对象都不会被销毁,就会造成内存泄漏的问题。 111 | 112 | 113 | 114 | ## 4.weak_ptr 115 | 116 | `weak_ptr` 弱指针是一种不控制对象生命周期的智能指针,它指向一个 `share_ptr` 管理的对象,进行该对象的内存管理的是那个强引用的 `share_ptr` ,也就是说 `weak_ptr` 不会修改引用计数,只是提供了一种访问其管理对象的手段,这也是它称为弱指针的原因所在。 117 | 118 | 此外,`weak_ptr` 和 `share_ptr` 之间可以相互转化,`share_ptr` 可以直接赋值给`weak_ptr` ,而`weak_ptr` 可以通过调用 `lock` 成员函数来获得`share_ptr` 。 119 | 120 | ```C++ 121 | //shared_ptr和weak_ptr代码示例 122 | #include 123 | #include 124 | using namespace std; 125 | 126 | class B; 127 | class A { 128 | public: 129 | shared_ptr _pb; 130 | //weak_ptr _pb; 131 | ~A() { cout << "A delete" << endl; } 132 | }; 133 | class B { 134 | public: 135 | shared_ptr _pa; 136 | ~B() { cout << "B delete" << endl; } 137 | }; 138 | 139 | int main() { 140 | shared_ptr pb(new B()); //创建一个B类对象的share_ptr 141 | shared_ptr pa(new A()); //创建一个A类对象的share_ptr 142 | pb->_pa = pa; //B指向A 143 | pa->_pb = pb; //A指向B 144 | cout << pb.use_count() << endl; //打印结果为:2 145 | cout << pa.use_count() << endl; //打印结果为:2,可见两个指针相互引用了 146 | 147 | //system("pause"); 148 | return 0; 149 | } 150 | //注:若将第9行代码注释掉,第10行取消注释,打印的结果将分别为:1 和 2 151 | //当B析构函数时,其计数会变为0,B被释放,B释放的同时会使得A的计数减1, 152 | //A再析构后A的计数也会变为0,得以释放 153 | ``` 154 | 155 | 156 | 157 | ## 参考资料 158 | 159 | [牛客网-C++工程师面试宝典](https://www.nowcoder.com/tutorial/93/8f38bec08f974de192275e5366d8ae24) 160 | 161 | [C++ STL 四种智能指针](https://blog.csdn.net/k346k346/article/details/81478223) -------------------------------------------------------------------------------- /C++基础/11_new&delete与malloc&free 之间的关系与区别.md: -------------------------------------------------------------------------------- 1 | ## 问题 2 | 3 | 请说说new/delete与malloc/free的区别? 4 | 5 | ## 区别 6 | 7 | ● new和delete是**C++的关键字**,是一种操作符,**可以被重载** 8 | 9 | ● malloc和free是**C语言的库函数**,并且**不能重载** 10 | 11 | ● malloc使用时需要自己显示地计算内存大小,而new使用时由编译器自动计算 12 | 13 | ```C++ 14 | int *q = (int *)malloc(sizeof(int) * 2); //显示计算内存大小 15 | int *p = new int[2]; //编译器会自动计算 16 | ``` 17 | 18 | ● malloc分配成功后返回的是**void*指针**,需要强制类型转换成需要的类型;而new**直接就返回了对应类型的指针** 19 | 20 | ● new和delete使用时会分别调用构造函数和析构函数,而malloc和free只能申请和释放内存空间,不会调用构造函数和析构函数 21 | 22 | 23 | 24 | ## 代码示例 25 | 26 | 注意:**delete和free被调用后,内存不会立即回收,指针也不会指向空**,delete或free仅仅是告诉操作系统,这一块内存被释放了,可以用作其他用途。但是由于没有重新对这块内存进行写操作,所以内存中的变量数值并没有发生变化,这时候就会出现野指针的情况。因此,释放完内存后,应该把指针指向NULL。 27 | ```C++ 28 | int main() { 29 | int *q = (int *)malloc(sizeof(int) * 2); 30 | int *p = new int[2]; 31 | cout << "p = " << p << " q = " << q << endl; 32 | free(p); //指针还没指向空 33 | delete q; //同上 34 | cout << "p = " << p << " q = " << q << endl; 35 | return 0; 36 | } 37 | //上面程序运行的结果,可见第二次打印的时候p和q指针还没指向空 38 | //这里不知为何编译器第二次打印的q和第一次不一样 39 | // p = 000000000039B9B0 q = 000000000039B960 40 | // p = 000000000039B9B0 q = 0000000000008123 41 | ``` 42 | 43 | 44 | 45 | ## 参考资料 46 | 47 | [new 和 malloc free 和 delete 的区别](https://blog.csdn.net/ii0789789789/article/details/86851445) 48 | 49 | [new/delete与malloc/free的区别与联系详解!](new/delete与malloc/free的区别与联系详解!) -------------------------------------------------------------------------------- /C++基础/12_delete和delete[]的区别.md: -------------------------------------------------------------------------------- 1 | ## 问题 2 | 3 | C++中释放内存的方式有 delete 和 delete[] 两种,它们的区别是什么呢? 4 | 5 | ## 区别 6 | 7 | 在C++ Primer的书中提到,delete用来释放new申请的动态内存,delete[]用来释放由new[]申请的动态内存,也就是说: 8 | 9 | ● delete释放new分配的**单个对象指针**指向的内存 10 | 11 | ● delete[]释放new分配的**对象数组指针**指向的内存 12 | 13 | 直接这样理解对吗?先看看下面这段代码: 14 | 15 | ```C++ 16 | int *a = new int[10]; 17 | delete a; //方式1 18 | delete [] a; //方式2 19 | ``` 20 | 21 | 事实上,上面两种释放内存的方式都是正确的,方式1并不会造成内存泄漏。这是为什么呢?因为在申请动态内存的时候,一般有两种情况:基本数据类型的分配和自定义数据类型的分配,两者在释放内存的时候略有不同。 22 | 23 | ## 基本数据类型 24 | 25 | 对于像 **int/char/long** 等等这些基本数据类型,使用new分配的不管是数组还是非数组形式的内存空间,delete和delete[]都可以正常释放,不会存在内存泄漏。**原因是:分配这些基本数据类型时,其内存大小已经确定,系统可以记忆并且进行管理,在析构时不会调用析构函数,它通过指针可以直接获取实际分配的内存空间**。 26 | 27 | ```c++ 28 | int *a = new int[10]; 29 | delete a; //正确 30 | delete [] a; //正确 31 | ``` 32 | 33 | ## 自定义数据类型 34 | 35 | 对于自定义的Class类,delete和delete[]会有一定的差异。先看一段代码示例: 36 | 37 | ```c++ 38 | class T { 39 | public: 40 | T() { cout << "constructor" << endl; } 41 | ~T() { cout << "destructor" << endl; } 42 | }; 43 | 44 | int main() { 45 | T* p1 = new T[3]; //数组中包含了3个类对象 46 | cout << hex << p1 << endl; //输出P1的地址 47 | delete[] p1; 48 | 49 | cout << endl; 50 | 51 | T* p2 = new T[3]; 52 | cout << p2 << endl; //输出P2的地址 53 | delete p2; 54 | 55 | return 0; 56 | } 57 | ``` 58 | 59 | 上面程序的运行结果: 60 | 61 | ![](https://i.loli.net/2020/05/22/mIwzKtRjFVMYTJX.png) 62 | 63 | 可以看到,delete[]在释放内存的时候,调用了3次析构函数,也就是说,**delete[]会调用析构函数对数组的所有对象进行析构**;而**delete只调用了一次析构函数**,即数组中第一个对象的析构函数,而数组中剩下的对象的析构函数没有被调用,因此**造成了内存泄漏**。 64 | 65 | ## 总结 66 | 67 | **● 对于基本数据类型:** 68 | 69 | 不管释放的是单个对象还是数组对象,使用delete和delete[]释放内存的效果相同。 70 | 71 | **● 对于自定义数据类型:** 72 | 73 | 释放数组对象时,使用delete时只会调用第一个对象的析构函数,可能会造成内存泄漏;而使用delete[]时会逐个调用析构函数来释放数组的所有对象。 74 | 75 | ## 参考资料 76 | 77 | [delete 和 delete[] 的真正区别](https://www.cnblogs.com/wangjian8888/p/7905176.html) 78 | 79 | [C++中的delete和delete[ ]的区别](https://blog.csdn.net/u012936940/article/details/80919880) 80 | 81 | -------------------------------------------------------------------------------- /C++基础/14_虚函数详解.md: -------------------------------------------------------------------------------- 1 | ## 问题 2 | 3 | 虚函数可以说是在涉及C++的面试问题中经久不衰的话题了,这里就介绍一下虚函数的概念以及相关的常见问题。 4 | 5 | ## 初识虚函数 6 | 7 | ● 虚函数是指在**基类内部**声明的成员函数前添加**关键字 virtual 指明的函数** 8 | 9 | ● 虚函数存在的意义是为了**实现多态**,**让派生类能够重写(override)**其基类的成员函数 10 | 11 | ● 派生类重写基类的虚函数时,可以添加 virtual 关键字,但不是必须这么做 12 | 13 | ● 虚函数**是动态绑定的,在运行时才确定**,而非虚函数的调用在编译时确定 14 | 15 | ● 虚函数**必须是非静态成员函数**,因为静态成员函数需要在编译时确定 16 | 17 | ● **构造函数不能是**虚函数,因为虚函数是动态绑定的,而构造函数创建时需要确定对象类型 18 | 19 | ● **析构函数一般是**虚函数 20 | 21 | ● 虚函数一旦声明,就一直是虚函数,派生类也无法改变这一事实 22 | 23 | 下面举个例子来帮助理解: 24 | 25 | ```c++ 26 | #include 27 | using namespace std; 28 | 29 | class Base { 30 | public: 31 | void f() { cout << "Base::f" << endl; }; //一般成员函数 32 | virtual void f1() { cout << "Base::f1" << endl; }; //虚函数 33 | private: 34 | char aa[3]; 35 | }; 36 | class Derived : public Base { 37 | public: 38 | void f1() { cout << "Derived::f1" << endl; }; //重写基类的虚函数f1() 39 | private: 40 | char bb[3]; 41 | }; 42 | 43 | int main() { 44 | Base a; 45 | Derived b; 46 | Base *p = &a; 47 | Base *p0 = &b; 48 | Base *p1 = &a; 49 | Base *p2 = &b; 50 | Derived *p3 = &b; 51 | 52 | p->f(); //基类的指针调用自己的基类部分 Base::f(), 打印结果为 Base::f 53 | p0->f(); //基类的指针调用派生类的基类部分 Base::f(), 打印结果为 Base::f 54 | 55 | p1->f1(); //基类的指针调用基类自身的函数 Base::f1(), 打印结果为 Base::f1 56 | p2->f1(); //基类的指针调用派生类重写的函数 Derived::f1(), 打印结果为 Derived::f1 57 | p3->f1(); //派生类调用自己重写的函数 Derived::f1(),打印结果为 Derived::f1 58 | return 0; 59 | } 60 | ``` 61 | 62 | ## 虚函数的工作机制 63 | 64 | **主要思路** 65 | 66 | 虚函数表 + 虚表指针 67 | 68 | **具体实现** 69 | 70 | ● 编译器在含有虚函数的类中创建一个虚函数表,称为vtable,这个vtable用来存放虚函数的地址。另外还隐式地设置了一个虚表指针,称为vptr,这个vptr指向了该类对象的虚函数表。 71 | 72 | ● 派生类在继承基类的同时,也会继承基类的虚函数表(暂且可以认为派生类此时包含了两个虚函数表所有的内容,具体接着看下面两种情况)。 73 | 74 | ● 当派生类重写(override)了基类的虚函数时,则会将重写后的虚函数的地址 **替换掉** 由基类继承而来的虚函数表中对应虚函数的地址(有点绕,多读一遍这一句就清楚了,这也是重写的含义所在吧,也就是覆盖掉基类部分虚函数的地址)。 75 | 76 | ● 若派生类没有重写,则由基类继承而来的虚函数的地址将直接保存在派生类的虚函数表中。 77 | 78 | **补充** 79 | 80 | 每个类都只有一个虚函数表,**该类的所有对象共享这个虚函数表**,而不是每个实例化对象都分别有一个虚函数表。 81 | 82 | **图片理解** 83 | 84 | ![](https://i.loli.net/2020/05/18/9gcnszwpJy2ZKh8.png) 85 | 86 | ![](https://i.loli.net/2020/05/18/M3be2OyvgWFXYqx.png) 87 | 88 | ![](https://i.loli.net/2020/05/18/fd8CEGanByJ1Ovr.png) 89 | 90 | ![](https://i.loli.net/2020/05/18/pFj5u39bdBsnkE6.png) 91 | 92 | 如果上面4张图还不足以帮助你理解的话,还可以更直观地看下面这张图: 93 | 94 | ![](https://i.loli.net/2020/05/19/1K8GqvrILXfOCVU.png) 95 | 96 | 97 | 98 | ## 虚函数与运行时多态 99 | 100 | 首先要理解什么是多态? 101 | 102 | 多态的实现主要分为静态多态和动态多态,静态多态主要是重载(overload),在编译时就已经确定;而动态多态是通过虚函数机制类实现,在运行时动态绑定。 103 | 104 | **动态多态是指基类的指针指向其派生类的对象,通过基类的指针来调用派生类的成员函数。**如何理解这句话?我们再来看看本文开头的一段代码: 105 | 106 | ```c++ 107 | int main() { 108 | Base a; 109 | Derived b; //派生类的对象 110 | Base *p1 = &a; 111 | Base *p2 = &b; //基类的指针,指向派生类的对象 112 | p1->f1(); 113 | p2->f1(); //基类的指针调用派生类重写的虚函数 Derived::f1(), 打印结果为 Derived::f1 114 | return 0; 115 | } 116 | ``` 117 | 118 | 如果基类通过引用或者指针调用的是非虚函数,无论实际的对象是什么类型,都执行基类所定义的函数。即: 119 | 120 | ```C++ 121 | int main() { 122 | Base a; 123 | Derived b; 124 | Base *p = &a; //基类的指针,指向基类的对象 125 | Base *p0 = &b; //基类的指针,指向派生类的对象 126 | //f()是非虚函数,所以无论指向是基类的对象a还是派生类的对象b,执行的都是基类的函数f() 127 | p->f(); 128 | p0->f(); 129 | return 0; 130 | } 131 | ``` 132 | 133 | 现在可以理解什么是运行时多态了。 134 | 135 | **C++类的多态性是通过虚函数来实现的。如果基类通过引用或指针调用的是虚函数时,我们并不知道执行该函数的对象是什么类型的,只有在运行时才能确定调用的是基类的虚函数还是派生类中的虚函数,这就是运行时多态。** 136 | 137 | ```C++ 138 | int main() { 139 | Base a; 140 | Derived b; 141 | Base *p1 = &a; //基类的指针,指向基类的对象 142 | Base *p2 = &b; //基类的指针,指向派生类的对象 143 | //f1()是虚函数,只有运行时才知道真正调用的是基类的f1(),还是派生类的f1() 144 | p1->f1(); //p1指向的是基类的对象,所以此时调用的是基类的f1() 145 | p2->f1(); //p2指向的是派生类的对象,所以调用的是派生类重写后的f1() 146 | return 0; 147 | } 148 | ``` 149 | 150 | 总结一下: 151 | 152 | **多态性其实就是想让基类的指针具有多种形态,能够在尽量少写代码的情况下让基类可以实现更多的功能**。比如说,派生类重写了基类的虚函数f1()之后,基类的指针就不仅可以调用自身的虚函数f1(),还可以调用其派生类的虚函数f1(),这是不是就可以多实现一些操作了呀。 153 | 154 | 155 | 156 | ## 虚函数与静态函数的区别 157 | 158 | **静态函数在编译时已经确定,而虚函数是在运行时动态绑定的**。 159 | 160 | 虚函数因为用了虚函数表的机制,所以在调用的时候会**增加一次内存开销**。 161 | 162 | 163 | 164 | ## 参考资料 165 | 166 | [C++虚函数表解析](https://blog.csdn.net/haoel/article/details/1948051) 167 | 168 | [虚函数](https://blog.csdn.net/qq_42039281/article/details/80596006) -------------------------------------------------------------------------------- /C++基础/15_纯虚函数.md: -------------------------------------------------------------------------------- 1 | ## 纯虚函数 2 | 3 | 上一篇文章整理了什么是虚函数及其工作机制(请参考14_虚函数详解),接下来就顺便也看看什么是纯虚函数吧。 4 | 5 | ## 初识纯虚函数 6 | 7 | ● 纯虚函数**只在基类中声明,但没有定义**,因此没有函数体。 8 | 9 | ● 纯虚函数的声明只需在虚函数形参列表后面**添加 =0** 即可。 10 | 11 | ● **含有纯虚函数的类都是抽象类**。 12 | 13 | ● **只含有纯虚函数的类称为接口类。** 14 | 15 | ## 函数声明 16 | 17 | 纯虚函数的声明很简单,就是在虚函数的形参列表后面添加一个 **=0** 即可,如: 18 | 19 | ```C++ 20 | class Cat { 21 | public: 22 | virtual void eat()=0; 23 | }; 24 | ``` 25 | 26 | ## 纯虚函数与抽象类 27 | 28 | **含有纯虚函数的类称为抽象类**(注意!!只要含有就是)。什么是抽象类?它有以下几个特点: 29 | 30 | ● **抽象类不能实例化对象。** 31 | 32 | ● **抽象类的派生类也可以是抽象类(会继承)**,**也可以通过实现全部的纯虚函数使其变成非抽象类**,从而可以实例化对象。 33 | 34 | ● **抽象类的指针可以指向其派生类对象,并调用派生类对象的成员函数。** 35 | 36 | 举个例子,在基类Cat中有两个纯虚函数eat()和sleap(),基类不能直接实例化一个对象来调用这两个函数,但在其派生类CatA和CatB中,可以通过实现这两个函数,当派生类不是抽象类时,便可以实例化对象了。具体请看下面代码示例: 37 | 38 | ```C++ 39 | class Cat{ 40 | public: 41 | //含有纯虚函数,因此Cat为抽象类 42 | virtual void eat() = 0; 43 | virtual void sleap() = 0; 44 | }; 45 | class CatA : public Cat { 46 | public: 47 | virtual void eat() { cout << "eat fish." << endl; }; //实现了eat()函数 48 | virtual void sleap() = 0; //仍为纯虚函数,因此CatA也是抽象类 49 | }; 50 | class CatB : public CatA { 51 | public: 52 | //两个纯虚函数都被实现,都变成一般的虚函数,因此CatB不是抽象类 53 | virtual void eat() { cout << "eat fish." << endl; }; 54 | virtual void sleap() { cout << "sleap for a long time." << endl; }; 55 | }; 56 | 57 | int main() { 58 | Cat a; //报错,Cat是抽象类,不能实例化对象 59 | CatA A; //报错,CatA也是抽象类,不能实例化对象 60 | CatB B; //正确,CatB不是抽象类 61 | CatB *p1 = &B; 62 | CatA *p2 = &B; //抽象类虽然不能实例化对象,但是可以声明其指针或引用 63 | p1->eat(); //打印出 eat fish. 64 | p1->sleap(); //打印出 sleap for a long time. 65 | p2->eat(); //打印出 eat fish. 66 | p2->sleap(); //打印出 sleap for a long time. 67 | return 0; 68 | } 69 | ``` 70 | 71 | 补充一个小疑惑: 72 | 73 | 刚了解抽象类概念的时候,有个疑惑:既然抽象类不能直接实例化对象,为何不直接使用带有虚函数的一般类就好,而是要定义一个不能实例化对象的抽象类?原因我们可以这样理解,比如我们定义一个类的时候,我们希望它应该具有某种功能(如Teachers类有一个teaching功能,但具体教什么科目还不知道),因此我们可以在定义了它的派生类之后(如MathTeacher),再具体实现teaching这个函数。 74 | 75 | 76 | 77 | ## 纯虚函数与接口类 78 | 79 | **● 只含有纯虚函数的类称为接口类。**(注意!!是只含有) 80 | 81 | **● 接口类没有任何数据成员,也没有构造函数和析构函数。** 82 | 83 | **● 接口类的指针也可以指向其派生类对象** 84 | 85 | -------------------------------------------------------------------------------- /C++基础/16_析构函数.md: -------------------------------------------------------------------------------- 1 | ## 问题 2 | 3 | 本节主要整理一下关于构造函数和析构函数的几个问题。 4 | 5 | ## 初识析构函数 6 | 7 | ● 析构函数通过在函数名前面加一个**位取反符号 ~**,如 ~Cat() 8 | 9 | ● 析构函数名与类名相同,一个类只能有一个析构函数,不能重载 10 | 11 | ● 析构函数不能有任何参数,也没有返回值 12 | 13 | ● 若没有写析构函数,编译器会自动生成一个默认的析构函数 14 | 15 | ● 当对象的生命周期结束时,系统会自动调用析构函数 16 | 17 | 18 | 19 | ### 构造函数为什么一般不定义为虚函数? 20 | 21 | 在 ***14_虚函数详解*** 那一节中也有提到,**构造函数不能是虚函数,原因是构造函数在创建对象时必须确定对象类型**。具体的理解就是: 22 | 23 | ①首先,**在创建一个对象时必须确定其类型。因为类型规定了对象可以进行哪些操作**,所以创建对象时必须确定类型,以防止一些不恰当的操作,否则编译器就会报错。 24 | 25 | ②其次,因为虚函数是在运行时才确定对象的类型的,**如果构造函数声明为虚函数,那么在构造对象时,由于这个对象还没创建成功,编译器就不知道对象的实际类型**(比如基类还是派生类),就会报错。 26 | 27 | ③另外,从内存的角度来看,虚函数的调用需要虚表指针,而该指针存放在内存空间中,**如果构造函数声明为虚函数,由于对象还没创建,就没有内存空间,因此就没有虚表指针来调用虚函数(构造函数)了**。 28 | 29 | 30 | 31 | ### 析构函数为什么一般是虚函数? 32 | 33 | 为什么这里说的是一般,而不是必须?因为析构函数可以不是虚函数,也可以是虚函数。 34 | 35 | ①C++默认的析构函数不是虚函数。因为虚函数需要虚函数表和虚表指针,会占用额外的内存空间。**对于没有派生类的基类而言,将析构函数定义为虚函数就会浪费内存空间**。 36 | 37 | ②如果存在派生类继承了基类,而基类的析构函数不是声明为虚函数,那么**在析构一个指向派生类的基类指针时,就只会调用基类的析构函数,不会调用派生类的析构函数,因此会造成内存泄漏的问题**。 38 | 39 | 40 | 41 | ### 子类在析构时,要调用父类的析构函数吗? 42 | 43 | **不需要自己显示地调用基类的析构函数,因为编译器会自动调用**,如果再显示地调用,就会调用了两次,可能会出现意外的错误。 44 | 45 | 析构函数在析构时的顺序是,**先析构派生类然后析构基类**,也就是说在析构基类的时候,派生类的全部信息已经销毁了。析构函数调用的顺序与构造函数的顺序刚好相反。 46 | 47 | -------------------------------------------------------------------------------- /C++基础/24_sizeof与strlen的区别.md: -------------------------------------------------------------------------------- 1 | ## 问题 2 | 3 | sizeof 与 strlen 的区别与使用详解。这算是 C++ 中基础中的基础了,很容易被考到。 4 | 5 | ## 区别 6 | 7 | 1. `strlen` 是一个函数,只能以 `char*` (字符串)作为参数,用来计算指定字符串` str` 的长度,但不包括结束字符` '\0'`。所以其参数必须是以` '\0'`作为结束符才可以正确统计其字符长度,否则是个随机数,具体看下面的代码。 8 | 2. `sizeof` 是一个单目运算符,它的参数可以是数组、指针、字符串、对象等等,计算的是参数所对应内存空间的实际字节数。 9 | 3. 在统计字符串 `str` 的长度时,包含结束字符 `'\0'` 10 | 11 | **具体看下面的代码进一步理解:** 12 | 13 | ```c++ 14 | #include 15 | using namespace std; 16 | int main() { 17 | char* s1 = "0123456789"; 18 | cout< 32 | #include 33 | using namespace std; 34 | 35 | int main() { 36 | char src1[10] = "hello"; 37 | strcpy(src1, src1+1); 38 | cout<<"src1:"<dest 串长度,dest 栈空间溢出产生崩溃异常。一般情况下,使用 strncpy 时,建议将n置为dest串长度,复制完毕后,为保险起见,将dest串最后一字符置NULL。 66 | 67 | **安全性**:比较安全,当dest的长度小于n时,会抛出异常。 68 | 69 | 函数实现: 70 | 71 | ```c++ 72 | char *strncpy(char *dest, const char *src, int len) 73 | { 74 | assert(dest!=NULL && src!=NULL); 75 | char *temp; 76 | temp = dest; 77 | for(int i =0;*src!='\0' && i 88 | #include 89 | using namespace std; 90 | 91 | int main() { 92 | char *src5 = "best"; 93 | char dest5[30] = "you are the best one."; 94 | strncpy(dest5+8, src5, strlen(src5)); 95 | cout<<"dest5:"< 0) //循环size次,复制memFrom的值到memTo中 116 | *tempTo++ = *tempFrom++ ; 117 | return memTo; 118 | } 119 | ``` 120 | 121 | **注意**:memcpy没有考虑内存重叠的情况,所以如果两者内存重叠,会出现错误。 122 | 123 | ```c++ 124 | #include 125 | #include 126 | using namespace std; 127 | 128 | int main() { 129 | char *src6 = "best"; 130 | memcpy(src6+1, src6, 3); // 报错,内存重叠 131 | cout<<"src6:"<= strlen(s1)+1;这个1 就是最后的“\0”。 149 | 3. 使用strncpy时,确保dest的最后一个字符是“\0”。 150 | 151 | ## 参考资料 152 | 153 | [strcpy、strncpy和memcpy的用法与区别详解](http://www.jeepshoe.net/art/7393.html) 154 | [strcpy()的注意事项以及strncpy()的用处](https://blog.csdn.net/ccblogger/article/details/78266762) -------------------------------------------------------------------------------- /C++基础/29_哈希冲突和解决方法.md: -------------------------------------------------------------------------------- 1 | ## 问题 2 | 3 | 哈希冲突及其解决方法是面试中的常见问题之一,需要掌握一下。这节内容的概念偏多,为了不造成阅读疲劳,可能会适当地简写一些,如果想深入理解的话,可以看看本文最后的参考博客。 4 | 5 | ## 哈希表 6 | 7 | 在处理哈希冲突之前,首先得清楚什么是哈希表,以及哈希函数如何产生的。 8 | 9 | 哈希表(hash table)是一种通过关键字(key)和值(value)进行访问的数据结构。 10 | 11 | 既然要访问,那肯定需要知道访问的内容存放在哪里,所以这就需要通过一种映射关系将访问元素的关键字和元素的存储地址关联起来,而这种映射关系就是通过哈希函数来实现的。 12 | 13 | ## 哈希函数 14 | 15 | 在元素的关键字 k 和元素的存储位置 p 之间建立一个对应关系 f,使得 $p=f(k)$,这个对应关系 f 就称为哈希函数。 16 | 17 | 按照这种对应关系,在创建哈希表时,将关键字为 k 的元素存放在地址为 $f(k)$ 的单元上;之后若需要查找关键字 k 时,再利用哈希函数计算出该元素的存储位置 $p=f(k)$,就可以直接获取到该元素了。(这应该也是为什么哈希查找时间复杂度为$O(1)$的原因吧) 18 | 19 | 那我们怎么知道这个映射关系 f 是什么呢?那就需要自己构造一个这样的哈希函数了。 20 | 21 | **哈希函数的构造原则:** 22 | 23 | ①函数本身便于计算; 24 | 25 | ②计算出来的地址分布均匀,即对任一关键字 $k$,$f(k)$对应不同地址的概率相等,目的是尽可能减少冲突。 26 | 27 | 接下来介绍5种构造哈希函数的方法:(具体原理不展开讲述了,有需要了解可以看看参考博客,担心面试问这么详细的话可以记住其中一两种方法应该就够了,例如**除留余数法和伪随机数法**,其他的了解即可) 28 | 29 | #### 1.数字分析法 30 | 31 | 如果事先知道关键字集合,并且每个关键字的位数比哈希表的地址码位数多时,可以从关键字中选出分布较均匀的若干位,构成哈希地址。 32 | 33 | #### 2.平方取中法 34 | 35 | 当无法确定关键字中哪几位分布较均匀时,可以先求出关键字的平方值,然后按需要取平方值的中间几位作为哈希地址。 36 | 37 | #### 3.分段叠加法 38 | 39 | 这种方法是按哈希表地址位数将关键字分成位数相等的几部分(最后一部分可以较短),然后将这几部分相加,舍弃最高进位后的结果就是该关键字的哈希地址。 40 | 41 | #### 4.除留余数法 42 | 43 | 假设哈希表长为 $m$,$p$ 为小于等于 $m$的最大素数,则哈希函数为: 44 | $$ 45 | H(k)=k\%p,\ \ 其中\%为取余运算 46 | $$ 47 | ![](https://i.loli.net/2020/05/24/uHRTsWiXhVqn9zG.png) 48 | 49 | #### 5.伪随机数法 50 | 51 | 采用一个伪随机函数做哈希函数,即 $H(key)=random(key)$。 52 | 53 | 54 | 55 | ## 哈希冲突 56 | 57 | ### 哈希冲突产生的原因 58 | 59 | 因为通过哈希函数产生的值是有限的,而数据可能比较多,导致通过哈希函数映射后仍有很多不同的数据对应了相同的哈希值,这时候就产生了哈希冲突。 60 | 61 | 例如,用除留余数法构造一个$H(k)=k\%p$ 的哈希函数,若散列表为 $\{ 18,75,60,43,54,90,46 \}$,$p=7$,那么经过哈希函数计算后的结果为 $\{ 4,5,4,1,5,6,4\}$,可以发现,有些原本不同的数据却对应到了相同的哈希值上了,因此发生了冲突。 62 | 63 | ### 解决哈希冲突的四种方法 64 | 65 | 通过构造性能良好的哈希函数,可以减少冲突,但一般不可能完全避免冲突,因此解决冲突是哈希法的另一个关键问题。创建哈希表和查找哈希表都会遇到冲突,两种情况下解决冲突的方法应该一致。下面以创建哈希表为例,说明解决冲突的方法。常用的解决冲突方法有以下四种: 66 | 67 | #### 1.开放地址法(也称再散列法) 68 | 69 | **基本思想是:**当关键字 key 的哈希地址 $p=H(key)$ 出现冲突时,以 $p$为基础,产生另一个哈希地址 $p1$,如果 $p1$ 仍然冲突,再以 $p$ 为基础,产生另一个哈希地址 $p2$,…,**直到找出一个不冲突的哈希地址 $p_{i}$,将相应元素存入其中**。 70 | 71 | 这种方法有一个通用的再散列函数形式: 72 | $$ 73 | H_{i} = (H(key)+d_{i})\%m,\ \ i=1,2,...,n.\\ 74 | 其中,H(key)为哈希函数,m 为表长,d_{i}为增量序列。 75 | $$ 76 | 增量序列 $d_{i}$ 的取值方式不同,相应的再散列方式也不同,主要有以下三种: 77 | 78 | ##### (1)线性探测 79 | 80 | 增量序列为: $d_{i}=1,2,...,m-1$ 81 | 82 | **其特点是:**在发生冲突时,顺序查看表中下一单元,直到找出一个空单元或查遍全表,将冲突的元素存放进去。 83 | 84 | ##### (2)再平方探测 85 | 86 | 增量序列为: $d_{i}=1^2,-1^2,2^2,-2^2,...,k^2,-k^2.\ \ (k\leq m/2)$ 87 | 88 | **其特点是:**在发生冲突时,在表的左右进行跳跃试探,比较灵活。 89 | 90 | ##### (3)伪随机探测 91 | 92 | 增量序列: $d_{i}=伪随机序列$ 93 | 94 | 具体实现时,建立一个伪随机数发生器生成伪随机序列(如2,5,9,......),在发生冲突时,先令 $d_{i}=2$,算出 $H_{i}$,如果还有冲突,则再令 $d_{i}=5$,算出 $H_{i}$,若还有冲突,则继续下去...... 95 | 96 | 97 | 98 | #### 2.链式地址法 99 | 100 | **基本思想:**将所有哈希地址为 $i$ 的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第 $i$ 个单元中,因而查找、插入和删除都比较方便。 101 | 102 | ![](https://i.loli.net/2020/05/24/4ynGPofVedNvaXl.png) 103 | 104 | #### 3.建立公共溢出区 105 | 106 | **基本思想是:**将哈希表分为**基本表**和**溢出表**两部分,凡是和基本表发生冲突的元素,一律填入溢出表中。 107 | 108 | 109 | 110 | #### 4.再哈希表 111 | 112 | **基本思想是:**对于冲突的哈希值,再构造另一个哈希函数进行处理,直至没有哈希冲突,也就是同时使用多个哈希函数来处理。 113 | 114 | 115 | 116 | ## 参考资料 117 | 118 | [解决哈希(HASH)冲突的主要方法](https://www.cnblogs.com/zhangbing12304/p/7997980.html) 119 | 120 | [哈希表和哈希冲突](https://blog.csdn.net/weixin_42044037/article/details/81838693) -------------------------------------------------------------------------------- /C++基础/31_c++中的左值引用与右值引用.md: -------------------------------------------------------------------------------- 1 | ## 问题 2 | 3 | 之前都没接触过左值引用和右值引用的概念…… 4 | 5 | ## 解答 6 | 7 | 在 C++ 中所有的值必属于左值、右值两者之一。 8 | 9 | **左值**:可以取地址的,有名字的,非临时的 10 | **右值**:不能取地址的,没有名字的,临时的 11 | 12 | 举个栗子:`int a = b + c` ,a 就是左值,其变量名为 a ,通过 `&a` 可以取得该变量的地址;而表达式 `b + c` 和函数返回值 `int fun()` 就是右值,在其被赋值给某一变量前,我们不能通过变量名找到它,`&(b + c)` 这样的操作则不会通过编译。 13 | 14 | 可见**临时值,函数返回的值**等都是右值;而**非匿名对象(包括变量),函数返回的引用,const对象**等都是左值。 15 | 16 | 从本质上理解,创建和销毁由编译器幕后控制,程序员只能确保在本行代码有效的,就是右值(包括立即数);而用户创建的,通过作用域规则可知其生存期的,就是左值(包括函数返回的局部变量的引用以及const对象)。 17 | 18 | ### 左值引用 19 | 20 | 所谓的左值引用就是对左值的引用。先看一下传统的左值引用: 21 | 22 | ```c++ 23 | int a = 10; 24 | int &b = a; // 定义一个左值引用变量 25 | b = 20; // 通过左值引用修改引用内存的值 26 | ``` 27 | 28 | *左值引用在汇编层面其实和普通的指针是一样的;*定义引用变量必须初始化,因为引用其实就是一个别名,需要告诉编译器定义的是谁的引用。 29 | 30 | 下面的这种是无法编译通过的,因为 10 是一个立即数,无法对一个立即数取地址,因为立即数并没有在内存中存储,而是存储在寄存器中。 31 | 32 | ```c++ 33 | int &c = 10; 34 | ``` 35 | 36 | 这个问题可以这么解决: 37 | 38 | ```c++ 39 | const int& c = 10; 40 | ``` 41 | 42 | 使用常引用来引用常量数字 10,因为此刻内存上产生了临时变量保存了10,这个临时变量是可以进行取地址操作的,因此 c 引用的其实是这个临时变量,相当于下面的操作: 43 | 44 | ```c++ 45 | const int temp = 10; 46 | const int &var = temp; 47 | ``` 48 | 49 | **结论:** 50 | 51 | >左值引用要求右边的值必须能够取地址,如果无法取地址,可以用**常引用**。但使用常引用后,我们只能通过引用来读取数据,无法去修改数据,因为其被 const 修饰成常量引用了。 52 | 53 | ### 右值引用 54 | 55 | 右值引用是 C++11 新增的特性,右值引用用来绑定到右值,绑定到右值以后,本来会被销毁的右值的生存期会延长到与绑定到它的右值引用的生存期。(有点绕,多读两遍) 56 | 57 | 定义右值引用的格式如下: 58 | 59 | ```c++ 60 | 类型 && 引用名 = 右值表达式; 61 | int &&c = 10; 62 | ``` 63 | 64 | 在汇编层面右值引用做的事情和常引用是相同的,即产生临时量来存储常量。但是,唯一 一点的区别是,右值引用可以进行读写操作,而常引用只能进行读操作。 65 | 66 | 直接看下面这两段话很难理解,建议好好看下参考资料中的代码,写得非常好。 67 | 68 | 右值引用的存在并不是为了取代左值引用,而是充分利用右值(特别是临时对象)的构造来减少对象构造和析构操作以达到提高效率的目的。 69 | 70 | 带右值引用参数的拷贝构造和赋值重载函数,又叫**移动构造函数**和**移动赋值函数**,这里的移动指的是把临时量的资源移动给了当前对象,临时对象就不持有资源,为nullptr了,实际上没有进行任何的数据移动,没发生任何的内存开辟和数据拷贝。 71 | 72 | **注意:** 73 | 74 | 右值引用通常不能绑定到任何的左值,要想绑定一个左值到右值引用,通常需要使用 `std::move()` 函数将左值强制转换为右值,如: 75 | 76 | ```c++ 77 | int val = 10; 78 | int &&rrval = std::move(val); 79 | ``` 80 | 81 | 但是这里需要注意:在调用完`std::move()`之后,不能再使用val,只能使用 rrval,这一点用于基本类型可能没什么直接影响,当应用到类函数的时候,用好`std::move()` 可以减少构造函数数的次数 82 | 83 | ## 参考资料 84 | 85 | [c++ 左值引用与右值引用](https://zhuanlan.zhihu.com/p/97128024) -------------------------------------------------------------------------------- /C++基础/32_C++中的内存管理是怎样的.md: -------------------------------------------------------------------------------- 1 | ## at问题 2 | 3 | C++中的内存管理是怎样的? 4 | 5 | 内存管理属于C++底层的知识,基本上是自己的知识盲区。看了几篇博客的总结,对内存管理和内存分配的分类似乎没有一个比较统一的说法,不过有些类似。这里主要参考两篇博客列举两种内存管理总结方式及其对应的内存分类方式,具体怎么回答就见仁见智啦,个人比较推荐第二种解读方式,感觉相对容易理解和记忆。 6 | 7 | 8 | 9 | **方式一:** 参考自牛客网的 [C++工程师面经汇总](https://www.nowcoder.com/tutorial/93/8f140fa03c084299a77459dc4be31c95) 10 | 11 | ### 内存管理 12 | 13 | 在C++中,虚拟内存分为代码段、数据段、BSS段、堆区、文件映射区以及栈区六部分。 14 | 15 | **代码段**:包括只读存储区和文本区,其中**只读存储区存储字符串常量**,**文本区存储程序的机器代码**。 16 | 17 | **数据段**:存储程序中**已初始化的全局变量和静态变量**。 18 | 19 | **BSS 段**:存储**未初始化的全局变量和静态变量(**局部+全局),以及**所有被初始化为0的全局变量和静态变量**。 20 | 21 | > 补充:BSS段的BSS是英文 Block Started by Symbol 的简写 22 | 23 | **堆区**:调用new/malloc函数时在堆区动态分配内存,同时需要调用delete/free来手动释放申请的内存。 24 | 25 | **栈区**:使用栈空间存储**函数的返回地址、参数、局部变量、返回值**。 26 | 27 | **映射区**:存储**动态链接库以及调用mmap函数进行的文件映射**。 28 | 29 | ### 内存分配 30 | 31 | 32bitCPU可寻址4G线性空间,每个进程都有各自独立的4G逻辑地址,其中0~3G是用户态空间,3~4G是内核空间,不同进程相同的逻辑地址会映射到不同的物理地址中。其逻辑地址其划分如下: 32 | 33 | ![](https://i.loli.net/2020/05/25/ulZtD1biVyzCcdK.png) 34 | 35 | 各段的说明如下: 36 | 37 | | 区域 | 分段 | 内容 | 38 | | :--: | :--------------------------------: | ------------------------------------------------------------ | 39 | | 静态 | **代码段(text segment)** | 包括只读存储区和文本区,其中只读存储区存储字符串常量,文本区存储程序的机器代码 | 40 | | 静态 | **数据段(data segment)** | 存储程序中已初始化的全局变量和静态变量 | 41 | | 静态 | **bss segment** | 存储未初始化的全局变量和静态变量(局部+全局),以及所有被初始化为0的全局变量和静态变量,对于未初始化的全局变量和静态变量,程序运行main之前时会统一清零,即未初始化的全局变量编译器会初始化为0 | 42 | | | | | 43 | | 动态 | **堆(heap)** | 使用栈空间存储函数的返回地址、参数、局部变量、返回值,从高地址向低地址增长。在创建进程时会有一个最大栈大小,Linux可以通过ulimit命令指定 | 44 | | 动态 | **栈 (stack)** | 使用栈空间存储函数的返回地址、参数、局部变量、返回值,从高地址向低地址增长。在创建进程时会有一个最大栈大小,Linux可以通过ulimit命令指定 | 45 | | 动态 | **映射区(memory mapping segment)** | 存储动态链接库等文件映射、申请大内存(malloc时调用mmap函数) | 46 | 47 | 48 | 49 | **方式二:**参考自这篇博客,原文链接:https://blog.csdn.net/cherrydreamsover/article/details/81627855 50 | 51 | ### 内存管理 52 | 53 | **栈区(stack):**由编译器自动分配与释放,存放为运行时函数分配的局部变量、函数参数、返回值、返回地址等。 54 | 55 | **堆区(heap):**一般由程序员自动分配(使用new分配),如果程序员没有释放(使用delete释放),程序结束时系统自动回收。 56 | 57 | **全局区(静态区):**存放全局变量、静态数据、常量。程序结束后由系统释放。全局区分为已初始化全局区(data)和未初始化全局区(bss)。 58 | 59 | **文字常量区:**存放常量字符串,程序结束后由系统释放。 60 | 61 | **代码区:**存放函数体(类成员函数和全局区)的二进制代码。 62 | 63 | ### 内存分配 64 | 65 | 先看下面这张图: 66 | 67 | ![](https://i.loli.net/2020/05/25/HhwFfOpdaT5Z8sy.png) 68 | 69 | 具体请看: 70 | 71 | | 分配方式 | 内容 | 72 | | :----------------------------------: | :----------------------------------------------------------- | 73 | | 从静态存储区分配 | 内存在程序编译的时候已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static变量。 | 74 | | | | 75 | | 在栈上创建 | ●在执行函数时,函数内局部变量的存储单元可以在栈上创建,函数执行结束时,这些内存单元会自动被释放。
●栈内存分配运算内置于处理器的指令集中,效率高,但是分配的内存容量有限。 | 76 | | | | 77 | | 从堆上分配(也称为**动态内存分配**) | ● 程序在运行的时候使用malloc或者new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。
● 动态内存的生命周期由程序员决定,使用非常灵活,但如果在堆上分配了空间,既有责任回收它,否则运行的程序会出现内存泄漏,频繁的分配和释放不同大小的堆空间将会产生内存碎片。 | 78 | 79 | 80 | 81 | ## 拓展 82 | 83 | 上面第二位博主总结的太好了,顺便摘抄其剩下的总结。 84 | 85 | ### 堆和栈的区别 86 | 87 | ● **管理方式不同:**栈是由编译器自动申请和释放空间,堆是需要程序员手动申请和释放; 88 | 89 | ● **空间大小不同:**栈的空间是有限的,在32位平台下,VC6下默认为1M,堆最大可以到4G; 90 | 91 | ● **能否产生碎片:**栈和数据结构中的栈原理相同,在弹出一个元素之前,上一个已经弹出了,不会产生碎片,如果不停地调用malloc、free对造成内存碎片很多; 92 | 93 | ● **生长方向不同:**堆生长方向是向上的,也就是向着内存地址增加的方向,栈刚好相反,向着内存减小的方向生长。 94 | 95 | ● **分配方式不同:**堆都是动态分配的,没有静态分配的堆。栈有静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由 malloc 函数进行分配,但是栈的动态分配和堆是不同的,它的动态分配是由编译器进行释放,无需我们手工实现。 96 | 97 | ● **分配效率不同:**栈的效率比堆高很多。栈是机器系统提供的数据结构,计算机在底层提供栈的支持,分配专门的寄存器来存放栈的地址,压栈出栈都有相应的指令,因此比较快。堆是由库函数提供的,机制很复杂,库函数会按照一定的算法进行搜索内存,因此比较慢。 98 | 99 | 100 | 101 | ### 静态全局变量与全局变量的区别 102 | 103 | ● 静态全局变量和全局变量都属于 **常量区**。 104 | 105 | ● 静态全局区只在本文件中有效,别的文件想调用该变量是调不了的,而全局变量可以在别的文件中调用。 106 | 107 | ● 如果别的文件中定义了一个该全局变量相同的变量名,是会出错的。 108 | 109 | 110 | 111 | ### 静态局部变量和局部变量的区别 112 | 113 | ● 静态局部变量是属于常量区的,而函数内部的局部变量属于栈区。 114 | 115 | ● 静态局部变量在该函数调用结束时,不会销毁,而是随整个程序结束而结束,但是别的函数调用不了该变量,局部变量随该函数的结束而结束。 116 | 117 | ● 如果定义这两个变量的时候没有初始值时,静态局部变量会自动定义为0,而局部变量就是一个随机值。 118 | 119 | ● 静态局部变量在编译期间只赋值一次,以后每次函数调用时,不再赋值,调用上次的函数调用结束时的值。局部变量在调用期间,每调用一次,赋一次值。 120 | 121 | 122 | 123 | ### 补充 124 | 125 | 对于内存管理,有一个很重要的概念就是动态内存管理,通过malloc/new、free/delete来实现,具体可以参考之前总结的C++基础第 11、12个问题的内容,也可以深入阅读参考资料中最后一篇文章。 126 | 127 | 128 | 129 | ## 参考资料 130 | 131 | [请你说一说C++的内存管理是怎样的?](https://blog.csdn.net/N1314N/article/details/93192139?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.nonecase&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.nonecase) 132 | 133 | [C/C++程序内存的分配]( https://blog.csdn.net/cherrydreamsover/article/details/81627855?utm_medium=distribute.pc_relevant.none-task-blog-baidujs-3) 134 | 135 | [C语言中内存分配](https://www.cnblogs.com/wanghuaijun/p/6509016.html) 136 | 137 | [C/C++动态内存管理malloc/new、free/delete的异同](C/C++动态内存管理malloc/new、free/delete的异同) 138 | 139 | -------------------------------------------------------------------------------- /C++基础/35_内存泄漏是什么.md: -------------------------------------------------------------------------------- 1 | ## 问题 2 | 3 | 什么是内存泄漏?有哪几种情况?如何判断是否存在内存泄漏? 4 | 5 | ## 什么是内存泄漏? 6 | 7 | 内存泄漏(memory leak),是指由于疏忽或错误造成了程序未能释放掉不再使用的内存的情况。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费或者性能不良的情况。 8 | 9 | ## 内存泄漏有哪几种情况? 10 | 11 | #### 1.堆内存泄漏 (Heap leak) 12 | 13 | 堆内存指的是程序运行中根据需要分配通过malloc,realloc,new等从堆中分配的一块内存,在完成相关操作后必须通过调用对应的 free或者delete 删掉。如果程序的设计的错误导致这部分内存没有被释放,那么此后这块内存将不会被使用,就会产生Heap Leak。 14 | 15 | #### 2.系统资源泄露(Resource Leak)。 16 | 17 | 主要是指系统分配给程序的资源没有使用相应的函数释放掉(比如 Bitmap,handle,socket等),导致系统资源的浪费,严重可导致系统效能降低,系统运行不稳定。 18 | 19 | #### 3.没有将基类的析构函数定义为虚函数 20 | 21 | 当基类指针指向派生类对象时,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确是释放,因此造成内存泄露。(这一点在之前总结的C++问题 ***16_析构函数*** 中关于**析构函数为什么一般定义成虚函数**中也有说明。) 22 | 23 | #### 4.在释放对象数组时没有使用delete[]而是使用了delete 24 | 25 | 在之前总结的C++问题 ***12_delete和delete[]的区别*** 中有过举例说明,也就是说,当一个数组中的多个元素均为对象时,在使用delete释放该数组时必须加上方括号([]),否则就只会调用一次析构函数释放数组的第一个对象,而剩下的数组元素没有被析构掉,造成了内存泄漏。 26 | 27 | #### 5.缺少拷贝构造函数 28 | 29 | 在C++问题 ***46_深拷贝与浅拷贝的区别*** 中有提到过,如果类中没有手动编写拷贝构造函数,用该类对象进行拷贝赋值时,会使用默认的拷贝构造函数,即浅拷贝,浅拷贝的缺陷之一被赋值对象原本的内存没被释放,因此造成了内存泄漏。 30 | 31 | ```C++ 32 | //例如,假设有一个String类 33 | String a("hello"); 34 | String b("world"); 35 | b = a; //b的指针会指向a所在的内存,但b原本的内存没有被释放,造成了内存泄漏 36 | ``` 37 | 38 | 39 | 40 | ## 如何判断内存泄漏? 41 | 42 | 1.**在 Linux 环境下可以使用内存泄漏检查工具 valgrind。** 43 | 44 | 2.在编写代码时可以**手动添加内存申请和释放的统计功能,根据当前申请和释放的内存是否一致来判断**是否发生内存泄漏。 45 | 46 | 47 | 48 | ## 如何解决内存泄漏的问题? 49 | 50 | 1.可以按照上述判断内存泄漏的几种方法来防止内存泄漏 51 | 52 | 2.**使用智能指针**。智能指针可以自行管理指针,因为智能指针就是一个类,在类的作用域结束时会自动调用析构函数来释放内存空间,可以减少内存泄漏的问题(注意!!是减少,而不是完全解决)。 53 | 54 | 55 | 56 | ## 智能指针有内存泄漏的问题吗? 57 | 58 | 前面说了,智能指针可以减少内存泄漏的问题,但不能完全解决,也就是说**智能指针也是存在内存泄漏的问题的**。那智能指针什么时候会发生内存泄漏呢? 59 | 60 | **当两个对象同时使用一个`shared_ptr`成员变量指向对方时,会造成循环引用,使引用计数失效,从而导致内存泄漏。** 61 | 62 | > 补充:如何理解上面这句话?(个人理解,作为参考) 63 | > 64 | > 使用共享指针`shared_ptr`可以使得多个指针同时指向同一个对象,同时对指向该对象的指针进行引用计数,并且该对象会在最后一个引用被销毁时释放,也就是说当引用计数为0时,该对象即相关内存就会被自动释放。因此,问题来了,当两个对象同时使用`shared_ptr`指针指向对方时,彼此会相互引用,引用计数不会为0,因此最后这两个对象的资源不会被释放,造成了内存泄漏。 65 | 66 | #### 如何解决上述智能指针的内存泄漏问题? 67 | 68 | **使用`weak_ptr`弱指针。**`shared_ptr` 指针指向一个对象时,并不会修改该对象上的引用计数,但可以访问该对象及获取该对象上的引用计数(这也是为什么它叫做弱指针的原因)。因此,可以使用`shared_ptr` 指针来避免一些非法访问。 69 | 70 | > 关于智能指针的内容,之后再做详细的整理。 71 | 72 | 73 | 74 | ## 参考资料 75 | 76 | [牛客网-C++工程师面试宝典](https://www.nowcoder.com/tutorial/93/8f140fa03c084299a77459dc4be31c95) 77 | 78 | [C++中内存泄漏的几种情况](https://www.cnblogs.com/zzdbullet/p/10478744.html) 79 | 80 | [C++智能指针的内存泄漏和解决方法](https://blog.csdn.net/qq_40261882/article/details/100751480) -------------------------------------------------------------------------------- /C++基础/36_为什么要内存对齐.md: -------------------------------------------------------------------------------- 1 | ## 问题 2 | 3 | 为什么要进行内存对齐?如何对齐?这个问题其实困扰我很久了,之前做过一些笔试题,经常在sizeof()的问题上出错,但一直没有充分地去理解过,所以这次想好好梳理一下。 4 | 5 | 6 | 7 | ## 什么是内存对齐? 8 | 9 | 所谓的内存对齐,就是为了让内存存取更加有效率而采取的一种优化手段,对齐的结果是**使得内存中数据的首地址是CPU单次获取数据大小的整数倍**。 10 | 11 | 比如,CPU单次获取数据的大小是4个字节,对于 int x 而言,如果 x 的地址是0x00000000、0x00000004...等4的倍数,就是内存对齐。 12 | 13 | 此外,这里说的内存对齐,一般就是针对结构体来进行探讨的,所以这就可以理解在本文后面提到的对整体和成员有不同的对齐方式了。 14 | 15 | 16 | 17 | ## 为什么要内存对齐? 18 | 19 | #### 1.硬件因素 20 | 21 | **经过内存对齐之后,CPU对内存访问的效率会大大提高。** 22 | 23 | 举个例子: 24 | 25 | > ● 对于int变量 x 占用4个字节的内存大小,假设它存放在 $ 0x00000003\sim0x00000006$ 的位置上,此时 $0x00000003$ 不是4的整数倍。因此,对于每次只取4个字节的CPU而言,对 x 的读取就必须分两次进行,第一次读取 $ 0x00000000\sim0x00000003$,第二次读取 $ 0x00000004\sim0x00000007$,然后再进行拼接处理,才能得到我们想要的数据,可见这样的效率会很低下。 26 | > 27 | > ● 倘若经过对齐,即数据的首地址是CPU单次获取数据大小的整数倍,假设 x 存放在 $ 0x00000004\sim0x00000007$ 的位置,那么CPU只需要访问一次内存就可以读取出 x 的值了。 28 | 29 | #### 2.可移植性 30 | 31 | 不是所有的硬件平台都能访问任意地址上的任意数据的,例如有些平台上CPU在内存非对齐的情况下执行二进制代码会崩溃。为了代码的可移植性,进行内存对齐是很有必要的。 32 | 33 | 34 | 35 | ## 如何进行内存对齐? 36 | 37 | ### 对齐方式 38 | 39 | **方式一:** 编译器提供了一种手动指定对齐值的方式,**只要在代码前添加关键字 `#pragma pack(n)` 即可**,其中 `n` 是手动指定的内存对齐的字节数。比如 `#pragma pack(4)`表示以4个字节进行对齐。 40 | 41 | **方式二:** 倘若没有手动设置对齐值,**或者手动设置的对齐值 `n` 大于成员变量中最大的类型的字节数**(注意这一点!),编译器则会**默认将成员变量中最大的类型的字节数设置为对齐值(假设为 m)**。 42 | 43 | ### 对齐规则 44 | 45 | **● 成员对齐**: ① 第一个成员的首地址为0 46 | 47 | ​ ② 假设某成员的类型所占字节数为 `k`,则该成员的首地址为 **`min(n,k)` 的整数倍**。 48 | 49 | **● 整体对齐**: 结构体总的大小,应该为 **`min(n,m)` 的整数倍**,如果不够就在后面填补占位。 50 | 51 | > 补充:如果不能理解上面说的 `min(n,k)`和 `min(n,m)`,可以看下面的解释: 52 | > 53 | > ● 对于`min(n,k)` 的理解:若手动设置了对齐值 `n`,且 `n<=k`,那么首地址就是 `n` 的倍数,也就是上面的对齐方式一;若 `k=k`,则该值必然也是 `k` 的整数倍,因此 `min(n,k)` 就可以理解啦。 54 | > 55 | > ● 对于`min(n,m)` 的理解:根据对齐方式一和二,其实系统的对齐值就是`n`和`m`中最小的那个。当然,整体对齐的意思是整个结构体的总大小要对齐,不够就填补占位。比如,假设对齐值为8,结构体各个成员对齐之后的大小为12,由于12不是8的整数倍,所以编译器会继续填补4个空位,最终结构体的总大小为16。 56 | 57 | 58 | 59 | ## 代码解释 60 | 61 | 对于方式一,手动设置对齐值 `#pragma pack(n)`,且`n`不大于成员变量的最大类型,此时编译器的对齐值就是 `n`。 62 | 63 | ```C++ 64 | #include 65 | #pragma pack(4) //对齐值为4 66 | using namespace std; 67 | struct MyStruct 68 | { 69 | char c; 70 | double b; 71 | int a; 72 | }; 73 | 74 | int main() { 75 | MyStruct data; 76 | cout << sizeof(data.a) << endl; //结果为4 77 | cout << sizeof(data.b) << endl; //结果为8 78 | cout << sizeof(data.c) << endl; //结果为1,自动填充3个字节 79 | cout << sizeof(data) << endl; //结果为16,如果对齐值设置为8,这里结果就是24 80 | //system("pause"); 81 | return 0; 82 | } 83 | 84 | ``` 85 | 86 | 对于方式二,先看不进行手动设置对齐值的情况,编译器默认将成员中最大类型的字节数作为对齐值,即double的类型大小,为 8,具体看代码: 87 | 88 | ```C++ 89 | #include 90 | using namespace std; 91 | struct MyStruct 92 | { 93 | char c; 94 | double b; 95 | int a; 96 | }; 97 | 98 | int main() { 99 | MyStruct data; //没有手动设置对齐值,编译器默认为最大类型字节数,即8 100 | cout << sizeof(data.a) << endl; //结果为4,自动填充4个字节 101 | cout << sizeof(data.b) << endl; //结果为8 102 | cout << sizeof(data.c) << endl; //结果为1,自动填充7个字节 103 | cout << sizeof(data) << endl; //结果为24 104 | //system("pause"); 105 | return 0; 106 | } 107 | ``` 108 | 109 | 对于方式二,手动设置对齐值`n`,且n大于成员变量中的最大类型的字节数`m`,则编译器采用`m`作为对齐值。 110 | 111 | ```C++ 112 | #include 113 | #pragma pack(16) //设置对齐值为16,实际对齐值为sizeof(double)=8 114 | using namespace std; 115 | struct MyStruct 116 | { 117 | int a; 118 | double b; 119 | char c; 120 | }; 121 | 122 | int main() { 123 | MyStruct data; 124 | cout << sizeof(data.a) << endl; //结果为4,自动填充4个字节 125 | cout << sizeof(data.b) << endl; //结果为8 126 | cout << sizeof(data.c) << endl; //结果为1,自动填充7个字节 127 | cout << sizeof(data) << endl; //结果为24 128 | //system("pause"); 129 | return 0; 130 | } 131 | ``` 132 | 133 | 134 | 135 | ## 总结 136 | 137 | 用两句话来总结一下内存的对齐方式: 138 | 139 | ① 若没有手动设置对齐值,则编译器默认使用成员变量中最大的类型的字节数作为对齐值; 140 | 141 | ② 若手动设置了对齐值,则编译器会在默认对齐值和手动设置的对齐值之间选择最小的那个作为最终对齐值。 142 | 143 | 144 | 145 | ## 参考资料 146 | 147 | [【C/C++】内存对齐 到底怎么回事?](https://zhuanlan.zhihu.com/p/101140160) -------------------------------------------------------------------------------- /C++基础/37_全局变量和局部变量的区别.md: -------------------------------------------------------------------------------- 1 | ## 问题 2 | 3 | C++中的全局变量和局部变量有什么区别? 4 | 5 | > 注:内容全部参考自文末的参考资料 6 | 7 | ## 全局变量和局部变量的区别 8 | 9 | 可以从以下4个角度来区分: 10 | 11 | | 区别 | 全局变量 | 局部变量 | 12 | | :------: | :----------------------------------------------------------: | :----------------------------------------------------------: | 13 | | 作用域 | 全局作用域 | 局部作用域 | 14 | | 内存分配 | 全局变量在静态数据区 | 静态局部变量在静态数据区,非静态局部变量在栈区 | 15 | | 生命周期 | 存在于整个程序运行期间 | 静态局部变量存在于整个程序运行期间,非静态局部变量存在于局部函数内部或局部循环体内部 | 16 | | 对外链接 | ①全局变量可以在当前文件的任何地方使用
②**非静态全局变量**可以在其他文件上使用,**静态全局变量**不能在其他文件上使用 | 局部变量不能在其他文件上使用,只可以在局部的函数中使用 | 17 | 18 | 19 | 20 | ## 全局变量与局部变量区别汇总 21 | 22 | 这部分内容在C++问题 ***32_C++内存管理是怎样的*** 中也有过总结,这里再汇总一下。 23 | 24 | | 区别 | 静态全局变量 | 非静态全局变量 | 静态局部变量 | 非静态局部变量 | 25 | | :------: | :----------: | :------------: | :----------: | :------------: | 26 | | 作用域 | 全局 | 全局 | 全局 | 局部 | 27 | | 内存分配 | 静态数据区 | 静态数据区 | 静态数据区 | 栈区 | 28 | | 生命周期 | 整个程序 | 整个程序 | 整个程序 | 局部函数体 | 29 | | 对外链接 | 不允许 | 允许 | 不允许 | 不允许 | 30 | 31 | 32 | 33 | ## 补充:static对变量的影响 34 | 35 | ● 使得静态局部变量内存分配在静态数据区,导致延长了生命周期 36 | 37 | ● 使得静态全局变量无法被其他文件使用,局限了其文件对外链接 38 | 39 | 40 | 41 | ## 参考资料 42 | 43 | [[C++] 全局变量与局部变量的区别](https://blog.csdn.net/weixin_44922845/article/details/104580514) https://blog.csdn.net/weixin_44922845/article/details/104580514 -------------------------------------------------------------------------------- /C++基础/46_深拷贝与浅拷贝的区别.md: -------------------------------------------------------------------------------- 1 | ## 问题 2 | 3 | 深拷贝和浅拷贝的区别是面试中的常见问题之一,对于不同的编程语言,这个问题的回答可能稍有差别,下面我们就来探索一下它们之间的异同吧。 4 | 5 | 先来看看在JavaScript对象的深拷贝与浅拷贝的区别: 6 | 7 | **浅拷贝:**只是复制了指向某个对象的指针,而不是复制对象本身,因此浅拷贝后新旧对象还是**共用同一块内存**,旧 对象改变**会修改新对象的值**。 8 | 9 | **深拷贝:**会另外构造一个与旧对象一模一样的对象,新旧对象**不共享内存**,**旧对象改变不会修改新对象的值**。 10 | 11 | ![](https://i.loli.net/2020/05/22/QrLdBNKF2mtJDVT.jpg) 12 | 13 | 理解了上面的内容,可以说已经理解了深拷贝和浅拷贝的本质区别了。但是,如果面试官是问你在C++中深拷贝和浅拷贝是和拷贝的区别,上面的回答可能还不能让面试官满意哦。在C++中,要说出深拷贝和浅拷贝的区别,想必是需要提到拷贝构造函数这个概念才能让面试官满意的。既然如此,那我们就不妨来看看C++中怎么说的吧。 14 | 15 | ## 浅拷贝 16 | 17 | 浅拷贝,也称为位拷贝。C++中的浅拷贝是通过拷贝构造函数来实现的,如果程序员不主动编写拷贝构造函数和赋值函数,编译器将以浅拷贝的方式自动生成缺省的函数,也就是在拷贝时简单地复制某个对象的指针,这样很容易造成一些问题。 18 | 19 | 例如,假设String类有两个对象a和b,a.data的内容为“hello”,b.data为“world”,当将a的值赋给b时,可能会出现以下3个问题: 20 | 21 | ① b.data的内存没释放,**造成内存泄漏** 22 | 23 | ② b.data和a.data指向了同一块内存,a或b**任何一方的值改变都会修改另一方的值** 24 | 25 | ③ **在对象被析构时,data被释放了两次** 26 | 27 | 看看下面的代码来理解一下: 28 | 29 | ```C++ 30 | //假设有一个String类 31 | String a("hello"); 32 | String b("world"); 33 | //调用了拷贝构造函数,不过这里最好写成 string c(a),a原本的内存没有释放,且a或c改变都会影响另一方 34 | String c = a; 35 | c=b; //调用了赋值函数 36 | ``` 37 | 38 | 39 | 40 | ## 深拷贝 41 | 42 | 深拷贝**必须显示地提供拷贝构造函数和赋值运算符,而且新旧对象不共享内存**,也就是说,在编写拷贝构造函数时会开辟一个新的内存空间。那什么时候会使用深拷贝? 43 | 44 | ①一个对象以值传递的方式传入函数体 45 | 46 | ②一个对象以值传递的方式从函数体返回 47 | 48 | ③一个对象需要通过另外一个对象进行初始化 49 | 50 | 51 | 52 | ## 总结 53 | 54 | C++中,浅拷贝不需要自己实现,编译器会自动生成缺省的拷贝构造函数,浅拷贝新旧对象共享一块内存,任何一方的值改变都会影响另一方;深拷贝需要自己手动编写拷贝构造函数,深拷贝新旧对象不共享内存。 55 | 56 | 57 | 58 | ## 参考资料 59 | 60 | [浅拷贝与深拷贝的区别](https://segmentfault.com/a/1190000018874254) 61 | 62 | [C++细节 深拷贝和浅拷贝(位拷贝)详解](https://blog.csdn.net/weixin_41143631/article/details/81486817?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.nonecase&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.nonecase) -------------------------------------------------------------------------------- /C++基础/50_引用作为函数参数和返回值的好处.md: -------------------------------------------------------------------------------- 1 | ## 问题 2 | 3 | 在函数定义的时候,经常看到使用引用作为函数的参数或者返回值,这样做的好处在哪里? 4 | 5 | 引用就是某一变量(目标)的别名,对引用的操作与对变量直接操作完全一样。 6 | 7 | ## 引用作为函数参数 8 | 9 | 作为函数参数时引用有种原因: 10 | 11 | 1. 在函数内部会对此参数进行修改 12 | 2. 提高函数调用和运行效率 13 | 14 | **关于第一点:**我们都知道C++里提到函数就会提到形参和实参。函数的参数实质就是形参,不过这个形参的作用域只是在函数体内部,也就是说实参和形参是两个不同的东西,要想形参代替实参,肯定有一个值的传递。函数调用时,值的传递机制是通过“形参=实参”来对形参赋值达到传值目的,产生了一个实参的副本。即使函数内部有对参数的修改,也只是针对形参,也就是那个副本,实参不会有任何更改。函数一旦结束,形参生命也宣告终结,做出的修改一样没对任何变量产生影响。而如果我们将形参定义成引用,那么此时的赋值后形参只是实参的一个别名而已,此时函数体内对形参的任何修改都同样作用与实参。 15 | 16 | **关于第二点:**可以结合第一点分析,形参是实参的引用,不用经过值的传递机制,已经有了实参值的信息。所以没有了传值和生成副本的时间和空间消耗。当程序对效率要求比较高时,这是非常必要的. 17 | 18 | ## 引用作为函数返回值 19 | 20 | 1. 以引用返回函数值,定义函数时需要在函数名前加 `&` 21 | 2. 用引用返回一个函数值的最大好处是:在内存中不产生被返回值的副本 22 | 23 | ```c++ 24 | #include 25 | using namespace std; 26 | 27 | float temp; //定义全局变量temp 28 | float fn1(float r); //声明函数fn1 29 | float &fn2(float r); //声明函数fn2 30 | 31 | float fn1(float r) //定义函数fn1,它以返回值的方法返回函数值 32 | { 33 | temp=(float)(r*r*3.14); 34 | return temp; 35 | } 36 | float &fn2(float r) //定义函数fn2,它以引用方式返回函数值 37 | { 38 | temp=(float)(r*r*3.14); 39 | return temp; 40 | } 41 | 42 | int main() 43 | { 44 | float a=fn1(10.0); //第1种情况,系统生成要返回值的副本(即临时变量) 45 | //float &b=fn1(10.0); //第2种情况,编译不通过,左值引用不能绑定到临时值 46 | float &d=fn2(10.0); //第3种情况,系统不生成返回值的副本,可以从被调函数中返回一个全局变量的引用 47 | float c=fn2(10.0); //第4种情况,变量c前面不用加&号,这种也是可以的 48 | cout<>,这两个操作符常常希望被连续使用,例如:cout << "hello" << endl; 因此这两个操作符的返回值应该是一个仍然支持这两个操作符的流引用。 58 | 59 | ## 参考资料 60 | 61 | [“引用作为函数参数”与 “引用作为函数返回值”](https://blog.csdn.net/caomin1hao/article/details/82227317) -------------------------------------------------------------------------------- /C++基础/52_虚函数可以声明为inline吗.md: -------------------------------------------------------------------------------- 1 | ## 问题 2 | 3 | 虚函数可以声明为inline吗? 4 | 5 | ## 内联函数(inline) 6 | 7 | 内联函数的作用是**提高函数执行的效率**,在程序中的每个调用点将函数体展开,而不是按照通常的函数调用机制取调用,从而减少调用函数花费的额外开销。内联函数有一下一些特点: 8 | 9 | ● 定义在class内的成员函数默认是inline函数(虚函数除外) 10 | 11 | ● 通常只有函数非常短小的时候(如10行代码内)才适合定义成inline函数,否则会导致程序变慢 12 | 13 | ● 头文件中不仅要包含inline函数的声明,还要包含其定义,方便编译器查找。 14 | 15 | ● (缺点)inline函数会增加执行文件的大小。 16 | 17 | 18 | 19 | ## 虚函数可以声明为inline吗? 20 | 21 | 可以,但是**只在编译器知道调用的对象是哪个类型的时候才可以**。 22 | 23 | 虚函数一般不能声明为inline的,因为inline函数是在编译期将函数内容替换到函数调用处的,是静态编译的。而**当基类指针或引用来调用虚函数时,不能声明为inline**,因为虚函数是在运行时动态调用的,编译器并不知道它绑定的是哪个对象。 24 | 25 | 借助下面的代码来理解上面的两种情况: 26 | 27 | ```C++ 28 | class Base { 29 | public: 30 | inline virtual void who() { cout << "I am Base\n"; } 31 | }; 32 | class Derived :public Base { 33 | public: 34 | //从语法上讲,这里可以写成inline,只是当基类指针调用派生类时,不能内联,编译器会自动忽略掉inline 35 | inline void who() { cout << "I am Derived\n"; } 36 | }; 37 | int main() { 38 | Base b; 39 | b.who(); //这里的who()是通过基类对象直接调用的,在编译期间就确定了,因此它可以是内联的 40 | 41 | Base *p = new Derived(); 42 | p->who(); //通过基类的指针调用,在运行时才能确定,所以不能内联 43 | return 0; 44 | } 45 | ``` 46 | 47 | 48 | 49 | ## 参考资料 50 | 51 | [C++内联函数能否是虚函数?](https://zhuanlan.zhihu.com/p/37436574) 52 | 53 | [C++中虚函数不能是inline函数的原因](https://blog.csdn.net/flydreamforever/article/details/61429140) 54 | 55 | [C++虚函数(10) - 虚函数能否为inline?](https://blog.csdn.net/shltsh/article/details/45999947?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.nonecase&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.nonecase) -------------------------------------------------------------------------------- /C++基础/54_常量指针和指针常量的区别.md: -------------------------------------------------------------------------------- 1 | ## 问题 2 | 3 | 常量指针和指针常量有什么区别? 4 | 5 | ## const的优点 6 | 7 | 在C++中,关键字const用来只读一个变量或对象,它有以下几个优点: 8 | 9 | 1. **便于类型检查**,如函数的函数 func(const int a) 中a的值不允许变,这样**便于保护实参**。 10 | 2. 功能**类似于宏定义,方便参数的修改和调整**。如 const int max = 100; 11 | 3. 节省空间,如果再定义a = max,b=max...等就不用在为max分配空间了,而用宏定义的话就一直进行宏替换并为变量分配空间 12 | 4. 为函数重载提供参考,即可以添加const进行重载。 13 | 14 | 15 | 16 | ## 常量指针和指针常量的区别 17 | 18 | 首先要区分常量指针和指针常量分别是什么,这里有一种很好的记忆方法: 19 | 20 | **指针(\*)和常量(const)谁在前先读谁 ;* 象征着地址,const象征着内容;谁在前面谁就不允许改变。** 21 | 22 | ```C++ 23 | int a = 1; 24 | int b = 2; 25 | int c = 3; 26 | int const *p1 = &b; // const在前,p1为常量指针 27 | int *const p2 = &c; // * 在前,p2为指针常量 28 | //注意:允许将非const对象的地址赋给指向const对象的指针,所以第4行代码是正确的 29 | ``` 30 | 31 | **常量指针p1:即指向const对象的指针,指向的地址可以改变,但其指向的内容(即对象的值)不可以改变。** 32 | 33 | ```c++ 34 | //p1可以改变,但不能通过p1修改其指向的对象(即 b)的值;不过,通过其他方式修改b的值是允许的 35 | p1 = &a; //正确,p1是常量指针,可以指向新的地址(即&a),即p1本身可以改变 36 | *p1 = a; //错误,*p1是指针p1指向对象的值,不可以改变,因此不能对*p重新赋值 37 | ``` 38 | 39 | **指针常量p2:指针本身是常量,即指向的地址本身不可以改变,但内容(即对象的值)可以改变。** 40 | 41 | ```C++ 42 | p2 = &a; //错误,p2是指针常量,本身不可以改变,因此将a的地址赋给p2是错误的 43 | *p2 = a; //正确,p2指向的对象允许改变 44 | ``` 45 | 46 | 47 | 48 | **补充:**要分辨是常量指针还是指针常量,可以从右向左来看其定义,具体如下: 49 | 50 | ①对于 int const *p1=&b,先将\*和p1结合,即p1首先是一个指针,然后再左结合const,即常量指针,它指向了const对象,因此我们不能改变 \*p1的值。 51 | 52 | ②对于 int *const p2=&c,现将const和p2结合,即p2首先是一个常量,然后再左结合\*,即指针常量,它本身是一个常量,因此我们不能改变p2本身。另外因为p2本身是const,而const必须初始化,因此p2在定义时必须初始化,即不能直接 int *const p2; 53 | 54 | 55 | 56 | ## 参考资料 57 | 58 | [常量指针和指针常量的区别详解](https://www.cnblogs.com/youyifeng/articles/4542142.html) 59 | 60 | 《C++ Primer》 -------------------------------------------------------------------------------- /C++基础/55_函数指针和指针函数的区别.md: -------------------------------------------------------------------------------- 1 | ## 问题 2 | 3 | 函数指针和指针函数有什么区别? 4 | 5 | ## 指针函数 6 | 7 | 指针函数:**本质上是一个函数,其返回值是一个指针**。 8 | 9 | 声明形式:(以下几种都可以) 10 | 11 | ```C++ 12 | int *fun(int x,int y); 13 | int * fun(int x,int y); 14 | int* fun(int x,int y); //这种写法相对容易理解一些,函数的返回值是一个 int* 指针 15 | ``` 16 | 17 | 指针函数代码示例: 18 | 19 | ```C++ 20 | #include 21 | using namespace std; 22 | struct Data { 23 | int a; 24 | int b; 25 | }; 26 | //指针函数 27 | Data* fun(int a, int b) { 28 | Data* data = new Data; 29 | data->a = a; 30 | data->b = b; 31 | return data; //返回的是一个指针 32 | } 33 | int main(){ 34 | Data* myData = fun(4, 5); //调用指针函数 35 | cout << "a = " << myData->a << endl; 36 | cout << "b = " << myData->b << endl; 37 | return 0; 38 | } 39 | ``` 40 | 41 | ## 函数指针 42 | 43 | 函数指针:**本质上是一个指针,一个指向函数的指针变量**。 44 | 45 | 声明格式:类型说明符 (*函数名) (形参) 46 | 47 | ```C++ 48 | int (*fun)(int x,int y); //声明一个函数指针,即一个指向函数的指针 49 | ``` 50 | 51 | 函数赋值:(即函数指针需要把一个函数的地址赋值给它) 52 | 53 | ```C++ 54 | fun = &Function; //假设Function是一个函数名,&取函数的地址 55 | fun = Function; //&不是必需的,因为函数名本身就表示了它的地址,类似数组名一样 56 | ``` 57 | 58 | 函数指针的调用:(以下两种都可以,但都必须包含一个圆括号括起来的参数列表) 59 | 60 | ```C++ 61 | a = (*fun)(); //这种写法更加直观一点,可以直接看出是通过只针的方式调用函数的 62 | a = fun(); 63 | ``` 64 | 65 | 函数指针代码示例: 66 | 67 | ```C++ 68 | #include 69 | using namespace std; 70 | 71 | int add(int x, int y) { return x + y; } 72 | int sub(int x, int y) { return x - y; } 73 | 74 | int main(){ 75 | int (*fun)(int x, int y); //声明一个函数指针 76 | 77 | fun = &add; //函数赋值第一种方式 78 | cout << "(*fun)(1,2) = " << (*fun)(1, 2) << endl; //通过函数指针调用函数 79 | 80 | fun = sub; //函数赋值第二种方式 81 | cout << "(*fun)(5,3) = " << (*fun)(5, 3) << endl; 82 | return 0; 83 | } 84 | ``` 85 | 86 | 87 | 88 | ## 总结 89 | 90 | 总结一下函数指针和指针函数的区别: 91 | 92 | #### 定义不同 93 | 94 | 指针函数:**本质上是一个函数,其返回值是一个指针**。 95 | 96 | 函数指针:**本质上是一个指针,一个指向函数的指针**。 97 | 98 | #### 写法不同 99 | 100 | 指针函数: int* fun(int x,int y); 101 | 102 | 函数指针: int (*fun)(int x,int y); 103 | 104 | 区分方法:函数名带括号的就是函数指针,不带括号的就是指针函数。 105 | 106 | > 补充:还可以通过从右到左结合的方式区分,如 107 | > 108 | > ①对于指针函数 int* fun(int x,int y),形参列表先和函数名fun结合,也就是说fun首先是一个函数,剩下的为函数的返回类型int*。 109 | > 110 | > ②对于函数指针 int (*fun)(int x,int y),因为括号优先级高,所以\*号会先和函数名fun结合,也就是说fun此时先是一个指针,再与形参列表结合,成为一个指向函数的指针,剩下的int为函数的返回类型。 111 | 112 | #### 用法不同 113 | 114 | 具体请看上面代码示例。 115 | 116 | 117 | 118 | ## 参考资料 119 | 120 | [函数指针和指针函数用法和区别](https://blog.csdn.net/luoyayun361/article/details/80428882) -------------------------------------------------------------------------------- /C++基础/56_数组指针和指针数组的区别.md: -------------------------------------------------------------------------------- 1 | ## 问题 2 | 3 | 数组指针和指针数组有什么区别? 4 | 5 | ## 数组指针 6 | 7 | 数组指针:**本质是一个指针,指向了一个数组**,数组中的每个元素都是某种数据类型的值(比如 int 类型)。 8 | 9 | ```C++ 10 | int (*p)[n]; //定义了一个数组指针,指向一个大小为n的数组,数组中的每个元素都是int类型 11 | ``` 12 | 13 | 数组指针也称**行指针**,也就是说,当指针p执行p+1时,指针会指向数组的下一行,如: 14 | 15 | ```c++ 16 | int a[3][4]; 17 | int (*p)[4]; //p是一个数组指针,指向了一个包含4个int型元素的数组 18 | p=a; //将二维数组的首地址赋给p,即a[0]或a[0][0] 19 | p++; //跨过第一行,p指向了a[1][0] 20 | ``` 21 | 22 | 23 | 24 | ## 指针数组 25 | 26 | 指针数组:**本质是一个数组,该数组中的每个元素都是一个指针**。 27 | 28 | ```c++ 29 | int *p[n]; //定义了一个指针数组,数组大小为n,数组中的每个元素都是一个int*指针 30 | ``` 31 | 32 | 指针数组是一个包含若干个指针的数组,p是数组名,当执行p+1时,则p会指向数组中的下一个元素。 33 | 34 | ```C++ 35 | int a[3][4]; 36 | int *p[3]; //定义了一个数组,该数组中有3个int*指针变量,分别为p[0]、p[1]、p[2] 37 | //p++; //若执行此语句,则数组p指向下一个数组元素 38 | for(int i=0;i<3;i++){ 39 | p[i]=a[i]; //数组p中有3个指针,分别指向二维数组a的每一行 40 | } 41 | ``` 42 | 43 | 44 | 45 | ## 访问数组 46 | 47 | 补充一下二维数组的访问方式:对于一个二维数组 $p[m][n]$,表示其第 i 行第 j 列的元素的方式有以下4种: 48 | 49 | ``` 50 | p[i][j] //方式1 51 | *(p[i]+j) //方式2 52 | *(*(p+i)+j) //方式3 53 | (*(p+i))[j] //方式4 54 | ``` 55 | 56 | 57 | 58 | ## 总结 59 | 60 | 总结一下数组指针和指针数组的区别: 61 | 62 | #### 定义不同 63 | 64 | 数组指针:**本质是一个指针,指向了一个数组** 65 | 66 | 指针数组:**本质是一个数组,该数组中的每个元素都是一个指针** 67 | 68 | #### 写法不同 69 | 70 | 数组指针:int (*p)[n]; 71 | 72 | 指针数组:int *p[n]; 73 | 74 | 区分方法:数组名带括号的就是数组指针,不带括号的就是指针数组。(这个类似于函数指针和指针函数的区别) 75 | 76 | > 补充:同样类似函数指针和指针函数的区分方法,可以通过从右到左结合来区分 77 | > 78 | > ①对于数组指针 int (*p)[n],因为括号优先级较高,因此\*号与数组名p先结合,也就是说p首先是一个指针,然后与[n]结合,表示指针p指向了一个大小为n的数组,数组的类型为int。 79 | > 80 | > ②对于指针数组 int *p[n],p和[]先结合,因此p首先是一个大小为n的数组,剩下的部分是数组的类型,即int\*类型,也就是数组的每个元素都是一个int\*指针。 81 | 82 | 83 | 84 | ## 参考资料 85 | 86 | [数组指针和指针数组的区别](https://www.cnblogs.com/mq0036/p/3382732.html) -------------------------------------------------------------------------------- /C++基础/72_vector的扩容原理以及resize和reserve的区别.md: -------------------------------------------------------------------------------- 1 | ## 问题 2 | 3 | vector 与 C 中的数组不同,是一个数组大小可以动态变化的容器。那么这个变化过程具体是怎么变的呢,怎样才可以提高 vector 的使用效率呢? 4 | 5 | ## 原理概述 6 | 7 | 1. vector 存储的空间在内存中是连续的,如果 vector 现有空间已存满元素,在 push_back 新增数据的时候就要分配一块更大的内存,将原来的数据 copy 过来,接着释放之前的内存,再在新的内存空间中存入新增的元素。 8 | 2. 不同编译器对 vector 的扩容方式实现不一样,在 vs 中以 1.5 倍扩容,而在 gcc 中以 2 倍扩容,后面我们会看到以 1.5 倍扩容的方式效果更好。 9 | 3. vector 的初始的扩容方式代价太大,初始扩容效率低,需要频繁增长,不仅操作效率比较低,而且频繁的向操作系统申请内存容易造成过多的内存碎片,所以这个时候需要合理使用`resize()`和`reserve()`方法提高效率减少内存碎片的。 10 | 4. 对 vector 的任何操作,一旦引起空间重新配置,指向原 vector 的所有迭代器就都会失效。 11 | 12 | ## gcc 中 vector 的扩容过程 13 | 14 | 我电脑系统是ubuntu,所以下面用一段代码来直观地体现在 gcc 中 vector 的扩容过程,需要说明的是 size() 表示的是 vector 当前的元素个数,而 capacity() 表示的是 vector 当前的容量大小,也就是当前给该 vector 分配的内存空间大小,当元素个数超过 capacity() 时,就需要扩容了。 15 | 16 | ```c++ 17 | #include 18 | #include 19 | using namespace std; 20 | int main() 21 | { 22 | vector vec; 23 | cout << vec.capacity() << endl; 24 | for (int i = 0; i<10; ++i) 25 | { 26 | vec.push_back(i); 27 | cout << "size: " << vec.size() << endl; 28 | cout << "capacity: " << vec.capacity() << endl; 29 | } 30 | system("pause"); 31 | return 0; 32 | } 33 | ``` 34 | 35 | 输出如下: 36 | 37 | ![](https://i.loli.net/2020/05/19/3UV4ldbkv62NrEM.jpg) 38 | 39 | ### 为什么以成倍的方式扩容而不是一次增加一个固定大小的容量 40 | 41 | 假设我们需要往 vector 中存储 n 个元素,成倍方式增长存储空间的话,那么均摊到每一次的 push_back 操作的时间复杂度为 O(1) ,而一次增加固定值空间的方式均摊到每一次的 push_back 操作的时间复杂度为 O(n) 的时间。(具体就不展开了,相信算法面试官也不会问得这么细,又不是做底层开发的,问这么细的意义不大) 42 | 43 | ### 那为什么是以 2 倍或者 1.5 倍进行扩容呢 44 | 45 | 我们用个例子算算看: 46 | 47 | #### 两倍扩容 48 | 49 | * 假设我们一开始申请了 16Byte 的空间。 50 | 51 | * 当需要更多空间的时候,将首先申请 32Byte,然后释放掉之前的 16Byte。这释放掉的16Byte 的空间就闲置在了内存中。 52 | 53 | * 当还需要更多空间的时候,你将首先申请 64Byte,然后释放掉之前的 32Byte。这将在内存中留下一个48Byte 的闲置空间(假定之前的 16Byte 和此时释放的32Byte 合并) 54 | 55 | * 当还需要更多空间的时候,你将首先申请128Byte,然后释放掉之前的 64 Byte。这将在内存中留下一个112Byte 的闲置空间(假定所有之前释放的空间都合并成了一个块) 56 | 57 | 58 | 59 | 这时你就会发现一个问题,要申请的空间都比之前释放的空间合并起来的都大,因此无法循环利用之前释放的空间。 60 | 61 | #### 1.5 倍扩容 62 | 63 | * 假设我们一开始申请了 16Byte 的空间。 64 | * 当需要更多空间的时候,将申请 24 Byte ,然后释放掉 16 ,在内存中留下 16Byte 的空闲空间。 65 | * 当需要更多空间的时候,将申请 36 Byte,然后释放掉 24,在内存中留下 40Byte (16 + 24)的空闲空间。 66 | * 当需要更多空间的时候,将申请 54 Byte,然后释放 36,在内存中留下 76Byte。 67 | * 当需要更多空间的时候,将申请 81 Byte,然后释放 54, 在内存中留下 130Byte。 68 | * 当需要更多空间的时候,将申请 122 Byte 的空间(复用内存中闲置的 130Byte) 69 | 70 | 71 | 从上面你可以看到当要申请122 Bybes 空间的时候,之前释放的空间合并起来已经比要申请的空间大了,这时我们就可以利用之前释放的空间来充当新空间,从而减少了内存碎片风险,重复利用高更高。 72 | 73 | 所以说明了 1.5 倍扩容方法比 2 倍的好,也解释了为什么不采用更高倍的扩容方法而一般都采用 2 倍或者 1.5 倍的问题。这是空间和时间的权衡,空间分配地越多,平摊时间复杂度越低,但浪费空间也多。最好把增长因子设在 (1,2) 之间。 74 | 75 | ## resize() 和 reserve() 区别 76 | 77 | 首先介绍下 vector 中与空间大小相关的四个函数含义: 78 | 79 | - size():返回vector中的元素个数 80 | - capacity():返回vector能存储元素的总数 81 | - resize()操作:创建指定数量的的元素并指定vector的存储空间 82 | - reserve()操作:指定vector的元素总数 83 | 84 | `size()`函数返回的是已用空间大小,`capacity()`返回的是总空间大小,`capacity()-size()`则是剩余的可用空间大小。当`size()`和`capacity()`相等,说明 vector 目前的空间已被用完,如果再添加新元素,则会引起 vector 空间的动态增长。 85 | 86 | 由于动态增长会引起重新分配内存空间、拷贝原空间、释放原空间,这些过程会降低程序效率。因此,可以使用 `reserve(n)` 预先分配一块较大的指定大小的内存空间,这样当指定大小的内存空间未使用完时,是不会重新分配内存空间的,这样便提升了效率。只有当`n>capacity()`时,调用`reserve(n)`才会改变vector容量,不然保持 capacity() 不变。 87 | 88 | 1. `resize()` 既修改 capacity 的大小,也修改 size 的大小,即分配了空间的同时也创建了对象。而`reserve()` 只修改 capacity 的大小,不修改 size 大小。 89 | 90 | 2. 使用`reserve()`预先分配一块内存后,在空间未满的情况下,不会引起重新分配,从而提升了效率。 91 | 92 | 3. 当`reserve()`分配的空间比原空间小时,是不会引起重新分配的。 93 | 94 | 4. 用`reserve(size_type)`只是扩大 capacity 值,这些内存空间可能还是“野”的,如果此时使用 “[ ]” 来访问,则可能会越界。而`resize(size_type new_size)`会真正使容器具有 new_size 个对象,这时可以使用 '[ ]' 来访问。 95 | 96 | 5. 下面有一段代码说明一下关键的地方: 97 | 98 | ```c++ 99 | vector v1; 100 | v1.resize(100); // size() == 100, capacity()== 100,且这 100 个元素都默认为 0 101 | v1.push_back(3); // 这时元素 3 所在的索引为100,size() 变成 101,而 capacity() 变为 200 102 | vector v2; 103 | v2.reserve(100); // 这时的 size() == 0, capacity()== 100 104 | v2.push_back(3); // 这时元素 3 所在的索引为0,size() 变成 1,而 capacity() 依然是100 105 | ``` 106 | 107 | 108 | 109 | ## 参考资料 110 | 111 | [vector扩容原理说明](https://blog.csdn.net/yangshiziping/article/details/52550291?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-4.nonecase&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-4.nonecase) 112 | [c++STL vector扩容过程](https://blog.csdn.net/rusbme/article/details/98102016) 113 | [C++ vector中的resize,reserve,size和capacity函数讲解](https://blog.csdn.net/amusi1994/article/details/81106455) 114 | 115 | -------------------------------------------------------------------------------- /C++基础/74_sort自定义排序.md: -------------------------------------------------------------------------------- 1 | ## 问题 2 | 3 | 算法题当中对原始数据进行排序后,很大概率可以使得解题变得简单。一般情况下都是对 vector 等基本类型进行排序,这时直接使用 sort 函数即可,但是有时候我们想对自定义的结构体等类型进行排序,这时候直接调用sort就行不通了,需要另外定义一个排序规则,然后传给sort函数才行,具体怎么实现呢? 4 | 5 | ## sort内部使用的是什么排序? 6 | 7 | sort并不是简单的快速排序,它对普通的快速排序进行了优化,此外,它还结合了插入排序和堆排序。系统会根据你的数据形式和数据量自动选择合适的排序方法,这并不是说它每次排序只选择一种方法,它是在一次完整排序中不同的情况选用不同方法,比如给一个数据量较大的数组排序,开始采用快速排序,分段递归,分段之后每一段的数据量达到一个较小值后它就不继续往下递归,而是选择插入排序,如果递归的太深,他会选择堆排序。 8 | 9 | ## sort的基本使用 10 | 11 | 要使用该函数需要先包含:`#include ` 12 | sort的函数原型为:`sort(first_pointer, first_pointer + n, cmp)` 13 | 14 | **参数1**:第一个参数是数组的首地址,一般写上数组名就可以,因为数组名是一个指针常量。(左闭) 15 | **参数2**:第二个参数相对较好理解,即首地址加上数组的长度n(代表尾地址的下一地址)。(右开) 16 | **参数3**:默认可以不填,如果不填sort会默认按数组升序排序。也就是1,2,3,4排序。也可以自定义一个排序函数,改排序方式为降序什么的,也就是4,3,2,1这样。 17 | 18 | 下面给一个sort最常使用的场景: 19 | 20 | ```c++ 21 | #include 22 | #include 23 | using namespace std; 24 | 25 | int main() { 26 | vector nums({13, 5, 3, 7, 43}); 27 | sort(nums.begin(), nums.end()); // 默认升序排序 28 | for(auto i:nums) { 29 | cout< 40 | #include 41 | using namespace std; 42 | 43 | //自定义一个降序排序函数 44 | bool cmp(const int a, const int b) { 45 | return a > b; // 前者大于后者返回true,因此为降序排序 46 | } 47 | 48 | int main() { 49 | vector nums({13, 5, 3, 7, 43}); 50 | sort(nums.begin(), nums.end(), cmp); // cmp函数作为第三个参数传进去即可 51 | for(auto i:nums) { 52 | cout< 63 | #include 64 | #include 65 | using namespace std; 66 | 67 | bool cmp(string a,string b) 68 | { 69 | return a>b; 70 | } 71 | int main() 72 | { 73 | string a[4]={"hhhhh","heheheh","xxxxxx","kkkkkk"}; 74 | sort(a,a+4,cmp); 75 | for(int i=0;i<4;i++) 76 | cout<实例化3只dog,并且按照先排公狗,后排母狗的规则排序。排序时先让年龄从大到小排序,如果年龄一样,再考按照体重从轻到重排。 86 | 87 | ```c++ 88 | #include 89 | #include 90 | #include 91 | using namespace std; 92 | struct Dog{ 93 | int age; 94 | int weight; 95 | string sex; 96 | }; 97 | bool cmp(struct Dog a,struct Dog b) 98 | { 99 | if(a.sex==b.sex){ 100 | if(a.age==b.age){ 101 | return a.weightb.age;//年龄降序 104 | } 105 | else return a.sex>b.sex;//性别字典序降序 106 | } 107 | int main() 108 | { 109 | Dog dog[3]; 110 | dog[0].sex="male"; 111 | dog[0].age=3; 112 | dog[0].weight=20; 113 | 114 | dog[1].sex="male"; 115 | dog[1].age=3; 116 | dog[1].weight=15; 117 | 118 | dog[2].sex="male"; 119 | dog[2].age=3; 120 | dog[2].weight=44; 121 | 122 | sort(dog,dog+3,cmp); 123 | for(int i=0;i<3;i++){ 124 | cout< 134 | #include 135 | #include 136 | using namespace std; 137 | typedef struct student{ 138 | char name[20]; 139 | int math; 140 | // 重载 < 运算符 141 | bool operator < (const student &x){ 142 | return math > x.math ; 143 | } 144 | }Student; 145 | int main(){ 146 | Student a[4]={{"apple",67},{"limei",90},{"apple",90}}; 147 | sort(a,a+3); // 在结构体内重载了 < 运算符后,就可以跟vector等类型一样只传入两个参数即可。 148 | for(int i=0;i<3;i++) 149 | cout<>1, m) % m; // 注意原则是“步步取模” 14 | return temp * temp % m; 15 | } 16 | } 17 | ``` 18 | 19 | ## 迭代写法 20 | 21 | ```c++ 22 | // 迭代写法 23 | typedef long long ll; 24 | ll bianarypow(ll x, ll n, ll m) { 25 | ll ans = 1; 26 | x = x % m; // 注意要加上这一句,即先对x取模,因为取模后相乘最后取模是不变的(根据下图运算法则) 27 | while(n) { 28 | if(n&1) ans = ans * x % m; 29 | n >>= 1; 30 | x = x * x % m; 31 | } 32 | return ans; 33 | } 34 | ``` 35 | 36 | ## 模运算规则 37 | 38 | ![](https://i.loli.net/2020/05/18/dquYF3UKaARnMOm.png) 39 | 40 | ## 各种运算符所要消耗的cpu时钟 41 | 42 | 1. +、-:需要2个cpu时钟 43 | 2. 位运算只需要1个cpu时钟 44 | 3. 乘法需要4个cpu时钟 45 | 4. 除法需要40个cpu时钟 46 | 47 | ## 参考资料 48 | 49 | [算法学习笔记(4):快速幂](https://zhuanlan.zhihu.com/p/95902286) 50 | [快速幂](https://blog.csdn.net/Harington/article/details/87602682) -------------------------------------------------------------------------------- /C++基础/77_牛客网笔试处理各种输入输出的解决方法.md: -------------------------------------------------------------------------------- 1 | ## 问题 2 | 3 | 不知各位大佬有没有在牛客网的笔试当中被怎么处理输入的事情烦恼过,我是每次都要纠结多长时间,浪费了宝贵的短暂笔试时间,导致最后笔试结果不理想(搞得好像搞定输入后就能写出来一样哈哈哈哈)。为了以后的笔试能够专心在思考算法上面,这里专门整理一下牛客网可能出现的各种输入样例的解决方案。 4 | 5 | ### C++中跟输入有关的知识 6 | 7 | #### cin>> 8 | 9 | **(1)** 获取输入的一个字符或数字:`cin>>`会自动过滤掉不可见字符(如空格、回车、tab等)。若想过滤掉空字符,可以使用 `noskipws` 流进行控制。如下程序,输入的空格字符将会被存入 `input[1]` 中,也可以用于输出。 10 | 11 | ```c++ 12 | #include 13 | using namespace std; 14 | int main(){ 15 | char input[5]; 16 | for(int i=0; i<5; i++) { 17 | cin>>noskipws>>input[i]; // 输入: d kjh 18 | } 19 | for(int i=0; i<5; i++){ 20 | cout< 32 | using namespace std; 33 | int main(){ 34 | char input[5]; 35 | cin>>input; // 输入: dede po 36 | cout<>s; // 这里直接从输入流中读取,不再需要我们输入 39 | cout< 50 | using namespace std; 51 | int main(){ 52 | char input[5]; 53 | for(int i=0; i<5; i++){ 54 | cin.get(input[i]); // 输入: f hkjil 55 | cout< 67 | using namespace std; 68 | int main(){ 69 | char input[5]; 70 | cin.get(input, 5); // 输入: d hlyoj 71 | cout< 86 | using namespace std; 87 | int main(){ 88 | char input[5]; 89 | cin.getline(input, 5); // 输入: d hlyoj 90 | cout<。使用需包含头文件#include。`getline(cin,string s)`,接收一个字符串,可以接收空格等,不设置的话默认遇到回车停止读入操作。getline(cin, string s) 处理后还留有结束符在输入流中,故需要使用 cin.get() 来接收最后一个结束符。 98 | 99 | **与cin.getline()的区别**: 100 | 101 | 1. cin.getline()接收输入字符串的是数组,getline()是string类型。 102 | 2. cin.getline()会在数组结尾是'\0',getline()不会 103 | 104 | ```c++ 105 | #include 106 | #include 107 | using namespace std; 108 | int main(){ 109 | string s; 110 | getline(cin, s); // 输入: d hlyoj 111 | cout<。可以接收空格,遇回车结束。作用跟getline()基本就是一样的了。 119 | 120 | ### 可能遇到的输入样例 121 | 122 | **1、全部数据在一行中输入,没有说明输入的数字个数是多少,而且数字之间的分隔符可能是空格或者逗号。** 123 | 124 | ```c++ 125 | #include 126 | using namespace std; 127 | int main(){ 128 | vector nums; // 将输入的数据先全部放到一个数组当中,之后再根据具体情况切割处理 129 | int a; 130 | while(cin>>a){ // 输入: 1,2,3,4,5,6 \n 或者 1 2 3 4 5 6 \n 131 | nums.push_back(a); // 读取结束后: nums=[1, 2, 3, 4, 5, 6] 132 | if(cin.get() != '\n') continue; 133 | else { 134 | // 在这里写具体的算法,这样便可以达到循环输入输出的问题,因为有的时候测试数据是多组,需要提交的代码需要做循环处理。 135 | } 136 | } 137 | return 0; 138 | } 139 | ``` 140 | 141 | 2、输入的是带空白的字符串,这种情况比较可恶,因为 scanf 和 cin 都是读到空白就结束的,这时候就得使用上面知识点讲到的 getline(cin, string s) 来读取了。 142 | 143 | 3、预先不知道输入数据的组数——读到文件结尾 144 | 145 | ```c++ 146 | // C语言这样写 147 | int a,b; 148 | while(scanf("%d%d, %a, %b") != EOF){ 149 | printf("%d\n",a+b); 150 | } 151 | 152 | // C++这样写 153 | int a,b; 154 | while(cin>>a>>b){ 155 | cout<>n; 174 | for(int i=0; i>a>>b; 177 | cout<>a>>b; 192 | cout<>n; 215 | for(int i=0; i>a>>b; 218 | cout<<"Case " << i+1 << a+b << endl; 219 | //cout<<"Case " << i+1 << a+b << endl << endl; // 情况三 220 | } 221 | ``` 222 | 223 | 3、每个 case 之后有空行,这个就在上面的基础上多加一个 ‘\n’ 即可,如上述代码注释掉的部分。 224 | 225 | 4、两个 case 之间有空行。这个的意思是最后一组输出的后面是没有空行的。 226 | 227 | ```c++ 228 | int n; 229 | cin>>n; 230 | for(int i=0; i>a>>b; 233 | if(i > 0){ 234 | cout<(对于这个,我还没具体在C++中遇到过,但是在写python程序的时候,定义了一个字典 dict() ,因为这个问题被坑了好久) 248 | 3. 不用保存所有的输入,读一组计算一组,输出一组即可 249 | 250 | ## 参考资料 251 | 252 | [C++获取字符cin,getchar,get,getline的区别](https://www.cnblogs.com/shrimp-can/p/5241544.html) 253 | [(经验贴)那些年在编程题中踩过的坑](https://www.nowcoder.com/discuss/8632) 254 | [牛客网在线判题系统使用帮助](https://www.nowcoder.com/discuss/276) 255 | [OnlineJudge-OJ输入输出基础必练0813新增三题](https://www.nowcoder.com/discuss/216684?type=post&order=time&pos=&page=1&channel=666&source_id=search_post) -------------------------------------------------------------------------------- /PDF版/C++基础/02_incluede后面使用双引号和尖括号的区别.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/C++基础/02_incluede后面使用双引号和尖括号的区别.pdf -------------------------------------------------------------------------------- /PDF版/C++基础/04_C++11常用新特性.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/C++基础/04_C++11常用新特性.pdf -------------------------------------------------------------------------------- /PDF版/C++基础/05_请说一下你理解的 C++ 中的四个智能指针.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/C++基础/05_请说一下你理解的 C++ 中的四个智能指针.pdf -------------------------------------------------------------------------------- /PDF版/C++基础/11_new&delete与malloc&free 之间的关系与区别.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/C++基础/11_new&delete与malloc&free 之间的关系与区别.pdf -------------------------------------------------------------------------------- /PDF版/C++基础/12_delete和delete[]的区别.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/C++基础/12_delete和delete[]的区别.pdf -------------------------------------------------------------------------------- /PDF版/C++基础/14_虚函数详解.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/C++基础/14_虚函数详解.pdf -------------------------------------------------------------------------------- /PDF版/C++基础/15_纯虚函数.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/C++基础/15_纯虚函数.pdf -------------------------------------------------------------------------------- /PDF版/C++基础/16_析构函数.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/C++基础/16_析构函数.pdf -------------------------------------------------------------------------------- /PDF版/C++基础/24_sizeof与strlen的区别.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/C++基础/24_sizeof与strlen的区别.pdf -------------------------------------------------------------------------------- /PDF版/C++基础/25_strcpy_strncpy_memcpy的区别.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/C++基础/25_strcpy_strncpy_memcpy的区别.pdf -------------------------------------------------------------------------------- /PDF版/C++基础/29_哈希冲突和解决方法.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/C++基础/29_哈希冲突和解决方法.pdf -------------------------------------------------------------------------------- /PDF版/C++基础/31_c++中的左值引用与右值引用.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/C++基础/31_c++中的左值引用与右值引用.pdf -------------------------------------------------------------------------------- /PDF版/C++基础/32_C++中的内存管理是怎样的.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/C++基础/32_C++中的内存管理是怎样的.pdf -------------------------------------------------------------------------------- /PDF版/C++基础/35_内存泄漏是什么.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/C++基础/35_内存泄漏是什么.pdf -------------------------------------------------------------------------------- /PDF版/C++基础/36_为什么要内存对齐.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/C++基础/36_为什么要内存对齐.pdf -------------------------------------------------------------------------------- /PDF版/C++基础/37_全局变量和局部变量的区别.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/C++基础/37_全局变量和局部变量的区别.pdf -------------------------------------------------------------------------------- /PDF版/C++基础/46_深拷贝与浅拷贝的区别.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/C++基础/46_深拷贝与浅拷贝的区别.pdf -------------------------------------------------------------------------------- /PDF版/C++基础/50_引用作为函数参数和返回值的好处.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/C++基础/50_引用作为函数参数和返回值的好处.pdf -------------------------------------------------------------------------------- /PDF版/C++基础/52_虚函数可以声明为inline吗.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/C++基础/52_虚函数可以声明为inline吗.pdf -------------------------------------------------------------------------------- /PDF版/C++基础/54_常量指针和指针常量的区别.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/C++基础/54_常量指针和指针常量的区别.pdf -------------------------------------------------------------------------------- /PDF版/C++基础/55_函数指针和指针函数的区别.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/C++基础/55_函数指针和指针函数的区别.pdf -------------------------------------------------------------------------------- /PDF版/C++基础/56_数组指针和指针数组的区别.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/C++基础/56_数组指针和指针数组的区别.pdf -------------------------------------------------------------------------------- /PDF版/C++基础/63_7种经典的排序算法原理及C++实现.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/C++基础/63_7种经典的排序算法原理及C++实现.pdf -------------------------------------------------------------------------------- /PDF版/C++基础/72_vector的扩容原理以及resize和reserve的区别.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/C++基础/72_vector的扩容原理以及resize和reserve的区别.pdf -------------------------------------------------------------------------------- /PDF版/C++基础/74_sort自定义排序.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/C++基础/74_sort自定义排序.pdf -------------------------------------------------------------------------------- /PDF版/C++基础/75_快速幂.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/C++基础/75_快速幂.pdf -------------------------------------------------------------------------------- /PDF版/C++基础/76_二叉树前中后层遍历代码模板.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/C++基础/76_二叉树前中后层遍历代码模板.pdf -------------------------------------------------------------------------------- /PDF版/C++基础/77_牛客网笔试处理各种输入输出的解决方法.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/C++基础/77_牛客网笔试处理各种输入输出的解决方法.pdf -------------------------------------------------------------------------------- /PDF版/其他常考察问题/01_用天平找次品.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/其他常考察问题/01_用天平找次品.pdf -------------------------------------------------------------------------------- /PDF版/其他常考察问题/02_TCP三次握手四次挥手的过程.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/其他常考察问题/02_TCP三次握手四次挥手的过程.pdf -------------------------------------------------------------------------------- /PDF版/图像处理/01_LBP算法原理.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/图像处理/01_LBP算法原理.pdf -------------------------------------------------------------------------------- /PDF版/图像处理/02_HOG算法原理.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/图像处理/02_HOG算法原理.pdf -------------------------------------------------------------------------------- /PDF版/图像处理/03_FAST、BRIEF、ORB算法原理.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/图像处理/03_FAST、BRIEF、ORB算法原理.pdf -------------------------------------------------------------------------------- /PDF版/图像处理/08_边缘检测算子有哪些以及它们之间的对比.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/图像处理/08_边缘检测算子有哪些以及它们之间的对比.pdf -------------------------------------------------------------------------------- /PDF版/图像处理/12_常见的三种图像插值方法.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/图像处理/12_常见的三种图像插值方法.pdf -------------------------------------------------------------------------------- /PDF版/图像处理/14_开操作与闭操作.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/图像处理/14_开操作与闭操作.pdf -------------------------------------------------------------------------------- /PDF版/图像处理/15_Hough变换检测直线与圆的原理.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/图像处理/15_Hough变换检测直线与圆的原理.pdf -------------------------------------------------------------------------------- /PDF版/图像处理/17_图像拼接原理介绍.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/图像处理/17_图像拼接原理介绍.pdf -------------------------------------------------------------------------------- /PDF版/图像处理/18_图像配准.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/图像处理/18_图像配准.pdf -------------------------------------------------------------------------------- /PDF版/机器学习/01_LR和SVM的比较.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/机器学习/01_LR和SVM的比较.pdf -------------------------------------------------------------------------------- /PDF版/机器学习/11_三种主要集成学习思想简介.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/机器学习/11_三种主要集成学习思想简介.pdf -------------------------------------------------------------------------------- /PDF版/机器学习/12_Adaboost_GBDT_XGBoost算法原理.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/机器学习/12_Adaboost_GBDT_XGBoost算法原理.pdf -------------------------------------------------------------------------------- /PDF版/机器学习/17_bagging算法思想及与DNN中的dropout思想的一致性.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/机器学习/17_bagging算法思想及与DNN中的dropout思想的一致性.pdf -------------------------------------------------------------------------------- /PDF版/机器学习/19_如何从偏差和方差的角度解释bagging和boosting的原理.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/机器学习/19_如何从偏差和方差的角度解释bagging和boosting的原理.pdf -------------------------------------------------------------------------------- /PDF版/机器学习/20_随机森林思想.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/机器学习/20_随机森林思想.pdf -------------------------------------------------------------------------------- /PDF版/机器学习/26_k-means算法原理.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/机器学习/26_k-means算法原理.pdf -------------------------------------------------------------------------------- /PDF版/机器学习/29_k-means和GMM的区别与联系.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/机器学习/29_k-means和GMM的区别与联系.pdf -------------------------------------------------------------------------------- /PDF版/机器学习/33_决策树的原理以及决策树如何生成.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/机器学习/33_决策树的原理以及决策树如何生成.pdf -------------------------------------------------------------------------------- /PDF版/机器学习/34_ID3_C4.5_CART算法总结与对比.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/机器学习/34_ID3_C4.5_CART算法总结与对比.pdf -------------------------------------------------------------------------------- /PDF版/机器学习/44_PCA算法原理.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/机器学习/44_PCA算法原理.pdf -------------------------------------------------------------------------------- /PDF版/机器学习/45_LDA算法原理.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/机器学习/45_LDA算法原理.pdf -------------------------------------------------------------------------------- /PDF版/机器学习/46_PCA与LDA比较.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/机器学习/46_PCA与LDA比较.pdf -------------------------------------------------------------------------------- /PDF版/机器学习/49_判别式模型和生成式模型.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/机器学习/49_判别式模型和生成式模型.pdf -------------------------------------------------------------------------------- /PDF版/机器学习/57_机器学习中常用的损失函数一览.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/机器学习/57_机器学习中常用的损失函数一览.pdf -------------------------------------------------------------------------------- /PDF版/机器学习/58_线性回归损失函数为什么要用平方形式.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/机器学习/58_线性回归损失函数为什么要用平方形式.pdf -------------------------------------------------------------------------------- /PDF版/机器学习/59_逻辑回归与线性回归之间的异同.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/机器学习/59_逻辑回归与线性回归之间的异同.pdf -------------------------------------------------------------------------------- /PDF版/计算机视觉/02_过拟合和欠拟合的表现与解决方法.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/计算机视觉/02_过拟合和欠拟合的表现与解决方法.pdf -------------------------------------------------------------------------------- /PDF版/计算机视觉/03_代码实现卷积操作.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/计算机视觉/03_代码实现卷积操作.pdf -------------------------------------------------------------------------------- /PDF版/计算机视觉/04_BN层的深入理解.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/计算机视觉/04_BN层的深入理解.pdf -------------------------------------------------------------------------------- /PDF版/计算机视觉/05_NMS详细工作机制及代码实现.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/计算机视觉/05_NMS详细工作机制及代码实现.pdf -------------------------------------------------------------------------------- /PDF版/计算机视觉/06_三种常见的激活函数.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/计算机视觉/06_三种常见的激活函数.pdf -------------------------------------------------------------------------------- /PDF版/计算机视觉/07_ReLU函数在0处不可导,为什么还能用.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/计算机视觉/07_ReLU函数在0处不可导,为什么还能用.pdf -------------------------------------------------------------------------------- /PDF版/计算机视觉/08_Pooling层的作用以及如何进行反向传播.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/计算机视觉/08_Pooling层的作用以及如何进行反向传播.pdf -------------------------------------------------------------------------------- /PDF版/计算机视觉/09_为什么输入网络前要对图像做归一化.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/计算机视觉/09_为什么输入网络前要对图像做归一化.pdf -------------------------------------------------------------------------------- /PDF版/计算机视觉/10_理清深度学习优化函数发展脉络.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/计算机视觉/10_理清深度学习优化函数发展脉络.pdf -------------------------------------------------------------------------------- /PDF版/计算机视觉/11_梯度消失和爆炸以及解决方法.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/计算机视觉/11_梯度消失和爆炸以及解决方法.pdf -------------------------------------------------------------------------------- /PDF版/计算机视觉/14_CNN在图像上表现好的原因.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/计算机视觉/14_CNN在图像上表现好的原因.pdf -------------------------------------------------------------------------------- /PDF版/计算机视觉/21_手动推导反向传播公式BP.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/计算机视觉/21_手动推导反向传播公式BP.pdf -------------------------------------------------------------------------------- /PDF版/计算机视觉/22_CNN网络各种层的FLOPs和参数量paras计算.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/计算机视觉/22_CNN网络各种层的FLOPs和参数量paras计算.pdf -------------------------------------------------------------------------------- /PDF版/计算机视觉/23_弄明白感受野大小的计算问题.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/计算机视觉/23_弄明白感受野大小的计算问题.pdf -------------------------------------------------------------------------------- /PDF版/计算机视觉/24_卷积网络中的卷积与互相关的那点事.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/计算机视觉/24_卷积网络中的卷积与互相关的那点事.pdf -------------------------------------------------------------------------------- /PDF版/计算机视觉/32_为什么用F1-score.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/计算机视觉/32_为什么用F1-score.pdf -------------------------------------------------------------------------------- /PDF版/计算机视觉/34_简述CNN分类网络的演变脉络及各自的贡献与特点.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/计算机视觉/34_简述CNN分类网络的演变脉络及各自的贡献与特点.pdf -------------------------------------------------------------------------------- /PDF版/计算机视觉/38_了解全卷积网络FCN.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/计算机视觉/38_了解全卷积网络FCN.pdf -------------------------------------------------------------------------------- /PDF版/计算机视觉/40_解释 ROI Pooling 和 ROI Align 的区别.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/计算机视觉/40_解释 ROI Pooling 和 ROI Align 的区别.pdf -------------------------------------------------------------------------------- /PDF版/计算机视觉/41_CascadeRCNN本质解决了什么问题.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/计算机视觉/41_CascadeRCNN本质解决了什么问题.pdf -------------------------------------------------------------------------------- /PDF版/计算机视觉/44_特征融合concat和add的区别.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/计算机视觉/44_特征融合concat和add的区别.pdf -------------------------------------------------------------------------------- /PDF版/计算机视觉/46_LSTM介绍理解.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/计算机视觉/46_LSTM介绍理解.pdf -------------------------------------------------------------------------------- /PDF版/计算机视觉/48_各种卷积方式串讲.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/计算机视觉/48_各种卷积方式串讲.pdf -------------------------------------------------------------------------------- /PDF版/计算机视觉/49_Faster_R_CNN和Mask_R_CNN的损失函数详解.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/PDF版/计算机视觉/49_Faster_R_CNN和Mask_R_CNN的损失函数详解.pdf -------------------------------------------------------------------------------- /其他常考察问题/01_用天平找次品.md: -------------------------------------------------------------------------------- 1 | ## 问题 2 | 3 | 现在假设有 n 个货物,其中有一个货物是次品,次品比正常的要轻一些,你只有一个天平,请问,至少需要称多少次能保证一定找到次品? 4 | 5 | 这其实是一道非常简单的问题,但是要是之前没遇到过还真有可能不知道怎么下手,通常就是采用对半分的方法了,但这个思路是错的。 6 | 7 | 我第一次碰到这个题目是在家辅导小学的妹妹做数学题的时候,那个时候我想的就是对半分,然后错了,然后就被鄙视了。后来在一些公司笔试以及面经当中居然又碰到,所以感觉这种题目考察地挺多的,还有有必要梳理一下解题的思路和套路。 8 | 9 | ## 解题思路 10 | 11 | 其实解题方法很简单,就是不断地3等分即可,那为什么是3等分,而不是4等分或者2等分呢?你想想,如果是三等分的话,假设分成 A、B、C三组,阿么可以先比较 A 和 B,如果次品在这两组中,那么后续就只要在轻的那一组中继续寻找即可,如果 A 和 B相等,那么次品就肯定在 C 中,后续只要在 C 中找即可,也就是说称一次可以排除掉三分之二的可能性。 12 | 13 | 而要是2等分的话,称一次只能排除掉二分之一的可能性。4等分的话,就不好说了,可能性很多,最差的情况是比3等分差的。 14 | 15 | 所以只要不断地3等分即可。但是有一个问题是每一步中要从中寻找次品的货物集合的数量 n 可能不是3的倍数,所以并不能完全地等分,一共有三种情况。 16 | 17 | 1. n % 3 = 0 18 | 2. n % 3 = 1 19 | 3. n % 3 = 2 20 | 21 | ### 第一种情况:n % 3 = 0 22 | 23 | 这种情况就很简单,直接三等分即可。例如 n = 21,那么三等分就是 7 、7、 7 24 | 25 | ### 第二种情况:n % 3 = 1 26 | 27 | 例如 n = 22时,这时候应该这么分: 7 、7、8。也就是前面两组数量相同,这样便可以先把这两组放到天平上去对比。第三组的数量比前面两组多一个。那么这个数字是怎么计算出来的呢? 28 | 29 | 前面两组的数量 m = (n - 1) / 3 30 | 31 | 那么第三组的数量便是 m + 1 32 | 33 | ### 第三种情况:n % 3 = 2 34 | 35 | 例如 n = 23时,这时候应该这么分: 8、8、7。也就是前面两组数量相同,这样便可以先把这两组放到天平上去对比。第三组的数量比前面两组少一个。那么这个数字又是怎么计算出来的呢? 36 | 37 | 前面两组的数量 m = (n + 1) / 3 38 | 39 | 那么第三组的数量便是 m - 1 40 | 41 | ## 总结 42 | 43 | 所以你需要不断三等分,然后下次迭代时选择数量组多的那组进行迭代,这样得到的次数便是符合要求的答案。例如 n = 22时,这时候应该这么分: 7 、7、8,然后下一次把 8 分成 3 、3、 2,然后下一次把3 分成 1 、1、 1。如此类推。 44 | 45 | 直到货物堆只剩1个,就能找到答案。 46 | 47 | 答案为 $log_3(n)$ 往下取整。 48 | 49 | ## 代码实现 50 | 51 | 网上看到一段代码,也摘抄下来,说不定面试官就叫你实现这个算法呢。 52 | 53 | ```c++ 54 | int end(int n) 55 | { 56 | int ans = 0; 57 | if(n % 3 == 0) 58 | n--; 59 | while(n > 0) 60 | { 61 | n /= 3; 62 | ans++; 63 | } 64 | return ans; 65 | } 66 | ``` 67 | 68 | 可能你不理解为什么上面需要n-- 69 | 70 | 那是因为,当n为3的指数倍时,如9,总有9/3 = 3, 9/3=1,剩下1一个本应该停止,但是还是继续进入循环。。变成3次 71 | 72 | 所以使用n--来减少一次次数。因为4~8进入循环返回的ans都为2。虽然6满足6%3==0但不会出问题,因为6跟5是返回ans都为2。 73 | 74 | 特殊的只有n的指数倍,但刻意去找n的指数太浪费时间,所以才使用n%3==0来排除,使用这个也不会影响6, 12等虽然满足3的整数倍但不是n的指数倍的数的问题 75 | 76 | ## 参考资料 77 | 78 | [用天平找次品的算法题,即三等分算法](https://blog.csdn.net/ljt735029684/article/details/80650771?utm_medium=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.nonecase&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.nonecase) -------------------------------------------------------------------------------- /其他常考察问题/02_TCP三次握手四次挥手的过程.md: -------------------------------------------------------------------------------- 1 | ## 问题 2 | 3 | 其实我觉得计算机网络是一个很好玩的东西,相信每个程序员都有一个黑客梦吧,而计算机网络是黑客必须要吃透的东西,虽然我也很崇拜黑客,但是却没有开始去学习这方面的东西,等以后工作了空余时间挺想用来专研这东西的。废话少说,我们稍微回到正题。 4 | 5 | 其实我感觉这个问题在CV算法岗面试的时候一般不会问的吧,但是不知道为什么实验室师兄的经验中都说也要掌握计算机网络的东西,所以还是要学习下,其实计算机网络在本科选修课中学过一下,所以对一些概念还是有一点印象的。 6 | 7 | ## TCP三次握手 8 | 9 | 首先要说明一下,这个握手是客户端与服务器端在相互信息传输过程中的概念。客户端与服务器之间数据的发送和返回的过程当中需要创建一个叫TCP connection的东西; 10 | 11 | 由于TCP不存在连接的概念,只存在请求和响应,请求和响应都是数据包,它们之间都是经过由TCP创建的一个从客户端发起,服务器接收的类似连接的通道。 12 | 13 | 其实在我们的理解中,一次通信只要两次握手就够了:第一次握手,客户端发了个连接请求消息到服务端,服务端收到信息后知道自己与客户端是可以连接成功的,但此时客户端并不知道服务端是否已经接收到了它的请求,所以服务端接收到消息后得应答,客户端得到服务端的反馈后,才确定自己与服务端是可以连接上的,这就是第二次握手。 14 | 15 | **那为什么需要第三次握手呢?** 16 | 17 | 肯定是因为两次握手会出现些问题,所以才需要第三次握手来进一步确认。那么仅仅两次握手的话会出现什么问题呢? 18 | 19 | 譬如发起请求遇到类似这样的情况:客户端发出去的第一个连接请求由于某些原因在网络节点中滞留了导致延迟,直到连接释放的某个时间点才到达服务端,这是一个早已失效的报文,但是此时服务端仍然认为这是客户端的建立连接请求第一次握手,于是服务端回应了客户端,第二次握手。 20 | 21 | 如果只有两次握手,那么到这里,连接就建立了,但是此时客户端并没有任何数据要发送,而服务端还在傻傻的等候佳音,造成很大的资源浪费。所以需要第三次握手,只有客户端再次回应一下,就可以避免这种情况。 22 | 23 | **所以第三次握手是为了防止服务器端开启一些无用的连接增加服务器开销以及防止已经失效的连接请求报文段突然又传到服务端,因而产生错误。** 24 | 25 | ![img](https://i.loli.net/2020/06/25/m8wG4YEp9h5UZOB.jpg) 26 | 27 | 为了更好更形象地理解这个机制的作用,建议看看我[参考的这篇博客](https://blog.csdn.net/X8i0Bev/article/details/83066876#commentBox)中举的一个男生女生太恋爱的例子,虽然作为单身狗很难与博主产生共鸣,但是确实帮助我更好的清晰地理解了TCP三次握手的含义,看来恋爱还是有点用处的。 28 | 29 | 当然上面只是比较形象比较浅显的理解,想要深入一点的话,可以看看下面参考资料的第一篇CSDN官方的文章。 30 | 31 | ## TCP的四次挥手 32 | 33 | 握手是连接的过程,那么挥手顾名思义就是TCP链接释放的过程。 34 | 35 | ![img](https://i.loli.net/2020/06/24/v74j9fQDTbHNwMO.jpg) 36 | 37 | 挥手之前主动释放连接的客户端结束ESTABLISHED阶段。随后开始“四次挥手”: 38 | 39 | 1. 首先客户端想要释放连接,向服务器端发送一段TCP报文 “请求释放链接”,随后客户端进入FIN-WAIT-1阶段,即半关闭阶段。并且停止在客户端到服务器端方向上发送数据,但是客户端仍然能接收从服务器端传输过来的数据。(注意:这里不发送的是正常连接时传输的数据(非确认报文),而不是一切数据,所以客户端仍然能发送ACK确认报文。) 40 | 2. 服务器端接收到从客户端发出的TCP报文之后,确认了客户端想要释放连接,随后服务器端结束ESTABLISHED阶段,进入CLOSE-WAIT阶段(半关闭状态)并返回一段TCP报文表示“接收到客户端发送的释放连接的请求,随后服务器端开始准备释放服务器端到客户端方向上的连接。客户端收到从服务器端发出的TCP报文之后,确认了服务器收到了客户端发出的释放连接请求,随后客户端结束FIN-WAIT-1阶段,进入FIN-WAIT-2阶段。 41 | 3. 服务器端自从发出ACK确认报文之后,经过CLOSED-WAIT阶段,做好了释放服务器端到客户端方向上的连接准备,再次向客户端发出一段TCP报文,表示“已经准备好释放连接了”,随后服务器端结束CLOSE-WAIT阶段,进入LAST-ACK阶段。并且停止在服务器端到客户端的方向上发送数据,但是服务器端仍然能够接收从客户端传输过来的数据。 42 | 4. 客户端收到从服务器端发出的TCP报文,确认了服务器端已做好释放连接的准备,结束FIN-WAIT-2阶段,进入TIME-WAIT阶段,并向服务器端发送一段报文表示“接收到服务器准备好释放连接的信号”。随后客户端开始在TIME-WAIT阶段等待2MSL,服务器端收到从客户端发出的TCP报文之后结束LAST-ACK阶段,进入CLOSED阶段。由此正式确认关闭服务器端到客户端方向上的连接。客户端等待完2MSL之后,结束TIME-WAIT阶段,进入CLOSED阶段,由此完成“四次挥手”。 43 | 44 | ## **为什么“握手”是三次,“挥手”却要四次?** 45 | 46 | TCP建立连接时之所以只需要"三次握手",是因为在第二次"握手"过程中,服务器端发送给客户端的TCP报文是以SYN与ACK作为标志位的。SYN是请求连接标志,表示服务器端同意建立连接;ACK是确认报文,表示告诉客户端,服务器端收到了它的请求报文。 47 | 48 | 即SYN建立连接报文与ACK确认接收报文是在同一次"握手"当中传输的,所以"三次握手"不多也不少,正好让双方明确彼此信息互通。 49 | 50 | TCP释放连接时之所以需要“四次挥手”,是因为FIN释放连接报文与ACK确认接收报文是分别由第二次和第三次"握手"传输的。为何建立连接时一起传输,释放连接时却要分开传输? 51 | 52 | > 建立连接时,被动方服务器端结束CLOSED阶段进入“握手”阶段并不需要任何准备,可以直接返回SYN和ACK报文,开始建立连接。释放连接时,被动方服务器,突然收到主动方客户端释放连接的请求时并不能立即释放连接,因为还有必要的数据需要处理,所以服务器先返回ACK确认收到报文,经过CLOSE-WAIT阶段准备好释放连接之后,才能返回FIN释放连接报文。 53 | 54 | 所以是“三次握手”,“四次挥手”。 55 | 56 | 这些内容在参考资料中都讲解地非常清楚了。建议去看看,其实挺好理解的,不过要我很清楚地讲明白这个过程,还是一件挺有难度的事情。 57 | 58 | ## 参考资料 59 | 60 | [详解 TCP 连接的“ 三次握手 ”与“ 四次挥手 ”](https://baijiahao.baidu.com/s?id=1654225744653405133&wfr=spider&for=pc) -------------------------------------------------------------------------------- /图像处理/01_LBP算法原理.assets/20140221184735734: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/图像处理/01_LBP算法原理.assets/20140221184735734 -------------------------------------------------------------------------------- /图像处理/01_LBP算法原理.assets/20150805105836031: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/图像处理/01_LBP算法原理.assets/20150805105836031 -------------------------------------------------------------------------------- /图像处理/01_LBP算法原理.assets/HAfcpDgYmkdnsK1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/图像处理/01_LBP算法原理.assets/HAfcpDgYmkdnsK1.png -------------------------------------------------------------------------------- /图像处理/01_LBP算法原理.assets/OkCFW9vpgAStKT1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/图像处理/01_LBP算法原理.assets/OkCFW9vpgAStKT1.png -------------------------------------------------------------------------------- /图像处理/01_LBP算法原理.assets/bzk3tnEWw5GjrC9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/图像处理/01_LBP算法原理.assets/bzk3tnEWw5GjrC9.png -------------------------------------------------------------------------------- /图像处理/01_LBP算法原理.assets/izH7Sl3AdDqWEUu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GYee/CV_interviews_Q-A/4f1974bd2414fc3e117010b2212e5d97f760a26e/图像处理/01_LBP算法原理.assets/izH7Sl3AdDqWEUu.png -------------------------------------------------------------------------------- /图像处理/01_LBP算法原理.md: -------------------------------------------------------------------------------- 1 | ## 问题 2 | 3 | LBP是一种常见的特征描述算法,用来提取局部的纹理特征,其原理其实很简单,下面我们就来看看它是怎么一回事吧。 4 | 5 | ## LBP简介 6 | 7 | LBP(Local Binary Patterns,局部二值模式)是一种很简单但很高效的局部纹理特征描述算子,于1994年由T. Ojala, M. Pietikäinen和D. Harwood提出,经过后续的改进,LBP特征具有**旋转不变性**和**灰度不变性**,在机器视觉领域中得到了广泛的应用,如人脸识别、指纹识别、光学字符识别以及车牌识别等。 8 | 9 | ## LBP算子的优缺点 10 | 11 | 参考: https://blog.csdn.net/lk3030/article/details/84034963 12 | 13 | 优点: 14 | 15 | - 一定程度上消除了光照变化的问题 16 | - 具有旋转不变性 17 | - 纹理特征维度低,计算速度快 18 | 19 | 缺点: 20 | 21 | - 当光照变化不均匀时,各像素间的大小关系被破坏,对应的LBP算子也就发生了变化。 22 | - 通过引入旋转不变的定义,使LBP算子更具鲁棒性。但这也使得LBP算子丢失了方向信息。 23 | 24 | ## LBP特征描述原理 25 | 26 | LBP是一种局部特征描述算子,最原始的LBP算子使用大小为3×3的窗口,将窗口中心邻域的8个像素分别与窗口中心像素其进行比较,邻域像素值大于中心像素值的位置标记为1,否则标记为0,从而得到一个8位的二进制值,将该值作为该窗口中心像素的LBP值(通常将8位的二进制值转换成十进制表示,即有256种可能的LBP值)。 27 | 28 | 如下图所示,可以更直观地理解LBP特征描述的思想(以窗口中心左上角的像素作为LBP值起始位): 29 | 30 | ![img](01_LBP算法原理.assets/HAfcpDgYmkdnsK1.png) 31 | 32 | > LBP的原理的确很简单吧?简单归简单,这显然还不能完全满足我们实际应用的需求,比如窗口大小的调整、旋转不变性等等,于是就有了以下一些改进的版本。 33 | 34 | ## 圆形LBP算子 35 | 36 | 原始的LBP算子的一个**缺陷**在于它只覆盖了一个固定半径范围内的小区域,这显然不能满足不同尺寸和频率纹理的需要。为了**适应不同尺度的纹理特征,并达到灰度和旋转不变性**的要求,Ojala等对LBP算子做了以下改进: 37 | 38 | 1. 将3×3邻域扩展到任意邻域 39 | 2. 用圆形邻域代替了正方形邻域 40 | 41 | 改进后的LBP算子允许在半径为 R 的圆形邻域内有任意多个像素点,从而得到了诸如半径为R的圆形区域内含有P个采样点的LBP算子。下图给出三种不同半径大小的圆形LBP特征描述算子: 42 | 43 | ![img](01_LBP算法原理.assets/OkCFW9vpgAStKT1.png) 44 | 45 | ## 旋转不变LBP算子 46 | 47 | 从LBP的定义可以看出,LBP算子是灰度不变的,但却不是旋转不变的。图像的旋转就会得到不同的LBP值。Maenpaa等人又将LBP算子进行了扩展,提出了具有旋转不变性的局部二值模式算子,即**不断旋转圆形邻域得到一系列初始定义的LBP值,取其最小值作为该邻域的LBP值**。 48 | 49 | 如下图所示,对于以下8种不同旋转角度的LBP值特征,它们的LBP值均为15(即最小值,其二进制表示为00001111),因此图像的LBP特征就具有了旋转不变性。 50 | 51 | ![img](01_LBP算法原理.assets/20140221184735734) 52 | 53 | ## LBP等价模式 54 | 55 | 考察LBP算子的定义可知,一个**LBP**算子可以产生多种二进制模式(p个采样点)如:3x3邻域有p=8个采样点,则可得到**2^8=256**种二进制模式;5x5邻域有p=24个采样点,则可得到**2^24=16777216**种二进制模式,以此类推......。显然,过多的二进制模式无论对于纹理的提取还是纹理的识别、分类及信息存取都是不利的,在实际应用中不仅要求采用的算子尽量简单,同时也要考虑到计算速度、存储量大小等问题。因此需要对原始的**LBP**模式进行**降维**。 56 | 57 | 为了解决上述二进制模式过多的问题,提高统计性,Ojala等人提出了采用一种**“等价模式”(Uniform Pattern)**来对LBP算子的模式种类进行降维。**其主要思想是**:对于某些代表了图像的边缘、斑点、角点等信息的LBP模式,其具有较好的特征区分度,我们不做降维,这些LBP模式归为**等价模式类**;而对于其他没什么代表性的LBP模式,我们不太关心,统一归为一种类型,叫做**剩余模式(混合模式)** 。 58 | 59 | ![img](01_LBP算法原理.assets/20150805105836031) 60 | 61 | 那么什么样的LBP模式具有较好的区分度,能够称为**等价模式**呢?Ojala等人认为,在实际图像中,绝大多数LBP模式**最多只包含两次从1到0或从0到1的跳变**。于是将等价模式定义为:**当某个LBP所对应的循环二进制数从0到1或从1到0最多有两次跳变时,该LBP所对应的二进制就称为一个等价模式类**。 62 | 63 | > 例如如00000000(0次跳变),00000111(只含一次从0到1的跳变),10001111(先由1跳到0,再由0跳到1,共两次跳变)都是等价模式类。除等价模式类以外的模式都归为另一类,称为混合模式类,例如10010111(共四次跳变) 64 | > 65 | > **判断**一个二进制模式是否为**等价模式**最简单**的办法**就是将LBP值与其循环移动一位后的值进行按位相与,计算得到的二进制数中1的个数,若个数小于或等于2,则是等价模式;否则,不是。 66 | 67 | 通过这种改进,二进制模式的种类大大减少,而不会丢失任何信息,模式种类由原来的$2^{n}$减少为$n*(n-1)+2+1$种,这使得特征向量的维数更少,并且可以减少高频噪声带来的影响。 其中,n表示邻域集内的采样点数,共有$n*(n-1)+2$种**等价模式**和1种**剩余模式(混合模式)**。 68 | 69 | > 等价模式种类的计算:由于等价模式中,所以为1的bit必须相邻,并且LBP模式是循环的(也就是收尾相连)。以n=8为例, 70 | > 71 | > 当有1个1时,有n种等价模式;(分别是10000000, 01000000, 00100000, ... , 00000001) 72 | > 73 | > 当有2个1时,有n种等价模式;(注意LBP是循环的,两个相邻的1移动到末尾后还可以再移动一次变成10000001) 74 | > 75 | > ... 76 | > 77 | > 当有n-1个1时,有n种等价模式; 78 | > 79 | > 另外再加上全0和全1这两种特殊的等价模式,共有(n-1)*n+2种等价模式,再加上其他统一归为一类的混合模式,所以降维后一共有(n-1)*n+3种LBP模式。 80 | 81 | ## LBP算子对均匀光照变化的鲁棒性 82 | 83 | 使用LBP算子提取特征,对于图像上的每个像素点都可以得到一个LBP值,这些LBP值也组成了一幅“图像”,如下图所示,上面为不同光照强度下的图像,下面为提取LBP特征后LBP值表示的图像,可以看出,**LBP特征提取对均匀的光照变化具有很强的鲁棒性**,因此这种算法在很多领域都得到了应用。 84 | 85 | ![img](01_LBP算法原理.assets/bzk3tnEWw5GjrC9.png) 86 | 87 | ## LBP特征检测步骤: 88 | 89 | 1. 将检测图像切分为若干个区块(cell,例如每个区块16x16像素)。 90 | 91 | 2. 对于每个cell中的一个像素,将相邻的8个像素的灰度值与其进行比较,若周围像素值大于中心像素值,则该像素点的位置被标记为1,否则为0。这样,3*3邻域内的8个点经比较可产生8位二进制数,即得到该窗口中心像素点的LBP值。 92 | 3. 然后计算每个cell的直方图,即每个LBP值出现的频率,然后对该直方图进行归一化处理。 93 | 4. 最后将得到的每个cell的统计直方图进行连接成为一个特征向量,也就是整幅图的LBP纹理特征向量。 94 | 95 | 然后就可以将该特征向量通过诸如SVM等机器学习算法来产生一个分类器进行分类任务了。 96 | 97 | > 补充: 98 | > 99 | > 假设一幅图像为128×128大小,可以将图像划分为8×8=64个cell,每个cell包含16×16=256个像素,然后对每个cell中的每个像素计算一个LBP值,即每个cell得到16×16个LBP值,接着对每个cell分别统计直方图并归一化,将所有cell直方图的结果连接起来就组成了整幅图像的特征向量。 100 | 101 | 102 | 103 | ## 参考资料 104 | 105 | [LBP原理加源码解析](https://blog.csdn.net/xidianzhimeng/article/details/19634573) https://blog.csdn.net/xidianzhimeng/article/details/19634573 106 | 107 | [Local Binary Patterns](http://www.scholarpedia.org/article/Local_Binary_Patterns) http://www.scholarpedia.org/article/Local_Binary_Patterns 108 | 109 | [lbp特征提取(等价模式)](https://www.cnblogs.com/xqy1205/p/7729572.html) -------------------------------------------------------------------------------- /图像处理/02_HOG算法原理.md: -------------------------------------------------------------------------------- 1 | ## 问题 2 | 3 | 在深度学习目标检测算法问世之前,HOG+SVM可以说是最经典的传统目标检测算法了,既然是经典,那自然需要了解了解对吧。所以今天我们就来看一看,HOG是如何进行特征提取的。 4 | 5 | ## HOG简介 6 | 7 | HOG(Histogram of Oriented Gradient,即方向梯度直方图)是应用在计算机视觉和图像处理领域,用于目标检测的特征描述子,由Navneet Dalal和Bill Triggs等人在2005年CVPR发表的论文上提出。HOG通过计算和统计图像局部区域的梯度方向直方图来构建特征,结合SVM等分类器常用于图像识别等领域,尤其在行人检测中具有非常不错的表现。 8 | 9 | ## HOG特征描述原理 10 | 11 | **● 主要思想:** 12 | 13 | 在一幅图像中,局部目标的表象和形状(appearance and shape)能够被梯度或边缘的方向密度分布很好地描述。(本质:梯度的统计信息,而梯度主要存在于边缘的地方)。 14 | 15 | **● 具体操作:** 16 | 17 | 先将图像划分为若干个cell,然后计算各个cell中每个像素点的梯度和边缘方向,然后分别统计每个cell的方向梯度直方图,构成了每个cell的特征描述子; 18 | 19 | 为了对光照和阴影有更好的不变性,需要对直方图进行对比度归一化,具体可以通过将局部直方图在图像更大的范围内(称为block)进行对比度归一化。组合每个block中归一化后的所有cell的descriptor,就构成了该block的descriptor。 20 | 21 | 最后将所有block的descriptor组合起来就是整幅图像的HOG特征描述子。 22 | 23 | ## HOG特征检测步骤 24 | 25 | 1. 计算图像每个像素点的梯度大小和方向 26 | 2. 将图像划分成若干个cell 27 | 3. 统计每个cell的方向梯度直方图 28 | 4. 将相邻的若干个cell组成更大的block 29 | 5. 对每个block的进行对比度归一化 30 | 6. 将归一化后的所有block特征描述子组合在一起,构成整幅图像的descriptor 31 | 32 | 然后就可以将整幅图像的HOG特征向量结合SVM等机器学习算法进行训练,实现目标检测任务了。 33 | 34 | > 通过上面的介绍,想必对HOG的原理已经有了大致的理解了,但对于过程中每一步具体怎么操作的是不是还有点模糊呢?不着急,我们接着往下看。。。 35 | 36 | ## HOG算法具体实现 37 | 38 | > 注意,有些文章或博客中HOG算法实现步骤中可能给出第一步是先进行图像预处理,如Gamma校正,即对图像的颜色空间进行归一化,其目的是调节图像的对比度,降低图像局部阴影和光照的影响,同时可以抑制噪声。但后来作者指出这一步其实可以省略掉,因为后面步骤中的block归一化处理能达到同样的效果,而图像预处理的贡献微薄,所以第一步可以直接计算梯度和方向。 39 | 40 | #### 1.梯度计算 41 | 42 | 计算图像上每个像素点的梯度大小和方向: 43 | $$ 44 | 水平方向梯度:\quad G_{x}(x,y) = I(x+1,y)-I(x-1,y)\\ 45 | 垂直方向梯度:\quad G_{y}(x,y) = I(x,y+1)-I(x,y-1)\\ 46 | 每个像素点在(x,y)处的梯度幅值和方向分别为:\qquad \\ 47 | 幅值: G(x,y) = \sqrt {G_{x}(x,y)^{2}+G_{y}(x,y)^2}\\ 48 | 方向: \alpha = arctan\frac{G_{y}(x,y)}{G_{x}(x,y)} \qquad \qquad \qquad 49 | $$ 50 | 51 | #### 2.计算梯度方向直方图 52 | 53 | 首先将图像划分为若干个cell,大小可以自由设定,例如将整幅图像划分为8×8个cell,每个cell都包含若干个像素。 54 | 55 | 然后分别统计每个cell的方向梯度直方图,大量的实验表明,统计直方图时采用9个bin来统计方向信息效果较好。具体来说就是,将cell的梯度方向 `0~180°`(或 `0~360°`,即考虑了正负)分成9个方向块,如下图所示: 56 | 57 | ![](https://i.loli.net/2020/06/25/Lasjh6PgRdKSAN9.png) 58 | 59 | 在直方图中,每 `20°` 组成一个bin,当梯度方向落在某个bin范围中,该bin上的计数就增加(可以理解为按照梯度方向来进行投影的过程)。那相应的bin计数应该增加多少呢?Dalal和Triggs等人认为,将梯度幅值大小作为投影权值效果通常较好。也就是说,假设某个像素点的梯度大小为3,其梯度方向刚好落在 `20~40°`之间,那么该bin上的计数就增加3(也就是将梯度大小作为权值用来计数)。通过这样的一 一映射,对于每个cell,都可以映射成9个bin的方向梯度直方图,即每个cell都是9维的descriptor。 60 | 61 | #### 3.block归一化 62 | 63 | 由于局部光照的变化,以及前景背景对比度的变化,会使得梯度强度的变化范围非常大,因此需要对梯度做局部对比度归一化。归一化能够进一步对光照、阴影、边缘进行压缩,使得特征向量对光照、阴影和边缘变化具有鲁棒性。 64 | 65 | 具体如何操作? 66 | 67 | 作者的做法是:将多个cell组成更大的block,然后对每个block进行对比度归一化。图像最终的HOG特征描述子就是所有block内的cell的直方图构成的向量。事实上,块之间是有重叠的,也就是说,每个细胞单元的直方图都会被多次用于最终的描述子的计算。块之间的重叠看起来有冗余,但可以显著的提升性能 。 68 | 69 | ![](https://i.loli.net/2020/06/26/XCeHTDhs65YmiUb.png) 70 | 71 | 如上图所示,每个block由2×2个cell组成,每个cell包含8×8个像素,每个cell得到9个方向的bin,因此每个block的特征向量维度为2×2×9=36维。 72 | 73 | 作者采用的归一化方法有4种:($||v||_{k}$表示 $v$ 的 $k$ 阶范数,$e$ 是一个很小的常数) 74 | $$ 75 | L2-norm:\quad f=\frac{v}{\sqrt{||v||_{2}^{2}+e^{2}}}\\ 76 | L2-hys: \quad 先计算L2范数,然后限制v的最大值为0.2,再进行归一化\\ 77 | L1-norm:\quad f=\frac{v}{||v||_{1}+e^{2}}\\ 78 | L1-sqrt:\quad f=\sqrt{\frac{v}{||v||_{1}+e^{2}}} 79 | $$ 80 | 81 | #### 4.提取HOG特征 82 | 83 | 最后一步就是将归一化后所有的block进行HOG特征的提取,并将它们结合成最终的特征向量送入分类器。 84 | 85 | 那一张图像的HOG特征维度是多少呢? 86 | 87 | 例如,一张大小为64×128的图像,每个block由2×2个cell组成,每个cell包含8×8个像素,每个cell计算9个bin的方向梯度直方图,以8个像素为步长,则水平方向有15个扫描窗口,垂直方向有7个扫描窗口,所以整幅图像的HOG特征维度为15×7×2×2×9=3780维。 88 | 89 | 90 | 91 | ## 参考资料 92 | 93 | [目标检测的图像特征提取之(一)HOG特征](https://blog.csdn.net/zouxy09/article/details/7929348) https://blog.csdn.net/zouxy09/article/details/7929348 94 | 95 | [第十八节、基于传统图像处理的目标检测与识别(HOG+SVM附代码)](https://www.cnblogs.com/zyly/p/9651261.html) https://www.cnblogs.com/zyly/p/9651261.html -------------------------------------------------------------------------------- /图像处理/03_FAST、BRIEF、ORB算法原理.md: -------------------------------------------------------------------------------- 1 | ## 问题 2 | 3 | 今天整理整理FAST、BRIEF、ORB算法的原理。 4 | 5 | 6 | 7 | ## FAST算法 8 | 9 | #### 一、FAST简介 10 | 11 | FAST(features from accelerated segment test)是一种角点检测算法,可以用于提取特征点,后来也长用于目标跟踪等计算机视觉任务中。FAST角点检测算法最初由 Edward Rosten 和 Tom Drummond 提出,并于2006年正式发表。如名字所示,FAST算法最大的优势就是计算效率,相比于其他特征检测算法(例如SIFT、SUSAN、Harris和DOG等)更加快速。此外,通过应用机器学习方法,FAST可以在计算时间和资源等方面得到进一步的性能提升。由于快速高效的性能,FAST角点检测算法非常适合实时视频处理应用。 12 | 13 | #### 二、FAST算法原理 14 | 15 | FAST角点检测算法主要考虑像素点邻域的圆形窗口上的16个像素。如下图所示,作者认为,以像素 `p` 为中心的周围圆环上的16个像素中,如果有连续 `n` 个像素点的灰度值都比 `p` 点的灰度值大或都小,则认为 `p` 是一个角点。 16 | 17 | ![](https://i.loli.net/2020/06/27/K1mbWacvpxt5JVG.jpg) 18 | 19 | 实际上,在比较像素灰度值时,需要加上一个阈值 `t`。 20 | $$ 21 | S_{p \rightarrow x} = \begin{cases} d, \quad I_{p \rightarrow x} \leq I_{p}-t \quad (darker)\\ 22 | s, \quad I_{p}-t < I_{p \rightarrow x} < I_{p}+t \quad (similar)\\ 23 | b, \quad I_{p}-t \leq I_{p \rightarrow x} \quad (brighter) \end{cases}\\ 24 | 其中,p \rightarrow x\ 表示像素点p周围圆环上的像素点x,(x=1,2,...,16),S_{p \rightarrow x}表示p点灰度对应的区间类别 25 | $$ 26 | 也就是说,如果 `p` 点邻域有连续 `n` 个点比较的结果为 $S_{p \rightarrow x} = d \ 或 \ b$,则认为 `p` 是角点。一般情况下, `n` 取12,称为FAST-12;实际中, `n=9` 的效果会更好一些。 27 | 28 | 由上面的分析可知,对于图像上的每个像素点,我们都需要遍历其邻域圆环上的16个像素点。实际的运用中,我们可以采用一种更加高效的检测方法,只需要检测第1,9,5和13四个位置的像素,就可以剔除掉一大部分非角点的像素。具体的做法是: 29 | 30 | 先检测第1和第9个像素,如果它们都与中心像素点 `p` 相似(即 $S_{p \rightarrow x} = s$),说明点 `p` 不是角点;否则继续检测第5和第13个像素点,如果这4个像素点中至少有3个像素点不相似($S_{p \rightarrow x} = d \ 或 \ b$),则可以认为点 `p` 是角点,否则 `p` 肯定不是角点。 31 | 32 | 33 | 34 | > 如果你已经理清楚了上面的4点检测思想,可以跳过下面这段补充理解: 35 | > 36 | > 1. 首先,上面的4点检测方法是针对 n=12 而言的,对于 n=9 不适用 37 | > 2. 另外,对于 n=12 的情况,即需要保证连续12个像素点的灰度值均大于或小于中心像素点才是角点。因此,如果第1和第9个像素都与中心像素相似,则无论如何都不可能保证连续12个像素满足角点的条件,即此时的`p`不可能是角点; 38 | > 3. 若继续考虑第5和第13个像素(即1,5,9,13一起考虑),如果不能满足至少3个像素与 `p` 不相似,显然 `p` 就无法保证连续12个点与它不相似的条件, `p` 就肯定不是角点; 39 | > 4. 否则,可以认为 `p` 是一个角点。事实上这是一种粗略的角点筛选,检测这4个点只能保证剔除不是角点的像素,还无法保证这个点就是角点。如果想要提高检测精度,可以继续检测完全部的16个像素,毕竟在剔除大部分非角点像素后,即使再检测完16个像素的计算量也会比原来少了很多。 40 | 41 | 42 | 43 | 上述方法的缺点: 44 | 45 | 1. 当 `n<12` 时,上述的高效检测方法不再适用 46 | 2. 检测的效率依赖于选择的4个点的检测顺序和角点附近的分布 47 | 3. 多个特征点检测的结果可能彼此相邻(可以通过非极大值抑制来解决) 48 | 49 | #### 三、非极大值抑制 50 | 51 | 1. 计算每个特征点的score(定义为中心像素与其周围16个像素点灰度值的差的绝对值之和) 52 | 2. 对于相邻的特征点,比较它们的score值,对score值较小的特征点会被删除 53 | 54 | #### 四、FAST算法总结 55 | 56 | 1. FAST角点检测算法最大的特点就是速度快 57 | 2. FAST检测角点的数量依赖于一个 `t` 值,需要人为设置, `t` 越大,检测到的角点数量越少 58 | 3. 受图像噪声影响大 59 | 4. FAST没有尺度不变性和旋转不变性 60 | 61 | 62 | 63 | 64 | 65 | ## BRIEF算法 66 | 67 | #### 一、BRIEF简介 68 | 69 | BRIEF(Binary Robust Independent Elementary Features)是一种特征描述子,而不是一种特征提取算法,因此不同于Harris、FAST、SIFT、SURF等特征检测算法。BRIEF算法由Michael Calonder等人在ECCV2010上上提出,采用了二进制串来描述特征,相比于SIFT的浮点型描述子而言,BRIEF描述子生成的速度更快,且其特征匹配速度也大大提升。 70 | 71 | #### 二、BRIEF算法原理 72 | 73 | BRIEF算法的主要思想是,在特征点邻域的 `s×s` 窗口内随机选取 `n` 个点对,通过比较这些点对来生成一个二进制串作为该特征点的特征描述子。 74 | 75 | > 注意:BRIEF只是特征描述子,特征点的提取要靠别的特征提取算法来完成,如FAST等。 76 | 77 | **具体的步骤:** 78 | 79 | 1. 先对整幅图像提取特征点 80 | 81 | 2. 为了减少噪声影响,对图像进行高斯滤波(因为BRIEF随机选取点对,对噪声比较敏感) 82 | 83 | 3. 对于某个特征点,在其 `s×s` 的邻域内随机选取一个点对,比较两个点的大小: 84 | $$ 85 | T(x,y)=\begin{cases} 1, \quad if\ p(x) 双线性插值 > 三次插值 81 | 82 | ## 参考资料 83 | 84 | [关于上采样方法总结(插值和深度学习)](https://blog.csdn.net/qq_34919792/article/details/102697817) 85 | [常用的几种图像插值算法](https://blog.csdn.net/Du_Shuang/article/details/82463502) 86 | 87 | -------------------------------------------------------------------------------- /图像处理/14_开操作与闭操作.md: -------------------------------------------------------------------------------- 1 | ## 问题 2 | 3 | 形态学中的膨胀、腐蚀、开操作和闭操作是图像处理中常见的问题之一,因为自己偶尔会记混“开操作和闭操作”的概念和应用,所以整理一下当作复习复习。 4 | 5 | 形态学中最基本的操作是膨胀和腐蚀,它们能实现很多功能:例如消除噪声、分割出独立的图像元素以及在图像中连接相邻的元素等。 6 | 7 | ## 膨胀 8 | 9 | ● 膨胀是指使用卷积核B(可以理解为**模板**)对图像A(或者某部分区域)进行卷积操作,卷积核可以是任意形状或大小。 10 | 11 | ● 卷积核B通常有一个自定义的参考点(anchor point),一般位于中心位置。 12 | 13 | ● **膨胀是求局部最大值的操作**。当卷积核B(模板)扫描图像A与其进行卷积操作时,计算模板B覆盖的区域的最大值并将最大值赋给模板的参考点。因为图像中亮点的灰度值大,所以**膨胀操作会使得图像中的高亮区域逐渐增长**。 14 | 15 | ![](https://i.loli.net/2020/06/03/owLYI9iyFGJphdR.png) 16 | 17 | ![](https://i.loli.net/2020/06/03/DC1tb489cp7WPEf.png) 18 | 19 | 如上图所示,膨胀操作后图像中亮的区域会增长。 20 | 21 | ## 腐蚀 22 | 23 | ● 腐蚀是膨胀的反操作,**腐蚀计算的是局部区域的最小值**。 24 | 25 | ● 将卷积核B与图像A进行卷积,将B所覆盖区域的最小值赋给参考点。 26 | 27 | ● **腐蚀操作会使得图像中亮的区域变小,暗的区域变大**。 28 | 29 | ![](https://i.loli.net/2020/06/03/HSVMjEtmPY4fLhy.png) 30 | 31 | ![](https://i.loli.net/2020/06/03/gPhQo5rITuZ37bc.png) 32 | 33 | ## 开操作 34 | 35 | ● 开操作是指:**先腐蚀,在膨胀**。 36 | 37 | ● 开操作**可以移除较小的明亮区域、在较细的地方分离物体**。(假设小物体是亮色,关注的前景是黑色,即可以移除小亮点) 38 | 39 | ● 应用例子:通过开操作将阈值处理后的细胞分离,可以更清晰地统计细胞数目 40 | 41 | > 下面给出一个开操作示例,图片来自第2篇参考博客:https://blog.csdn.net/Fishmemory/article/details/53113746?locationNum=6&fps=1 42 | 43 | ![](https://i.loli.net/2020/06/03/E3ICvQBYpzgqAdr.png) 44 | 45 | 46 | 47 | ## 闭操作 48 | 49 | ● 闭操作是指:**先膨胀,再腐蚀**。 50 | 51 | ● 闭操作**可以填充物体内的细小空洞、连接邻近的明亮物体**。 52 | 53 | > 下面给出一个闭操作示例,图片来自第2篇参考博客:https://blog.csdn.net/Fishmemory/article/details/53113746?locationNum=6&fps=1 54 | 55 | ![](https://i.loli.net/2020/06/03/8ijnCJsogkpA19N.png) 56 | 57 | 58 | 59 | ## 顶帽操作 60 | 61 | ● 顶帽操作是指:原图与开操作的差(dst = src - open) 62 | 63 | ● 效果:局部亮度极大点被分割出来(可以分两步理解,开操作移除了明亮的小区域,当用原图减去开操作的结果之后,之前被移除的明亮区域就会凸显出来,因此看到的效果就是一些亮度较大的小区域) 64 | 65 | > 下面图片摘自第3篇参考博客:https://blog.csdn.net/qq_36387683/article/details/80489631 66 | 67 | ![](https://i.loli.net/2020/06/03/Um5KxXgGqscl24d.png) 68 | 69 | 70 | 71 | ## 黑帽操作 72 | 73 | ● 黑帽操作是指:闭操作与原图的差(dst = close - src) 74 | 75 | ● 效果:局部黑色的洞被分割出来 76 | 77 | > 下面图片摘自第3篇参考博客:https://blog.csdn.net/qq_36387683/article/details/80489631 78 | 79 | ![](https://i.loli.net/2020/06/03/lFKEfk5crqG1wiu.png) 80 | 81 | 82 | 83 | ## 参考资料 84 | 85 | [形态学在图像处理中的应用](https://blog.csdn.net/sn_gis/article/details/57414029#%E8%85%90%E8%9A%80%E5%92%8C%E8%86%A8%E8%83%80) 86 | 87 | [形态学-腐蚀、膨胀、开操作、闭操作](https://blog.csdn.net/Fishmemory/article/details/53113746?locationNum=6&fps=1) 88 | 89 | [OpenCV学习笔记-顶帽、黑帽、形态学梯度](https://blog.csdn.net/qq_36387683/article/details/80489631) 90 | 91 | 《学习OpenCV3》 -------------------------------------------------------------------------------- /图像处理/15_Hough变换检测直线与圆的原理.md: -------------------------------------------------------------------------------- 1 | ## 霍夫变换的基本原理 2 | 3 | 霍夫变换(Hough Transform)可以理解为图像处理中的一种特征提取技术,通过投票算法检测具有特定形状的物体。霍夫变换运用**两个坐标空间**之间的变换将在一个空间中具有相同形状的曲线或直线映射到另一个坐标空间中的一个点形成峰值,从而把检测任意形状的问题转化为统计峰值问题。(把这句话背下来吧) 4 | 5 | ## 直线检测 6 | 7 | ### 背景知识 8 | 9 | 高中的时候我们都学过笛卡尔坐标系 (x, y) 也就是直角坐标系,在这个坐标系中可以有很多种方法来表示一条直线,例如点斜式 $y - b = k*(x - a)$ 或者两点式 $y - y_2 = \frac{y_1 - y_2}{x_1 - x_2}*(x - x_2)$ ,但是在霍夫变换中我们使用的是另外一种表示方法即极坐标系,使用两个变量 $(r, \theta)$ 来表示一条直线,具体地 $r$ 为一条直线到原点的距离,$\theta$ 为该直线的垂线与 x 轴的夹角。 10 | 11 | img 12 | 13 | ### hough变换检测直线的流程 14 | 15 | 怎么来检测一条直线呢?我们可以取需要检测的图像上的 N 个点,为每个点假设 n 个方向的直线(空间中经过一个点的直线有无数条),通常 n = 180 ,分别计算这 n 条直线的 $(r, \theta)$ 坐标,既可以得到 n 个坐标,N 个点就是 N*n 个 $(r, \theta)$ 坐标。 16 | 17 | 那么如果有多个点在一条直线上,那么这几个点在 $\theta$ 的某个取值 $\theta_i$ 处的 $r_i$ 相等,如果图中只有这条直线的话,那么 $\theta = \theta_i$ 时的 $r_i $ 的统计量是最多的,找到这个 $\theta_i$ 就意味着找到了这条直线。 18 | 19 | 拓展到一张图中有多条直线的情况,我们可以设置一个阈值来选择前 k 个统计量的 $(r_i, \theta_i)$ 来检测 k 条直线。当然还有相邻两条直线的拼接或者长度要超过某个阈值的直线才需要输出的问题,这些就要看怎么去实现了,不是我们这篇文章的要点,这些功能在 opencv 中都有实现,需要的可以去了解。 20 | 21 | 下面拿一个博客中的例子来说明: 22 | 23 | ![img](https://i.loli.net/2020/05/18/dieU4B871rtCPRM.jpg) 24 | 25 | 如果空间中有3个点,如何判断这三个点在不在一个直线上?这个例子中,对于每个点均求过该点的6条直线的 $(r,\theta)$ 坐标,共求了3*6个$(r,\theta)$ 坐标。可以发现在 $\theta = 60$ 时,三个点的 r 都近似为80.7,由此可判定这三个点都在直线(80.7,60)上。 26 | 27 | 通过 $(r, \theta)$ 坐标系可以更直观表示这种关系,如果对于一个给定点 $(x_0,y_0)$ ,我们在极坐标系下画出经过该点的所有直线的极坐标表示,将得到一条正弦曲线。下图是直角坐标系下的三个点在极坐标系下绘出的图:图中三个点的 $(r, \theta)$ 曲线汇集在一起,该交点就是同时经过这三个点的直线。 28 | 29 | ![img](https://i.loli.net/2020/05/18/2oUrP4SNmYCyZWw.jpg) 30 | 31 | ### hough变换检测圆的流程 32 | 33 | 检测直线的时候我们使用 $(r, \theta)$ 来表示一条直线,在检测圆时我们是使用 $(a,b,r)$ 来表示确定一个圆心为 $(a,b)$ 半径为 $r$ 的圆。 34 | 35 | 当某个圆过点 $(x,y)$ 时,满足 $(x - a)^2 + (y - b) = r^2$ ,所以过点 $(x,y)$ 的所有圆可以表示为 $(a^{(i)},b^{(i)}, r^{(i)})$ 。与上面检测直线的原理一样,要检测三个点是否在同一个圆上,首先计算过第一个点的所有圆表示为 $(a_1^{(i)},b_1^{(i)}, r_1^{(i)})$ ,过第二个点的所有圆表示为 $(a_2^{(i)},b_2^{(i)}, r_2^{(i)})$ ,过第三个点的所有圆表示为 $(a_3^{(i)},b_3^{(i)}, r_3^{(i)})$ ,如果这三个点在同一个圆上,那么存在一个值 $(a_k, b_k, r_k)$ 是三个点的所有圆表示中都存在的,也就是说这三个点同时在圆 $(a_k, b_k, r_k)$ 上。 36 | 37 | 下面借助这张图来形象理解: 38 | 39 | 先说说怎么表示过点 $(x_1, y_1)$ 的所有圆的表示。当 $r_1$ 确定时,根据 $(x_1 - a_1)^2 + (y_1 - b_1) = r_1^2 $ ,即 $(a_1,b_1)$ 的轨迹则变成了以 $(x_1, y_1)$ 为圆心 $r_1$ 为半径的圆,取完所有的 $r_1^i$ ,$(a_1^{(i)},b_1^{(i)}, r_1^{(i)})$ 的轨迹便如图中的一个圆锥,三个点的所有圆表示形成的圆锥的交点 A 所对应的那个 $(a_k, b_k, r_k)$ 便是经过这三个点的圆。![img](https://i.loli.net/2020/05/18/EJeyXO5zkruFtTq.jpg) 40 | 41 | 椭圆或者其他形状同理,只要找到一种表示即可将在一个空间中的形状问题变换到另一个空间的点统计问题。 42 | 43 | ## 拓展 44 | 45 | 图像处理中,在使用霍夫线变换之前,要首先对图像进行边缘检测,即霍夫线变换的直接输入只能是边缘二值图像。 46 | 47 | ## 参考资料 48 | 49 | [Hough变换检测原理](https://blog.csdn.net/shenziheng1/article/details/75307410) -------------------------------------------------------------------------------- /图像处理/17_图像拼接原理介绍.md: -------------------------------------------------------------------------------- 1 | ## 问题 2 | 3 | 图像拼接是图像处理的基础之一,虽然自己并没有直接做图像拼接方面的研究,但在面试中却多次被问到这方面的内容,可见这个知识点还是很重要的。事实上,很多场景都会用到图像拼接的知识,例如运动检测与跟踪、游戏画面的重建等。 4 | 5 | > 本文参考自公众号“计算机视觉life”(一个很不错的CV相关的公众号,有兴趣可以关注关注)关于退昂拼接的一篇文章https://mp.weixin.qq.com/s/sHv-VOnn9WHQvq4hPOLgrA。这里主要关注在图像拼接的原理部分,所以对部分内容稍作删减和补充,主要介绍计算单应性矩阵H以及图像变形和融合。 6 | 7 | 8 | 9 | ## 图像拼接介绍 10 | 11 | 图像拼接(image mosaic)是将同一场景中的两张或多张重叠图像拼接成一张更大图像的技术,在机器视觉、医学成像等多个领域有着广泛的应用。常见的图像拼接算法流程如下: 12 | 13 | ![](https://i.loli.net/2020/06/07/dKB79mlYUgONAiW.png) 14 | 15 | 下面简单梳理一下上面步骤中涉及到的一些基本概念: 16 | 17 | **● 特征提取:** 即提取输入图像中的特征,例如角点、边缘等信息。 18 | 19 | **● 图像配准:** 即将同一目标场景下的两张或多张图像在空间位置上对准。 20 | 21 | **● RANSAC:** 随机抽样一致性算法,用来剔除异常值(误匹配点)。 22 | 23 | **● 单应性矩阵:** 即同一场景中两幅图像的空间映射关系。 24 | 25 | **● 图像变形:** 将其中一幅图像进行重投影(可由单应性矩阵实现),并将图像放置在更大的画布上(拼接在一起)。 26 | 27 | **● 图像融合:** 处理拼接边界处的灰度,使两张图像拼接后平滑过渡。 28 | 29 | 30 | 31 | ## 1.特征提取(Feature Extraction) 32 | 33 | 特征提取的主要目的是为后面进行图像配准而服务的,因此特征提取算法也影响了图像配准的精度,常用的特征提取方法有: 34 | 35 | **● Harris角点检测算法** 36 | 37 | **● SIFT关键点检测算法** 38 | 39 | **● SURF角点检测算法** 40 | 41 | **● FAST角点检测算法** 42 | 43 | > 对于上述特征提取算法,这里不展开介绍,应该大多有过图像相关经验的都对这部分内容有一定的了解,如果有兴趣了解可以继续阅读参考资料。 44 | 45 | 46 | 47 | ## 2.图像配准(Image Registration) 48 | 49 | 图像配准是指将两张图像的特征点对应匹配起来,常见的图像配准方法有: 50 | 51 | **● NCC算法:** 归一化互相关(normalized cross correlation) 52 | 53 | > 归一化互相关算法的原理是计算第一幅图像每个点在周围窗口内的像素与另一幅图像每个点周围窗口内的像素之间的相关性,也就是窗口的灰度信息相关性,依次来衡量两幅图局部特征的相似度。 54 | > 55 | > 这种算法原理简单,只是要计算第一幅图像中的每个点与另一幅图像中同一行的每个点的互相关信息,计算量很大,所以也比较慢。当然,这可以改进,而且在提取特征点之后不需要对图像的每个点都遍历,可以只关注特征点附近的信息。 56 | 57 | **● SSD算法:** 差值平方和算法(sum of squared difference) 58 | 59 | > SSD算法和NCC思想差不多,都是局部的基于灰度信息的匹配算法,只不过在衡量相似性的方法上不同,NCC才用的是互相关信息,而SSD算法计算的是窗口内对应像素差值的平方和,其值越小相似性越高。 60 | 61 | 图像配准的方法还有很多,这里不作为重点介绍,了解即可,下面看看配准的效果图: 62 | 63 | ![](https://i.loli.net/2020/06/06/ZXH5ARo3pKQ6iPs.png) 64 | 65 | 66 | 67 | ## 3.计算单应性矩阵H 68 | 69 | 在介绍如何估计单应性矩阵之前,需要先采用RANSAC算法进行单应。 70 | 71 | ### 随机抽样一致性算法RANSAC(random sample consensus) 72 | 73 | **RANSAC算法从可能含有异常值的观测数据中拟合数学模型,是一种通过迭代方式估计模型参数的方法**。RANSAC是一种不确定算法,因为它只在一定的概率下产生一个合理的结果,若执行更多次的迭代,这个概率会增加。在存在大量可用数据分层的情况下,这个算法可以以鲁棒的方式拟合模型。RANSAC算法在计算机视觉中有许多应用。 74 | 75 | **● RANSAC的原理:** 76 | 77 | RANSAC算法在基于下面假设的情况下,**通过迭代的方式,直到某个参数模型的局内点数目最大,则认为该模型为最优模型**。 78 | 79 | **● RANSAC的基本假设:** 80 | 81 | ① 数据集中的**有效数据由“局内点”组成,可以估计模型参数** 82 | 83 | ② “局外点”是不能满足该模型的数据(其产生的原因有:噪声的极值、错误的测量方法、对数据的错误假设等) 84 | 85 | ③ 其余的数据属于噪声 86 | 87 | ![](https://i.loli.net/2020/06/06/fSGqJn1HuNoDbgW.png) 88 | 89 | **● RANSAC算法步骤:** 90 | 91 | 1. 随机选取N个数据(3个点对) 92 | 2. 估计参数 x(计算单应变换矩阵H) 93 | 3. 根于使用者设定的阈值,找到可用数据总数M中适合该模型向量 x 的数据对总数量 K( 计算每个匹配点经过单应变换矩阵后到对应匹配点的距离,根据预先设定的阈值将匹配点集合分为局内点和局外点,如果局内点足够多,则H足够合理,用所有内点重新估计H)。 94 | 4. 如果符合的数量K足够大,则接受该模型并退出,一次迭代结束 95 | 5. 重复1-4步骤 T次,退出 96 | 97 | K有多大取决于我们认为属于合适结构的数据的百分比以及图像中有多少结构。**如果存在多个结构,则在成功拟合后,删除拟合数据并重做RANSAC**。 98 | 99 | 在执行RANSAC之后,我们只能在图像中看到正确的匹配,因为**RANSAC找到了一个与大多数点相关的单应矩阵,并将不正确的匹配作为异常值丢弃**。 100 | 101 | 102 | 103 | ### 单应性矩阵(Homography) 104 | 105 | 有了两组相关点(即匹配的特征点),接下来就需要建立两组点的转换关系,也就是图像变换关系。 106 | 107 | ● 单应性是两个空间之间的映射,常用于表示同一场景的两个图像之间的对应关系; 108 | 109 | ● 这种对应关系可以匹配大部分相关的特征点,并且能实现图像投影,使一张图通过投影和另一张图实现大面积的重合。 110 | 111 | 假设两张图像的匹配点分别是 $X=[x,y]^{T}$ 和 $X'=[x',y']^{T}$,则两者的关系必须满足: $X'=HX$ 。如果使用齐次坐标表示这两个匹配点,则上述关系可以表示为: 112 | $$ 113 | \begin{pmatrix} x' \\ y' \\ 1 \end{pmatrix} = \begin{bmatrix} h_{11} & h_{12} & h_{13}\\ h_{21} & h_{22} & h_{23} \\ h_{31} & h_{32} & 1\end{bmatrix} \begin{pmatrix} x \\ y \\ 1 \end{pmatrix} 114 | $$ 115 | 可见,**单应性矩阵H是一个由8个参数组成的变换矩阵,因此,选取4对匹配点即可确定一个单应性矩阵H**。 116 | 117 | 118 | 119 | **用RANSAC方法估算H:** 120 | 121 | 1. 首先进行特征提取,检测两个图像的角点。 122 | 2. 采用NCC进行配准,收集相关性足够高的点对,形成一组候选匹配。 123 | 3. 选择4个点,计算单应性矩阵 H。 124 | 4. 选择与单应性一致的点配对,即计算由一幅图的匹配点经过H变换后与另一幅图原来的匹配点之间的距离,如果小于某个阈值:$Dist(P_{H},Q)<ε$,则点对 $(P,Q)$ 被认为与单应性H一致。 125 | 5. 重复3/4步骤,直到足够多的点对满足 H。 126 | 6. 使用找到的所有满足条件的点对,通过公式重新计算得到最终的 H。 127 | 128 | > 得到两幅图像之间的单应性矩阵H之后,就可以将某一幅图像通过单应性变换重投影到另一幅图像的平面上,实现拼接了,也就是下面的图像变形和融合。 129 | 130 | 131 | 132 | ## 4.图像变形和融合 133 | 134 | 最后一步是将所有输入图像变形并融合到一个符合的输出图像中。基本上,我们可以简单地将所有输入的图像变形到一个平面上,这个平面名为复合全景平面。 135 | 136 | #### 图像变形的步骤: 137 | 138 | 1. 首先计算每个输入图像的变形图像坐标范围,得到输出图像大小,可以很容易地通过映射每个源图像的四个角并且计算坐标 $(x,y)$ 的最小值和最大值确定输出图像的大小。最后,需要计算指定参考图像原点相对于输出全景图的偏移量 $x_{offset}$ 和偏移量 $y_{offset}$。 139 | 2. 下一步是使用上面所述的反向变形,将每个输入图像的像素映射到参考图像定义的平面上,分别执行点的正向变形和反向变形。(这里没看到推文中有提前的说明,我也没搞懂) 140 | 141 | ![](https://i.loli.net/2020/06/06/bxU3ArYJimMzP7c.png) 142 | 143 | 144 | 145 | #### 图像融合: 146 | 147 | 由于相机在拍摄时可能曝光不同,在图像拼接缝合处会出现一定的明暗差异,所以在融合过程中需要对这部分进行处理。最简单的可用处理方式是使用羽化(feathering),它使用加权平均颜色值融合重叠的像素。我们通常使用 `alpha `因子,通常称为 alpha 通道,它在中心像素处的值为1,在与边界像素线性递减后变为0。 148 | 149 | 图像融合结果:(右侧一小部分是原来图2拼接在图1后的结果) 150 | 151 | ![](https://i.loli.net/2020/06/06/xf25tURsQI6gMen.png) 152 | 153 | ![](https://i.loli.net/2020/06/06/Mdfp1xmPsDbK5w7.jpg) 154 | 155 | 156 | 157 | ## 补充 158 | 159 | 在图像拼接后,有时候可能会产生一些重影,例如:(这里直接给出拼接后的结果图) 160 | 161 | ![](https://i.loli.net/2020/06/06/WkoNjKMF7pfDCuJ.png) 162 | 163 | **出现重影可能的原因:** 164 | 165 | ① 拍摄方式:相机在拍摄图像时,曝光等参数设置不同可能影响图像后续处理的结果。 166 | 167 | ② 拍摄时间差:不同的时间拍摄同一场景,两张图像的重叠区域可能会有所不同,哪怕区别很微小。 168 | 169 | ③ 拍摄视差:由于拍摄过程中存在视差,导致拍摄的图像重叠区域可能不能完全重合。 170 | 171 | 不管何种方式导致的两张图像重叠区域不同,都有可能使得最终拼接的结果出现重影。 172 | 173 | 174 | 175 | **另外:**拍摄的图片必须要包含超过一定比例的重叠区域,当重叠区域过小时,图像会由于匹配的特征点对不足,而导致拼接失败。当重叠区域过大时,则需要多张图像进行拼接才能获取到更宽视野的图像。一般情况下,重叠区域占整幅图像的40%- -50%, 图像能够找到足够配准的特征点对。 176 | 177 | 178 | 179 | ## 参考资料 180 | 181 | 计算机视觉life公众号--图像拼接:https://mp.weixin.qq.com/s/sHv-VOnn9WHQvq4hPOLgrA 182 | 183 | [图像拼接处理](https://www.cnblogs.com/cenyan/p/12557065.html) https://www.cnblogs.com/cenyan/p/12557065.html 184 | 185 | -------------------------------------------------------------------------------- /图像处理/18_图像配准.md: -------------------------------------------------------------------------------- 1 | ## 问题 2 | 3 | 在我的一个传统图像处理项目中用到了图像配准技术,太久都忘了,为了防止面试被问到答不上来,这里还是要简要总结下。关于图像配准的概念,在另一个问题 “13_图像拼接原理介绍” 中也大体上介绍了一下,不过没那么详细。 4 | 5 | 随着技术的发展,图像配准已经有了深度学习的方法,但是我们这里讨论的还是传统的基于特征的方法。 6 | 7 | ## 图像配准流程 8 | 9 | > 假设我们要对一张参考图像和一张待配准图像之间进行图像配准,主要基于三个步骤:**关键点检测和特征描述**,**特征匹配**,**图像变形**。简而言之,我们在两幅图像中选择兴趣点,将参考图像中的每个兴趣点和它在待配准图像中的对应点关联起来,然后对待批准图像进行变换,这样两幅图像就得以对齐。 10 | 11 | ### 关键点检测和特征描述 12 | 13 | 关键点就是感兴趣的点。它定义了一幅图像中重要并且有特点的地方(如角,边等)。每个关键点都由一个描述子(包含关键点本质特点的特征向量)表征。描述子应该对图像变换(如位置变换、缩放变换、亮度变换等)是鲁棒的。很多算法都要执行关键点检测和特征描述,主流的关键点检测算法有: 14 | 15 | > SIFT(Scale-invariant feature transform,尺度不变的特征变换)是用于关键点检测的原始算法,但是它并不能免费地被用于商业用途。SIFT 特征描述子对均衡的缩放,方向、亮度变化是保持不变的,对仿射形变也是部分不变的。SURF(Speeded Up Robust Features,加速鲁棒特征)是受到 SIFT 深刻启发设计的检测器和描述子。与 SIFT 相比,它的运行速度要快好几倍。当然,它也是受专利保护的。ORB(定向的 FAST 和旋转的 BRIEF)是基于 FAST(Features from Accelerated Segment Test)关键点检测器和 BRIEF(Binary robust independent elementary features)描述子的组合的快速二值描述子,具有旋转不变性和对噪声的鲁棒性。它是由 OpenCV Lab 开发的高效、免费的 SIFT 替代方案。AKAZE(Accelerated-KAZE) 是 KAZE 的加速版本。它为非线性尺度空间提出了一种快速多尺度的特征检测和描述方法。它对于缩放和旋转也是具有不变性的,可以免费使用。 16 | 17 | 下面来介绍下 SIFT (Scale Invariant Feature Transform) 即尺度不变特征转换匹配算法。 18 | 19 | ### SIFT 20 | 21 | > SIFT算法的实质是在不同的尺度空间上查找关键点(特征点),并计算出关键点的方向。SIFT所查找到的关键点是一些十分突出,不会因光照,仿射变换和噪音等因素而变化的点,如角点、边缘点、暗区的亮点及亮区的暗点等。 22 | 23 | 广义上讲,整个过程可以分为四个部分: 24 | 25 | 1. **创建比例空间**:确保要素与比例无关 26 | 2. **关键点本地化**:确定合适的特征或关键点 27 | 3. **方向分配**:确保关键点是角度不变 28 | 4. **关键点描述符**:为每个关键点分配独一的指纹 29 | 30 | #### (1) 创建比例空间 31 | 32 | 使用高斯模糊技术(Gaussian Blur)来降低图像中的噪点。 33 | 34 | 因此,对于图像中的每个像素,高斯模糊技术会基于其相邻像素计算一个值。以下是应用高斯模糊之前和之后的图像示例。如图所示,纹理和次要细节将从图像中删除,并且仅保留诸如形状和边缘之类的相关信息: 35 | 36 | ![img](https://i.loli.net/2020/06/15/YVAGru5xtpUZT7w.png) 37 | 38 | 为了保证之后提取的特征与图像大小无关,需要创建“比例空间”,比例空间是从单个图像生成的具有不同比例的图像的集合。通常对原始图像进行四次缩放图像,并为每个缩放图像创建5个后续的模糊图像。 39 | 40 | ![img](https://i.loli.net/2020/06/15/H3dCblKxnD7EaVL.jpg) 41 | 42 | 接下来需要使用高斯差异(DoG)的技术来增强特征。DoG指的是在相同的比例尺下,前一个图像减去后一个图像,为每个octave创建另一组图像,每组图像剩下4个图像。 43 | 44 | ![img](https://i.loli.net/2020/06/15/N6LTyHYvtVUrSfz.jpg) 45 | 46 | #### (2) 关键点检测 47 | 48 | 创建图像后,下一步就是从图像中找到可用于特征匹配的重要关键点。即找到图像的局部最大值和最小值。分为两个步骤: 49 | 50 | 1. 找到局部最大值和最小值 51 | 2. 删除低对比度的关键点(关键点选择) 52 | 53 | 为了定位局部最大值和最小值,仔细检查图像中的每个像素,并将其与相邻像素进行比较。 54 | 55 | 当我说“邻近”时,它不仅包括该图像的周围像素(像素所在的尺度),还包括八度中上一张和下一张图像的九个像素。 56 | 57 | 这意味着将每个像素值与其他26个像素值进行比较,以确定是否为局部最大值/最小值。例如,在下图中,我们从第一个八度获得了三个图像。将标记为x的像素与相邻像素(绿色)进行比较,如果它是相邻像素中最高或最低的像素,则将其选择为关键点: 58 | 59 | ![img](https://i.loli.net/2020/06/15/e3AOhJrzDc4yGVx.png) 60 | 61 | 由此便找到了那些尺度不变形的关键点。 62 | 63 | 我们已经成功地生成了尺度不变的关键点。但是这些关键点中的一些可能对噪声没有鲁棒性。这就是为什么需要进行最终检查以确保我们拥有最准确的关键点来表示图像特征的原因。 64 | 65 | 因此,将消除对比度低或非常靠近边缘的关键点。 66 | 67 | 为了处理低对比度关键点,将为每个关键点计算二阶泰勒展开(second-order Taylor expansion)。如果结果值小于0.03(大小),则剔除该关键点。 68 | 69 | 那么,如何处理其余关键点呢?再次检查以确定位置不佳的关键点。这些是具有高边缘度但对少量噪点无鲁棒性的关键点。使用二阶Hessian矩阵来识别此类关键点。 70 | 71 | #### (3) 方向匹配 72 | 73 | 现在为每个关键点分配一个方向值以使旋转角度不变。再次将该步骤分为两个较小的步骤: 74 | 75 | 1. 计算幅度和方向 76 | 2. 创建大小和方向的柱状图 77 | 78 | 看下面的例图: 79 | 80 | ![img](https://pics5.baidu.com/feed/adaf2edda3cc7cd95413f370d1aee73ab90e9197.png?token=d6ccf5aa66a344cdc30ae411fb8a90da&s=9EAE7022E8DEC9EB4075A4CE010050E3) 81 | 82 | 假设要找到红色像素值的大小和方向。为此,通过提取55和46与56和42之间的差值来计算x和y方向上的梯度。得出的分别是 Gx = 9 和 Gy = 14 。 83 | 84 | 一旦有了梯度,就可以使用以下公式找到幅度和方向: 85 | 86 | $Magnitude = \sqrt{[(Gx)^2+(Gy)^2]} = 16.64$ 87 | 88 | $Φ = atan(\frac{Gy}{Gx}) = atan(1.55) =57.17$ 89 | 90 | 大小表示像素的强度,方向表示像素的方向。 91 | 92 | 现在,假设我们具有这些大小和方向值,就可以创建柱状图。 93 | 94 | 在x轴上,有一个角度值的区间,例如0-9、10 – 19、20-29,最大为360。假设现在角度值为57,它会落在第6个区间中。第6个bin值与像素的幅度成正比,即16.64。我们将对关键点周围的所有像素执行此操作。 95 | 96 | 这样就得到了下面的柱状图: 97 | 98 | ![img](https://pics4.baidu.com/feed/9213b07eca80653887c9bf127e726741ac3482c1.png?token=270a02f0b10ecc2897dd878ea54bee36&s=7D20347209267F0B0E5485C20300F0B3) 99 | 100 | #### (4) 关键信息描述符 101 | 102 | 这是SIFT的最后一步。到目前为止,我们有稳定的关键点——不变的比例以及旋转角度。在本部分中,我们将使用相邻像素,它们的方向和大小为该关键点生成一个唯一的指纹,称为“描述符”。 103 | 104 | 另外,由于我们使用周围的像素,因此描述符对于图像的照度或亮度部分不变。 105 | 106 | 首先在关键点周围采用16×16的邻域。将该16×16区域进一步划分为4×4子块,对于这些子块中的每一个小块,使用幅度和方向生成柱状图。 107 | 108 | ![img](https://i.loli.net/2020/06/15/Zifa4cIWUzrCvlu.jpg) 109 | 110 | 在此阶段,bin的大小增加,只占用8个bin(不是36个)。每一个箭头代表8个bin,箭头的长度定义了幅度。因此,每个关键点总共有128个bin值。 111 | 112 | 上面只是非常简略的描述,如果不是很熟悉的话根本理解不了,想要更加详细的了解请移步博客:[SIFT算法详解](https://blog.csdn.net/zddblog/article/details/7521424) 113 | 114 | ### 特征匹配 115 | 116 | 当组成一个图像对的两张图的关键点都被识别出来以后,我们需要将它们关联(或称「匹配」)起来,两张图像中对应的关键点在现实中是同一个点。一个可以实现该功能的函数是「BFMatcher.knnMatch()」。这个匹配器(matcher)会衡量每一对关键点的描述子之间的距离,然后返回与每个关键点距离最小的 k 个最佳匹配结果。 117 | 118 | ![](https://i.loli.net/2020/06/06/ZXH5ARo3pKQ6iPs.png) 119 | 120 | ### 图像变形 121 | 122 | 在匹配到至少 4 对关键点之后,我们就可以将一幅图像相对于另一幅图像进行转换。这个过程被称作图像变形(image warping)。空间中同一平面的任意两幅图像都是通过单应性变换关联起来的。单应性变换是具有 8 个参数的几何变换,通过一个 3×3 的矩阵表征。它们代表着对一幅图像整体所做的任何变形(与局部形变不同)。因此,为了得到变换后的待配准图像,我们计算了单应矩阵,并将它应用在了待配准图像上。 123 | 124 | 为了保证最优的变形,我们使用了 RANSAC 算法来检测轮廓,并且在进行最终的单应性变换之前将轮廓删除。该过程直接内置于 OpenCV 的「findHomography()」函数中。目前也有一些 RANSAC 的替代方案,例如 LMED(Least-Median robust method,最小中值鲁棒方法)。 125 | 126 | ## 参考资料 127 | 128 | [不能错过!超强大的SIFT图像匹配技术详细指南(附Python代码)](https://baijiahao.baidu.com/s?id=1650694563611411654&wfr=spider&for=pc) 129 | [SIFT算法详解](https://blog.csdn.net/zddblog/article/details/7521424) 130 | 131 | -------------------------------------------------------------------------------- /机器学习/01_LR和SVM的比较.md: -------------------------------------------------------------------------------- 1 | ## 问题 2 | 3 | 机器学习中LR(Logistic Regression)和SVM(Support Vector Machine)有什么区别与联系? 4 | 5 | ## 背景 6 | 7 | LR和SVM的概念大家都有了解甚至很熟悉了,不过在面试中可能不止是简单地单独考察你对LR或SVM的理解,可能会让你对这两个算法进行比较分析,因此就有必要将两者放在一起比较一下。 8 | 9 | ## LR和SVM的联系 10 | 11 | #### 1. LR和SVM都是分类算法 12 | 13 | 普通的LR和SVM算法只能处理二分类问题,当然,通过改进后的LR和SVM都可以用来处理多分类问题(后面会详细解释)。 14 | 15 | #### 2. 在不考虑核函数时,两者都是线性分类算法 16 | 17 | 注意,不考虑核函数时两者都是**线性**分类器。LR、SVM加了核函数后为分别为KLR、KSVM,只不过一般而言采用KSVM较多而KLR用得较少。 18 | 19 | #### 3. 两者都属于监督学习算法 20 | 21 | #### 4. 两者都是判别式模型 22 | 23 | 什么是判别式模型?假设给定观测集合X,需要预测的变量集合为Y,那么判别式模型就是直接**对条件概率分布P(Y|X)进行建模**来预测 Y;而生成式模型是指,先对联合概率模型P(X,Y)进行建模,然后在给定观测集合X的情况下,通过计算边缘分布来求解出P(Y|X)。 24 | 25 | 常见的判别式模型有:LR、SVM、KNN、神经网络、最大熵模型、条件随机场等 26 | 27 | 常见的生成式模型有:隐马尔科夫模型HMM、朴素贝叶斯、贝叶斯网络、高斯混合模型GMM等 28 | 29 | 30 | 31 | ## LR和SVM的区别 32 | 33 | #### 1. 采用的Loss Function不同 34 | 35 | 从目标函数来看,LR采用的是Logistic Loss,而SVM采用的是Hinge Loss。 36 | $$ 37 | LR\ Loss:\ L(\omega,b)=\sum_{i=1}^{m}ln(y_{i}p_{1}(x;\beta)+(1-y_{i})p_{0}(x;\beta)) = \sum_{i=1}^{m}(-y_{i}\beta^{T}x_{i}+ln(1+e^{\beta^{T}x_{i}})) 38 | \\其中,\beta=(\omega;b), p_{1}=p(y=1|x;\beta), p_{0}=p(y=0|x;\beta) \\ 39 | \\SVM\ Loss:\ L(\omega,b,\alpha)=\frac{1}{2}||w||^2+\sum_{i=1}^{m}\alpha_{i}(1-y_{i}(\omega^{T}x_{i}+b)) 40 | $$ 41 | LR: 基于概率理论,通过极大似然估计方法估计参数值 42 | 43 | SVM:基于几何间隔最大化原理 44 | 45 | > 补充: Logistic Loss: $L_{log}(z)=log(1+e^{-z})$ // 用于 LR 46 | > 47 | > ​ Hinge Loss: $L_{hinge}(z)=max(0,1-z)$ // SVM 48 | > 49 | > ​ Exponential Loss: $L_{exp}(z)=e^{-z}$ // Adaboost 50 | 51 | #### 2. SVM只考虑边界线上局部的点(即support vector),而LR考虑了所有点。 52 | 53 | 影响SVM决策分类面的只有少数的点,即边界线上的支持向量,其他样本对分类决策面没有任何影响,即SVM不依赖于数据分布;而LR则考虑了全部的点(即依赖于数据分布),通过非线性映射,减少远离分类平面的点的权重,即对不平衡的数据要先做balance。 54 | 55 | #### 3. 在解决非线性问题时,SVM采用核函数机制,而LR一般很少采用核函数的方法。 56 | 57 | SVM使用的是hinge loss,可以方便地转化成对偶问题进行求解,在解决非线性问题时,引入核函数机制可以大大降低计算复杂度。 58 | 59 | #### 4. SVM依赖于数据分布的距离测度,所以需对数据先做normalization,而LR不受影响。 60 | 61 | normalization的好处:进行梯度下降时,数值更新的速度一致,少走弯路,可以更快地收敛。 62 | 63 | #### 5. SVM的损失函数中自带正则化项($\frac{1}{2}||w||^2$),而LR需要另外添加。 64 | 65 | 66 | 67 | ## LR和SVM什么时候用? 68 | 69 | 来自Andrew Ng的建议: 70 | 71 | ①若feature数远大于样本数量,使用LR算法或者Linear SVM 72 | 73 | ②若feature数较小,样本数量适中,使用kernel SVM 74 | 75 | ③若feature数较小,样本数很大,增加更多的feature然后使用LR算法或者Linear SVM 76 | 77 | 78 | 79 | ## LR和SVM如何处理多分类问题? 80 | 81 | #### SVM: 82 | 83 | 方式一:组合多个二分类器来实现多分类器(两种方法OvO或OvR) 84 | 85 | ①OvO(one-versus-one): 任意两个类别之间设计一个二分类器,N个类别一共$\frac{N(N-1)}{2}$个二分类器 86 | 87 | ②OvR(one-versus-rest): 每次将一个类别作为正例,其余的作为反例,共N个分类器。 88 | 89 | > 注:OvO和OvR先训练出多个二分类器,在测试时,新样本将同时提交给所有的分类器进行预测,投票产生 最终结果,将被预测的最多的类别作为最终的分类结果 90 | 91 | 方式二:直接修改目标函数,将多个分类面的参数合并到一个最优化问题中,一次性实现多分类。 92 | 93 | #### LR: 94 | 95 | 方式一: OvR:同上,组合多个logistic 二分类器 96 | 97 | 方式二: 修改目标函数,改进成softmax回归 98 | 99 | 100 | 101 | #### 参考资料 102 | 103 | [LR和SVM的相同和不同](https://www.cnblogs.com/bentuwuying/p/6616761.html) 104 | 105 | [SVM学习笔记——SVM解决多分类问题的方法](https://blog.csdn.net/mao_hui_fei/article/details/80452424) 106 | 107 | [逻辑回归解决多分类和softmax](https://blog.csdn.net/u012879957/article/details/81197903) -------------------------------------------------------------------------------- /机器学习/11_三种主要集成学习思想简介.md: -------------------------------------------------------------------------------- 1 | ## 集成学习 2 | 3 | 集成学习通过训练多个分类器,然后将其组合起来,从而达到更好的预测性能,提高分类器的泛化能力。 4 | 5 | 目前集成学习有3个主要框架:bagging、boosting、stacking。 6 | 7 | ### bagging套袋法 8 | 9 | bagging是并行集成学习方法的最著名代表,其算法过程如下: 10 | 11 | 1. 从原始样本集中抽取训练集。每轮从原始样本集中使用Bootstraping的方法抽取n个训练样本(在训练集中,有些样本可能被多次抽取到,而有些样本可能一次都没有被抽中)。共进行k轮抽取,得到k个训练集。(k个训练集之间是相互独立的) 12 | 2. 每次使用一个训练集得到一个模型,k个训练集共得到k个模型。(注:这里并没有具体的分类算法或回归方法,我们可以根据具体问题采用不同的分类或回归方法,如决策树、感知器等) 13 | 3. 对分类问题:将上步得到的k个模型采用投票的方式得到分类结果;对回归问题,计算上述模型的均值作为最后的结果。(所有模型的重要性相同) 14 | 15 | ![bagging](https://i.loli.net/2020/05/25/6HXMElOCfYVqFha.png) 16 | 17 | ### boosting(提升方法) 18 | 19 | 俗话说“三个臭皮匠顶个诸葛亮”,对于一个复杂任务来说,将多个专家的判断进行适当的综合所得出的判断,要比其中任何一个专家单独的判断好。 20 | 21 | Leslie Valiant首先提出了“**强可学习**(strongly learnable)”和”**弱可学习**(weakly learnable)”的概念,并且指出:在概率近似正确(probably approximately correct, PAC)学习的框架中,一个概念(一个类),如果存在一个多项式的学习算法能够学习它,并且正确率很高,那么就称这个概念是强可学习的,如果正确率不高,仅仅比随即猜测略好,那么就称这个概念是弱可学习的。2010年的图灵奖给了L. Valiant,以表彰他的PAC理论。非常有趣的是Schapire后来证明强可学习与弱可学习是等价的,也就是说,在PAC学习的框架下,一个概念是强可学习的充要条件是这个概念是可学习的。 22 | 23 | 这样一来,问题便成为,在学习中,如果已经发现了“弱学习算法”,那么能否将它提升(boost)为”强学习算法”。大家知道,发现弱学习算法通常比发现强学习算法容易得多。那么如何具体实施提升,便成为开发提升方法时所要解决的问题。 24 | 25 | 对于分类问题而言,给定一个训练数据,求一个比较粗糙的分类器(即弱分类器)要比求一个精确的分类器(即强分类器)容易得多。提升方法就是从弱学习算法出发,反复学习,得到一系列弱分类器,然后组合这些弱分类器,构成一个强分类器。**大多数的提升方法都是改变训练数据的概率分布(训练数据中的各个数据点的权值分布),调用弱学习算法得到一个弱分类器,再改变训练数据的概率分布,再调用弱学习算法得到一个弱分类器,如此反复,得到一系列弱分类器**。 26 | 27 | ![boosting](https://i.loli.net/2020/05/25/7DzdFVx8GasNHRo.png) 28 | 29 | 这样,对于提升方法来说,有两个问题需要回答: 30 | 31 | 1. 是在每一轮如何改变训练数据的概率分布 32 | 2. 是如何将多个弱分类器组合成一个强分类器。 33 | 34 | 关于第一个问题,AdaBoost的做法是,提高那些被前几轮弱分类器线性组成的分类器错误分类的的样本的权值。这样一来,那些没有得到正确分类的数据,由于权值加大而受到后一轮的弱分类器的更大关注。于是,分类问题被一系列的弱分类器”分而治之”。至于第二个问题,AdaBoost采取加权多数表决的方法。具体地,加大分类误差率小的弱分类器的权值,使其在表决中起较大的作用,减小分类误差率大的弱分类器的权值,使其在表决中起较小的作用。 35 | 36 | ### Bagging和Boosting的区别 37 | 38 | **1)样本选择上:** 39 | 40 | Bagging:训练集是在原始集中有放回选取的,从原始集中选出的各轮训练集之间是独立的。 41 | 42 | Boosting:每一轮的训练集不变,只是训练集中每个样例在分类器中的权重发生变化。而权值是根据上一轮的分类结果进行调整。 43 | 44 | **2)样例权重:** 45 | 46 | Bagging:使用均匀取样,每个样例的权重相等 47 | 48 | Boosting:根据错误率不断调整样例的权值,错误率越大则权重越大。 49 | 50 | **3)预测函数:** 51 | 52 | Bagging:所有预测函数的权重相等。 53 | 54 | Boosting:每个弱分类器都有相应的权重,对于分类误差小的分类器会有更大的权重。 55 | 56 | **4)并行计算:** 57 | 58 | Bagging:各个预测函数可以并行生成 59 | 60 | Boosting:各个预测函数只能顺序生成,因为后一个模型参数需要前一轮模型的结果。 61 | 62 | ### stacking模型融合 63 | 64 | stacking 就是当用初始训练数据学习出若干个基学习器后,将这几个学习器的预测结果作为新的训练集,来学习一个新的学习器。stacking在深度学习大数据比赛中经常被使用,大概流程可以看看[这篇博客](https://www.jianshu.com/p/59313f43916f)。 65 | 66 | ![stacking](https://i.loli.net/2020/05/25/DQZo5tOAhq2uKBl.png) 67 | 68 | 69 | 70 | ## 参考资料 71 | 72 | [Boosting原理](https://www.jianshu.com/p/11083abc5738) 73 | [提升方法(boosting)详解](https://www.cnblogs.com/linyuanzhou/p/5019166.html) 74 | [Bagging和Boosting的区别](https://www.cnblogs.com/earendil/p/8872001.html) 75 | 76 | -------------------------------------------------------------------------------- /机器学习/17_bagging算法思想及与DNN中的dropout思想的一致性.md: -------------------------------------------------------------------------------- 1 | ## 问题 2 | 3 | 我们在机器学习文件夹的问题“11_三种集成学习思想简介”中大体上介绍了 bagging 思想,在这个问题中,我们便具体讨论下这种思想,且与DNN中的dropout思想做下对比。 4 | 5 | ## bagging 6 | 7 | 要得到泛化性能强的集成,集成中的个体学习器应尽可能表现好且相互独立,即“好而不同”。但是“独立”的学习方法在现实任务中无法做到,因为同一个数据集,训练得到的学习器肯定不会完全独立,但可以设法使基学习器尽可能具有较大的差异。给定一个训练数据集,一种可能的做法是对训练样本进行采样,产生出若干个不同的子集,再从每个数据子集中训练出一个基学习器。这样,由于训练数据的不同,我们获得的基学习器可望具有较大的差异。然而,为了获得好的集成,我们同时还希望个体学习器不能太差,如果采样出的每个子集都完全不同,则每个学习器都只用到了一小部分的训练数据,甚至不足以进行有效学习。为解决这个问题,我们可考虑使用相互有交叠的采样子集。 8 | 9 | bagging对训练数据集的采样使用的是 **bootstrap 自助采样法**,因此这里先对这个方法进行简单介绍: 10 | 11 | > 12 | > 13 | >给定包含 m 个样本的数据集 $D$ ,我们对它进行采样产生数据集 $D'$ :每次随机从 $D$ 中挑选一个样本,将其拷贝放入 $D'$ ,然后再将该样本放回初始数据集 $D$ 中,使得该样本在下次采样时仍有可能被采到;这个过程反复执行 m 次后,我们就得到了包含 m 个样本的数据集 $D'$ ,这就是自助采样法的结果。显然,$D$ 中有一部分样本会在 $D'$ 中多次出现,而另一部分样本不出现,可以做个简单的统计,样本在 m 次采样中始终不被采到的概率是 $(1-\frac{1}{m})^m$ ,取极限得到: 14 | >$$ 15 | >\lim_{m \rightarrow \infty}(1-\frac{1}{m})^m = \frac{1}{e} \approx 0.368 16 | >$$ 17 | 18 | 照上面的自助采样法,我们可以采样出 T 个含有 m 个训练样本的采样集,然后基于每个采样集训练出一个基学习器,再将这些基学习器进行结合,这便是 bagging 方法的基本流程。**在对预测进行结合时,Bagging 通常对分类任务使用简单投票法,对回归任务使用简单平均法。** 19 | 20 | bagging方法之所以有效,是因为并非所有的分类器都会产生相同的误差,只要有不同的分类器产生的误差不同,就会对减小泛化误差有效。 21 | 22 | **与 Adaboost 的区别:** 23 | 24 | >标准 AdaBoost 只适用于二分类任务,而 Bagging 能不经修改地用于多分类与回归任务。 25 | 26 | ## Bagging 与 Dropout 的联系 27 | 28 | dropout 思想继承自 bagging方法。bagging是每次训练一个基分类器的时候,都有一些样本对该基分类器不可见,而dropout是每次训练的时候,都有一些神经元对样本不可见。 29 | 30 | 我们可以把 dropout 类比成将许多大的神经网络进行集成的一种 bagging 方法。但是每一个神经元的训练是非常耗时和占用内存的,训练很多的神经网络进行集合分类就显得太不实际了,但是 dropout可以看做是训练所有子网络的集合,这些子网络通过去除整个网络中的一些神经元来获得。 31 | 32 | **dropout 具体怎么去除一个神经元呢?**可以在每个神经元结点处独立采样一个二进制掩膜,采样一个掩膜值为 0 的概率是一个固定的超参数,则掩膜值为 0 的被去除,掩膜值为 1 的正常输出。 33 | 34 | ### bagging与dropout训练的对比 35 | 36 | - 在bagging中,所有的分类器都是独立的,而在dropout中,所有的模型都是共享参数的。 37 | - 在bagging中,所有的分类器都是在特定的数据集下训练至收敛,而在dropout中没有明确的模型训练过程。网络都是在一步中训练一次(输入一个批次样本,随机训练一个子网络) 38 | 39 | ### dropout的优势 40 | 41 | - very computationally cheap。在dropout训练阶段,每一个样本每一次更新只需要O(n) ,同时要生成n个二进制数字与每个状态相乘。除此之外,还需要O(n)的额外空间存储这些二进制数字,直到反向传播阶段。 42 | - 没有很显著的限制模型的大小和训练的过程。 43 | 44 | ## 参考资料 45 | 46 | [《机器学习》西瓜书]() 47 | [从bagging到dropout(deep learning笔记)](https://blog.csdn.net/m0_37477175/article/details/77145459) -------------------------------------------------------------------------------- /机器学习/19_如何从偏差和方差的角度解释bagging和boosting的原理.md: -------------------------------------------------------------------------------- 1 | ## 偏差与方差 2 | 3 | 偏差指的是算法的期望预测与真实值之间的偏差程度,反映了模型本身的拟合能力; 4 | 5 | 方差度量了同等大小的训练集的变动导致学习性能的变化,刻画了数据扰动所导致的影响。 6 | 7 | ## Boosting 8 | 9 | Boosting从优化角度来看,是用 forward-stagewise 这种贪心法去最小化 loss 函数,由于采取的是串行优化的策略,各子模型之间是强相关的,于是子模型之和并不能显著降低 variance,而每一个新的分类器都在前一个分类器的预测结果上改进,力求预测结果接近真实值,所以说 boosting 主要还是靠降低 bias 来提升预测精度。 10 | 11 | ## Bagging 12 | 13 | Bagging对样本重采样,对每一重采样得到的子样本集训练一个模型,最后取平均。由于子样本集的相似性以及使用的是同种模型,因此各模型有近似相等的bias和variance(事实上,各模型的分布也近似相同,但不独立)。 14 | 15 | 由于 $E[\frac{\sum X_i}{n}] = E[X_i]$ ,所以bagging后的bias和单个子模型的接近,一般来说不能显著降低bias。 16 | 17 | 另一方面,若各子模型独立,则有 $Var(\frac{\sum X_i}{n}) = \frac{Var(X_i)}{n}$ ,此时可以显著降低variance。若各子模型完全相同,则 $Var(\frac{\sum X_i}{n}) = Var(X_i)$ ,此时不会降低variance。bagging方法得到的各子模型是有一定相关性的,属于上面两个极端状况的中间态,因此可以一定程度降低variance。 18 | 19 | 为了进一步降低variance,Random forest 通过随机选取特征子集,进一步减少了模型之间的相关性,从而使得variance进一步降低。 20 | 21 | ## 参考资料 22 | 23 | [为什么说bagging是减少variance,而boosting是减少bias?](https://www.zhihu.com/question/26760839/answer/40337791) -------------------------------------------------------------------------------- /机器学习/20_随机森林思想.md: -------------------------------------------------------------------------------- 1 | ## 问题 2 | 3 | 随机森林算法思想,怎么增加随机性,如何评估特征重要性,为什么不容易过拟合 4 | 5 | ## 随机森林思想怎么添加的随机性 6 | 7 | 随机森林 (RF) 是 Bagging 的一个变体。RF在以决策树为基学习器构建 Bagging 集成的基础上,进一步在决策树的训练过程中引入随机性: 8 | 9 | > 传统决策树在选择划分属性时,是在当前结点的属性集合(假定有 d 个属性)中选择一个最优属性;而在 RF 中,对基决策树的每一个结点,**先从结点的属性集合中随机选择一个包含 k 个属性的子集**,然后再从这个子集当中选择一个最优属性用于划分。 10 | 11 | 这里的参数 k 控制了随机性的引入程度。若令 $k=d$ ,则基决策树的构建与传统决策树相同,一般情况下,推荐值为 $k=log_2 d$ 。 12 | 13 | ## 为什么不容易过拟合 14 | 15 | 因为随机森林中每棵树的训练样本是随机的,每棵树中的每个结点的分裂属性也是随机选择的。这两个随机性的引入,使得随机森林不容易陷入过拟合。且树的数量越多,随机森林通常会收敛到更低的泛化误差。理论上当树的数目趋于无穷时,随机森林便不会出现过拟合,但是现实当做做不到训练无穷多棵树。 16 | 17 | ## 如何评估特征的重要性 18 | 19 | 这个问题是决策树的核心问题,而随机森林是以决策树为基学习器的,所以这里大概提提,详细的可以去看看决策树模型。 20 | 21 | 决策树中,根节点包含样本全集,其他非叶子结点包含的样本集合根据选择的属性被划分到子节点中,叶节点对应于分类结果。决策树的关键是在非叶子结点中怎么选择最优的属性特征以对该结点中的样本进行划分,方法主要有信息增益、增益率以及基尼系数3种,下面分别叙述。 22 | 23 | ### 信息增益 (ID3决策树中采用) 24 | 25 | **“信息熵”**是度量样本集合纯度最常用的一种指标,假定当前样本结合 $D$ 中第 $k$ 类样本所占的比例为 $p_k(k = 1, 2, ..., c)$ ,则 $D$ 的信息熵定义为: 26 | $$ 27 | Ent(D)= -\sum_{k=1}^{c}p_klog_2 p_k 28 | $$ 29 | $Ent(D)$ 的值越小,则 $D$ 的纯度越高。注意因为 $p_k \le 1$ ,因此 $Ent(D)$ 也是一个大于等于0小于1的值。 30 | 31 | 假定离散属性 $a$ 有 V 个可能的取值 $\{a^1,a^2,...,a^V\}$ ,若使用 $a$ 来对样本集合 $D$ 进行划分的话,则会产生 V 个分支结点,其中第 $v$ 个分支结点包含了 $D$ 中所有在属性 $a$ 上取值为 $a^v$ 的样本,记为 $D^v$ 。同样可以根据上式计算出 $D^v$ 的信息熵,再考虑到不同的分支结点所包含的样本数不同,给分支结点赋予权重 $\frac{|D^v|}{|D|}$ ,即样本数越多的分支结点的影响越大,于是可以计算出使用属性 $a$ 对样本集 $D$ 进行划分时所获得的“信息增益”: 32 | $$ 33 | Gain(D,a) = Ent(D) - \sum_{v=1}^{V}\frac{|D^v|}{|D|}Ent(D^v) 34 | $$ 35 | 一般而言,信息增益越大越好,因为其代表着选择该属性进行划分所带来的纯度提升,因此全部计算当前样本集合 $D$ 中存在不同取值的那些属性的信息增益后,取信息增益最大的那个所对应的属性作为划分属性即可。 36 | 37 | **缺点:**对可取值数目多的属性有所偏好 38 | 39 | ### 增益率 (C4.5决策树中采用) 40 | 41 | 从信息增益的表达式很容易看出,信息增益准则对可取值数目多的属性有所偏好,为减少这种偏好带来的影响,大佬们提出了增益率准则,定义如下: 42 | $$ 43 | Gain\_ratio(D,a) = \frac{Gain(D,a)}{IV(a)} \\ 44 | IV(a) = \sum_{v=1}^{V}\frac{|D^v|}{|D|}log_2 \frac{|D^v|}{|D|} 45 | $$ 46 | $IV(a)$ 称为属性 a 的“固有值”。属性 a 的可能取值数目越多,则 $IV(a)$ 的值通常会越大,因此一定程度上抵消了信息增益对可取值数目多的属性的偏好。 47 | 48 | **缺点:**增益率对可取值数目少的属性有所偏好 49 | 50 | > 因为增益率存在以上缺点,因此C4.5算法并不是直接选择增益率最大的候选划分属性,而是使用了一个启发式:**先从候选划分属性中找出信息增益高于平均水平的属性,再从中选择增益率最高的。** 51 | 52 | ### 基尼指数 (CART决策树中采用) 53 | 54 | 这里改用基尼值来度量数据集 $D$ 的纯度,而不是上面的信息熵。基尼值定义如下: 55 | $$ 56 | Gini(D) = \sum_{k=1}^{c} \sum_{k^, \not =k}p_kp_{k^,} = 1- \sum_{k=1}^{c}p_k^2 = 1-\sum_{k=1}^{c}(\frac{D^k}{D})^2 57 | $$ 58 | 直观来看,$Gini(D)$ 反映了从数据集 $D$ 中随机抽取两个样本,其类别标记不一致的概率,因此$Gini(D)$ 越小,则数据集 $D$ 的纯度越高。 59 | 60 | 对于样本D,个数为|D|,根据特征A的某个值a,把D分成|D1|和|D2|,则在特征A的条件下,样本D的基尼系数表达式为: 61 | $$ 62 | Gini\_index(D,A) = \frac{|D^1|}{|D|}Gini(D^1)+ \frac{|D^2|}{|D|}Gini(D^2) 63 | $$ 64 | 于是,我们在候选属性集合A中,选择那个使得划分后基尼系数最小的属性作为最优划分属性即可。 65 | 66 | ## 参考资料 67 | 68 | 《机器学习》周志华 69 | [决策树算法原理 (CART决策树)](https://www.cnblogs.com/keye/p/10564914.html) 70 | 71 | -------------------------------------------------------------------------------- /机器学习/26_k-means算法原理.md: -------------------------------------------------------------------------------- 1 | ## 问题 2 | 3 | k-means算法可以说是机器学习中大家最耳熟能详也是最基础的聚类算法,面试中也常常被问起,由浅至深,没有扎实的理论基础是很难过得了这一关的,因此,我们必须得梳理梳理并且深入理解它。 4 | 5 | ## k-means算法简介 6 | 7 | ● k-means是一种聚类算法。所谓的聚类,就是指在不知道任何样本的标签的情况下,通过数据之间的内在关系将样本分成若干个类别,使得相同类别样本之间的相似度高,不同类别之间的样本相似度低。因此,k-means算法属于非监督学习的范畴。 8 | 9 | ● k 是指 k 个簇(cluster),means 是指每个簇内的样本均值,也就是聚类中心。 10 | 11 | ● 基本思想:通过迭代的方式寻找 k 个簇的划分方案,使得聚类结果对应的代价函数最小。代价函数可以定义为各个样本距离它所属的簇的中心点的误差平方和: 12 | $$ 13 | J(c,\mu)=\sum_{i=1}^{N} ||x_{i} - \mu _{c_{i}}||^{2} \\ 14 | 其中,x_{i}代表第i个样本,c_{i}是x_{i}所属的簇,\mu_{c_{i}}代表簇对应的中心点(即均值),N是样本总数. 15 | $$ 16 | 17 | 18 | ## k-means算法流程 19 | 20 | k-means算法采用了**贪心策略**,通过多次迭代来近似求解上面的代价函数,具体算法流程如下: 21 | 22 | 1. 随机选取 k 个点作为初始聚类(簇)中心,记为 $\mu_{1}^{(0)},\mu_{2}^{(0)},...,\mu_{k}^{(0)}$。 23 | 24 | 2. 计算每个样本 $x_{i}$ 到各个簇中心的距离,将其分配到与它距离最近的簇。 25 | $$ 26 | c_{i}^{(t)} \leftarrow \underset{k'}{argmin} ||x_{i}-\mu _{k'}^{(t)}||^{2} \\ 27 | 其中,t为当前迭代步数,k'为第k'个簇(类别)(k'=1,2,..,k) 28 | $$ 29 | 30 | 3. 对于每个簇,利用该簇中的样本重新计算该簇的中心(即均值向量): 31 | $$ 32 | \mu_{k}^{(t+1)} \leftarrow \underset{\mu}{argmin} \sum_{i:c_{i}^{(t)}=k'} ||x_{i}-\mu||^{2} 33 | $$ 34 | 35 | 4. 重复迭代上面2-3步骤 T 次,若聚类结果保持不变,则结束。 36 | 37 | 38 | 39 | k-means算法迭代示意图如下: 40 | 41 | ![](https://i.loli.net/2020/06/11/cUOCwPasfu57qgh.png) 42 | 43 | 44 | 45 | ## k-means算法优缺点 46 | 47 | **优点:** 48 | 49 | ① 算法简单易实现。 50 | 51 | ② 对于大数据集,这种算法相对可伸缩且是高效的,计算复杂度为 $O(TNk)$ 接近于线性(其中T是迭代次数、N是样本总数、k为聚类簇数)。 52 | 53 | ③ 虽然以局部最优结束,但一般情况下达到的局部最优已经可以满足聚类的需求。 54 | 55 | **缺点:** 56 | 57 | ① 需要人工预先确定初始K值,该值与实际的类别数可能不吻合。 58 | 59 | ② **K均值只能收敛到局部最优。**因为求解这个代价函数是个NP问题,采用的是贪心策略,所以只能通过多次迭代收敛到局部最优,而不是全局最优。 60 | 61 | ③ **K均值的效果受初始值和离群点的影响大。**因为 k 均值本质上是基于距离度量来划分的,均值和方差大的维度将对数据的聚类结果产生决定性的影响,因此需要进行归一化处理;此外,离群点或噪声对均值会产生影响,导致中心偏移,因此需要进行预处理。 62 | 63 | ④ 对于数据簇分布差别较大的情况聚类效果很差。例如一个类别的样本数是另一类的100倍。 64 | 65 | ⑤ 样本只能被划分到一个单一的类别中。 66 | 67 | 68 | 69 | > 针对k-means上述缺点,有什么方法改进吗?当然是有的。 70 | 71 | 72 | 73 | ## k-means++算法 74 | 75 | 由于 k-means 算法中,初始K值是人为地凭借经验来设置的,聚类的结果可能不符合实际的情况。因此,K值的设置对算法的效果影响其实是很大的,那么,如何设置这个K值才能取得更好的效果呢? 76 | 77 | **k-means++算法的主要思想:** 78 | 79 | ● 首先随机选取1个初始聚类中心(n=1)。 80 | 81 | ● 假设已经选取了n个初始聚类中心(0 面试中可能会被问到这样的问题:k-means算法中的k值是怎么设置的?是一次初始k个值还是一个一个地初始直到k个?有了上面两种算法的对比,这个问题自然就迎刃而解了。 88 | > 89 | > 90 | > 91 | > 然而,k-means++算法虽然可以更好地初始k个聚类中心,但还是不能解决一个问题,k 值应该取多少才算合理? 92 | 93 | 94 | 95 | ## 如何选取k值? 96 | 97 | 如何才能合理地选取k值是k-means算法最大的问题之一,一般可以采取手肘法和`Gap Statistic`方法。 98 | 99 | ### 手肘法 100 | 101 | k值的选择一般基于经验或者多次实验来确定,手肘法便是如此,其主要思想是:通过多次实验分别选取不同的k值,将不同k值的聚类结果对应的最小代价画成折线图,将曲线趋于平稳前的拐点作为最佳k值。如下图所示: 102 | 103 | ![](https://i.loli.net/2020/06/11/DWTN23pnCzXqlwL.png) 104 | 105 | > 上图中,k取值在1~3时,曲线急速下降;当k>3时,曲线趋于平稳。因此,在k=3处被视为拐点,所以手肘法认为最佳的k值就是3。 106 | > 107 | > 然而,实际中很多时候曲线并非如同上图一样拐点处那么明显,因此单靠肉眼去分辨是很容易出错的。于是,就又有了一个改进的方法,可以不需要靠肉眼与分辨拐点,而是寻找某个最大值Gap(k),具体如下。 108 | 109 | ### Gap Statistic 方法 110 | 111 | Gap Statistics 定义为: 112 | $$ 113 | Gap(k)=E(logD_{k})-logD_{k} \\ 114 | 其中,D_{k}是第k簇聚类对应的损失值,E(logD_{k})是logD_{k}的期望。 115 | $$ 116 | 对于上式的 $E(logD_{k})$,一般通过蒙特卡洛模拟产生。具体操作是:在样本所在的区域内,按照均匀分布随机产生和原样本数目一样的随机样本,计算这些随机样本的均值,得到一个 $D_{k}$,重复多次即可计算出 $E(logD_{k})$ 的近似值。 117 | 118 | $Gap(k)$ 可以看做是随机样本的损失与实际样本的损失之差,假设实际样本最佳的簇类数目为 k,那么实际样本的损失应该相对较小,随机样本的损失与实际样本的损失的差值相应地达到最大,即**最大的 $Gap(k)$ 值应该对应最佳的k值。** 119 | 120 | 因此,我们只需要用不同的k值进行多次实验,找出使得$Gap(k)$最大的k即可。 121 | 122 | 123 | 124 | > 到现在为止我们可以发现,上面的算法中,k值都是通过人为地凭借经验或者多次实验事先确定下来了的,但是当我们遇到高维度、海量的数据集时,可能就很难估计出准确的k值。那么,有没有办法可以帮助我们自动地确定k值呢?有的,下面来看看另一个算法。 125 | 126 | 127 | 128 | ## ISODATA算法 129 | 130 | ISODATA,全称是迭代自组织数据分析法,这种方法是针对传统 k-means 算法需要人为地预先确定k值的问题而改进的,其主要的思想是: 131 | 132 | ● 当某个类别样本数目过多、分散程度较大时,将该类别分为两个子类别。(分裂操作,即增加聚类中心数) 133 | 134 | ● 当属于某个类别的样本数目过少时,把该类别去除掉。(合并操作,即减少聚类中心数) 135 | 136 | **算法优点:** 可以自动寻找合适的k值。 137 | 138 | **算法缺点:** 除了要设置一个参考聚类数量 $k_{0}$ 外,还需要指定额外的3个阈值,来约束上述的分裂和合并操作。具 体如下: 139 | 140 | 1. 预期的聚类数目 $k_{0}$ 作为参考值,最终的结果在 $k_{0}$ 的一半到两倍之间。 141 | 2. 每个类的最少样本数目 $N_{min}$,若分裂后样本数目会少于该值,则该簇不会分裂。 142 | 3. 最大方差 $Sigma$,用于控制某个簇的样本分散程度,操作该值且满足条件2,则分裂成两个簇。 143 | 4. 两个簇最小距离 $D_{min}$,若两个簇距离小于该值,则合并成一个簇。 -------------------------------------------------------------------------------- /机器学习/29_k-means和GMM的区别与联系.md: -------------------------------------------------------------------------------- 1 | ## 问题 2 | 3 | k-means和GMM(高斯混合模型)都是聚类算法,这两者其实也有一定的相似之处,值得我们探究一下。通过之前的整理,我们对k-means算法已经有了一定的了解,这里就着重补充一下GMM的内容以及它们之间的区别与联系。 4 | 5 | ## 高斯混合模型(GMM) 6 | 7 | ● 定义:高斯混合模型是指具有如下形式的概率分布模型: 8 | $$ 9 | P(x|\theta)=\sum_{k=1}^{K} \alpha_{k} \phi (x|\theta_{k})\\ 10 | 其中,\alpha_{k}是高斯混合系数,\alpha_{k} \geq 0 \ 且\sum_{k=1}^{K}\alpha_{k}=1; 11 | \theta_{k}=(\mu_{k},\sigma_{k}^{2});\\ 12 | \phi (x|\theta_{k})是第k个高斯分布模型的概率密度函数,具体形式如下:\\ 13 | \phi (x|\theta_{k})=\frac{1}{\sqrt {2\pi} \sigma_{k}} exp\left ( -\frac{(y-\mu_{k})^{2}}{2\sigma_{k}^{2}} \right ) 14 | $$ 15 | 16 | ## GMM聚类 17 | 18 | 高斯混合模型(GMM)聚类的思想和 k-means 其实有点相似,都是通过迭代的方式将样本分配到某个簇类中,然后更新簇类的信息,不同的是GMM是基于概率模型来实现的,而 k-means 是非概率模型,采用欧氏距离的度量方式来分配样本。 19 | 20 | #### GMM聚类主要思想和流程: 21 | 22 | 每个GMM由K个混合成分组成,每个混合成分都是一个高斯分布,$\alpha_{k}$ 为相应的混合系数。GMM模型假设所有的样本都根据高斯混合分布生成,那么每个高斯分布其实就代表了一个簇类。具体流程如下: 23 | 24 | 1. 先初始化高斯混合模型的参数 $\{(\alpha_{k},\mu_{k},\sigma_{k}^{2})\ | \ 1\leq k \leq K \ \}$ ,训练一个GMM模型需要估计这些参数,如何估计后面会介绍。 25 | 2. 对每个样本,固定各个高斯分布,计算样本在各个高斯分布上的概率(即该样本是由某个高斯分布生成而来的概率)。 26 | 3. 然后固定样本的生成概率,更新参数以获得更好的高斯混合分布。 27 | 4. 迭代至指定条件结束。 28 | 29 | 30 | 31 | > 上面的1-4给出了GMM算法的大致思想,虽然简略了一些,但对比k-means算法的思想一起来看应该也很容易理解。 32 | > 33 | > ● k-means初始化K个均值,GMM初始化K个高斯分布和相应的混合系数; 34 | > 35 | > ● k-means计算样本到各个簇中心的欧氏距离并选择最小距离来划分样本应该属于哪个簇类,而GMM给出的是样本由某个高斯分布生成而来的概率,比如有80%的概率是由A分布生成的,有20%的概率是B分布生成的。这一点在医学诊断上很有意义,比如相比于k-means算法会很硬性地认为某位病人得了肿瘤或正常,GMM给出病人有51%的概率患有肿瘤这样的结果往往会更有参考意义。 36 | > 37 | > ● 两者都是采用迭代的方式来不断更新参数,以求得最优解(都是局部最优解)。 38 | 39 | 40 | 41 | ## EM算法估计GMM参数 42 | 43 | 上面提到,要训练一个GMM模型,就需要估计每个高斯分布的参数 $\{(\alpha_{k},\mu_{k},\sigma_{k}^{2})\ | \ 1\leq k \leq K \ \}$,才能知道每个样本是由哪个高斯混合成分生成的,也就是说,数据集的所有样本是可观测数据, $\{(\alpha_{k},\mu_{k},\sigma_{k}^{2})\ | \ 1\leq k \leq K \ \}$ 这些是待观测数据(隐变量),而估计待观测数据常用的算法就是EM算法。 44 | 45 | 下面给出EM算法估计高斯混合模型的参数的步骤,详细的推导过程可以参考《统计学习方法》第9.3节的内容: 46 | 47 | 1. 给定数据集$D=\{x_{1},x_{2},...,x_{m} \}$,初始化高斯混合分布的模型参数 $\{(\alpha_{k},\mu_{k},\sigma_{k}^{2})\ | \ 1\leq k \leq K \ \}$。 48 | 49 | 2. **E步:**遍历每个样本,对每个样本 $x_{i}$,计算其属于第k个高斯分布的概率: 50 | $$ 51 | \gamma_{ik}=\frac{\alpha_{k}\phi(x_{i}|\theta_{k})}{\sum_{k=1}^{K}\alpha_{k}\phi(x_{i}|\theta_{k})} \ ,\quad 其中,\theta_{k}=(\mu_{k},\sigma_{k}^{2}) 52 | $$ 53 | 54 | 3. **M步:**更新各个高斯分布的参数为$\{(\hat{\alpha}_{k},\hat{\mu}_{k},\hat{\sigma}_{k}^{2})\ | \ 1\leq k \leq K \ \}$ : 55 | $$ 56 | \hat{\alpha}_{k}=\frac{\sum_{i=1}^{m} \gamma_{ik} x_{i}}{\sum_{i=1}^{m} \gamma_{ik}} \\ 57 | \hat{\mu}_{k}=\frac{\sum_{i=1}^{m} \gamma_{ik} (x_{i}-\mu_{k})^{2}}{\sum_{i=1}^{m} \gamma_{ik}} \\ 58 | \hat{\sigma}_{k}^{2}=\frac{\sum_{i=1}^{m} \gamma_{ik}}{m} 59 | $$ 60 | 61 | 4. 重复2-3步,直至收敛。 62 | 63 | > 注意,EM算法通过迭代的方式估计GMM模型的参数,得到的是**局部最优解**而不是全局最优。 64 | 65 | 66 | 67 | 在了解了EM算法后,让我们再来看看高斯混合聚类是怎么操作的吧。。。 68 | 69 | **在迭代收敛后,遍历所有的样本,对于每个样本 $x_{i}$,计算它在各个高斯分布中的概率,将样本划分到概率最大的高斯分布中**(每个高斯分布都相当于是一个簇类,因此可以理解为是将每个样本划分到相应的类别中,不过实际上是给出属于每个类别的概率而非属于某个类别)。 70 | 71 | 72 | 73 | ## k-means和GMM算法的区别与联系 74 | 75 | 终于要回到正题了,不过相信从上面的分析看下来,应该对这两种算法的区别与联系已经有了大致理解了吧,下面就再来总结一下: 76 | 77 | #### 区别: 78 | 79 | ① k-means算法是非概率模型,而GMM是概率模型。 80 | 81 | > 具体来讲就是,k-means算法基于欧氏距离的度量方式来将样本划分到与它距离最小的簇类,而GMM则是计算由各个高斯分布生成样本的概率,将样本划分到取得最大概率的高斯分布中。 82 | 83 | ② 两者需要计算的参数不同。 84 | 85 | > k-means计算的是簇类的均值,GMM计算的是高斯分布的参数(即均值、方差和高斯混合系数) 86 | 87 | ③ k-means是硬聚类,要么属于这一类要么属于那一类;而GMM算法是软聚类,给出的是属于某些类别的概率。 88 | 89 | ④ GMM每一步迭代的计算量比k-means要大。 90 | 91 | #### 联系: 92 | 93 | ① 都是聚类算法 94 | 95 | ② 都需要指定K值,且都受初始值的影响。k-means初始化k个聚类中心,GMM初始化k个高斯分布。 96 | 97 | ③ 都是通过迭代的方式求解,而且都是局部最优解。k-means的求解过程其实也可以用EM算法的E步和M步来理解。 98 | 99 | 100 | 101 | ## 参考资料 102 | 103 | 李航--《统计学习方法》 104 | 105 | [K-means算法和高斯混合模型的异同](https://blog.csdn.net/qq_38150441/article/details/80498590) https://blog.csdn.net/qq_38150441/article/details/80498590 -------------------------------------------------------------------------------- /机器学习/33_决策树的原理以及决策树如何生成.md: -------------------------------------------------------------------------------- 1 | ## 问题 2 | 3 | 决策树是怎么一步步生成的 4 | 5 | ## 决策树原理 6 | 7 | 决策树简单来说就是带有判决规则(if-then)的一种树,可以依据树中的判决规则来预测未知样本的类别和值。 8 | 9 | 决策树的学习本质上是从训练集中归纳出一组分类规则,得到与数据集矛盾较小的决策树,同时具有很好的泛化能力。决策树学习的**损失函数通常是正则化的极大似然函数**,通常采用启发式方法,近似求解这一最优化问题。 10 | 11 | ## 决策树生成流程 12 | 13 | 顾名思义,决策树是一种树结构,一般的,包含一个根节点、若干个内部结点和若干个叶节点。根节点包含样本全集;叶节点对应着分类结果,即在同一个叶节点中的样本被分类归属为一个类别;而内部结点中的每一个结点对应于属性测试,即当前内部结点是其父结点根据最优属性划分后的一个分支,在当前结点依然可分的情况下,继续寻找最优划分属性并划分为该属性所有取值的个数的分支,至于怎么选择最优划分属性,请参考机器学习文件夹中的问题“20_随机森林思想”。 14 | 15 | 决策树的生成是一个递归的过程。在决策树的基本算法中,有三种情况会导致递归返回: 16 | (1)当前节点包含的样本全属于同一类别,无需划分; 17 | (2)当前属性集为空,或是所有样本在所有属性上取值相同,无法划分,这时把当前结点标记为叶节点,并将其类别设定为该节点所含样本最多的类别; 18 | (3)当前节点包含的样本集为空,不能划分。 19 | 20 | ## 拓展 21 | 22 | 决策树不仅可以用于分类问题,同样可以应用于回归问题。 23 | 24 | **回归树**总体流程与分类树类似,不过在每个节点(不一定是叶子节点)都会得一个预测值,以年龄为例,该预测值等于属于这个节点的所有人年龄的平均值。分枝时穷举每一个feature的每个阈值找最好的分割点,但衡量最好的标准不再是最大熵,而是最小化均方差--即(每个人的年龄-预测年龄)^2 的总和 / N,或者说是每个人的预测误差平方和 除以 N。这很好理解,被预测出错的人数越多,错的越离谱,均方差就越大,通过最小化均方差能够找到最靠谱的分枝依据。分枝直到每个叶子节点上人的年龄都唯一(这太难了)或者达到预设的终止条件(如叶子个数上限),若最终叶子节点上人的年龄不唯一,则以该节点上所有人的平均年龄做为该叶子节点的预测年龄。 25 | 26 | ## 参考资料 27 | 28 | 《机器学习》周志华 29 | [决策树原理详解](https://blog.csdn.net/qq_38923076/article/details/82930949) 30 | [决策树模型 ID3/C4.5/CART算法比较](https://www.cnblogs.com/wxquare/p/5379970.html) -------------------------------------------------------------------------------- /机器学习/34_ID3_C4.5_CART算法总结与对比.md: -------------------------------------------------------------------------------- 1 | ## 问题 2 | 3 | ID3、C4.5、CART算法总结与对比 4 | 5 | ## 前言 6 | 7 | ID3、C4.5、CART算法是三种不同的决策树算法,区别主要在最优划分属性的选择上,下面把之前在随机森林中汇总过的复制过来,然后再总结下三者的不同。 8 | 9 | ## 三种算法所用的最优属性选择方法详述 10 | 11 | ### 信息增益 (ID3决策树中采用) 12 | 13 | **“信息熵”**是度量样本集合纯度最常用的一种指标,假定当前样本结合 $D$ 中第 $k$ 类样本所占的比例为 $p_k(k = 1, 2, ..., c)$ ,则 $D$ 的信息熵定义为: 14 | $$ 15 | Ent(D)= -\sum_{k=1}^{c}p_klog_2 p_k 16 | $$ 17 | $Ent(D)$ 的值越小,则 $D$ 的纯度越高。注意因为 $p_k \le 1$ ,因此 $Ent(D)$ 也是一个大于等于0小于1的值。 18 | 19 | 假定离散属性 $a$ 有 V 个可能的取值 $\{a^1,a^2,...,a^V\}$ ,若使用 $a$ 来对样本集合 $D$ 进行划分的话,则会产生 V 个分支结点,其中第 $v$ 个分支结点包含了 $D$ 中所有在属性 $a$ 上取值为 $a^v$ 的样本,记为 $D^v$ 。同样可以根据上式计算出 $D^v$ 的信息熵,再考虑到不同的分支结点所包含的样本数不同,给分支结点赋予权重 $\frac{|D^v|}{|D|}$ ,即样本数越多的分支结点的影响越大,于是可以计算出使用属性 $a$ 对样本集 $D$ 进行划分时所获得的“信息增益”: 20 | $$ 21 | Gain(D,a) = Ent(D) - \sum_{v=1}^{V}\frac{|D^v|}{|D|}Ent(D^v) 22 | $$ 23 | 一般而言,信息增益越大越好,因为其代表着选择该属性进行划分所带来的纯度提升,因此全部计算当前样本集合 $D$ 中存在不同取值的那些属性的信息增益后,取信息增益最大的那个所对应的属性作为划分属性即可。 24 | 25 | **缺点:**对可取值数目多的属性有所偏好 26 | 27 | ### 增益率 (C4.5决策树中采用) 28 | 29 | 从信息增益的表达式很容易看出,信息增益准则对可取值数目多的属性有所偏好,为减少这种偏好带来的影响,大佬们提出了增益率准则,定义如下: 30 | $$ 31 | Gain\_ratio(D,a) = \frac{Gain(D,a)}{IV(a)} \\ 32 | IV(a) = \sum_{v=1}^{V}\frac{|D^v|}{|D|}log_2 \frac{|D^v|}{|D|} 33 | $$ 34 | $IV(a)$ 称为属性 a 的“固有值”。属性 a 的可能取值数目越多,则 $IV(a)$ 的值通常会越大,因此一定程度上抵消了信息增益对可取值数目多的属性的偏好。 35 | 36 | **缺点:**增益率对可取值数目少的属性有所偏好 37 | 38 | > 因为增益率存在以上缺点,因此C4.5算法并不是直接选择增益率最大的候选划分属性,而是使用了一个启发式:**先从候选划分属性中找出信息增益高于平均水平的属性,再从中选择增益率最高的。** 39 | 40 | ### 基尼指数 (CART决策树中采用) 41 | 42 | ID3中根据属性值分割数据,之后该特征不会再起作用,这种快速切割的方式会影响算法的准确率,因为这是种贪心算法,不能保证找到全局最优值。CART是一棵二叉树,采用二元切分法,每次把数据切成两份,分别进入左子树、右子树。而且每个非叶子节点都有两个孩子,所以CART的叶子节点比非叶子多1。相比ID3和C4.5,CART应用要多一些,既可以用于分类也可以用于回归。 43 | 44 | 这里改用基尼值来度量数据集 $D$ 的纯度,而不是上面的信息熵。基尼值定义如下: 45 | $$ 46 | Gini(D) = \sum_{k=1}^{c} \sum_{k^, \not =k}p_kp_{k^,} = 1- \sum_{k=1}^{c}p_k^2 = 1-\sum_{k=1}^{c}(\frac{D^k}{D})^2 47 | $$ 48 | 直观来看,$Gini(D)$ 反映了从数据集 $D$ 中随机抽取两个样本,其类别标记不一致的概率,因此$Gini(D)$ 越小,则数据集 $D$ 的纯度越高。 49 | 50 | 对于样本D,个数为|D|,根据特征A的某个值a,把D分成|D1|和|D2|,则在特征A的条件下,样本D的基尼系数表达式为: 51 | $$ 52 | Gini\_index(D,A) = \frac{|D^1|}{|D|}Gini(D^1)+ \frac{|D^2|}{|D|}Gini(D^2) 53 | $$ 54 | 于是,我们在候选属性集合A中,选择那个使得划分后基尼系数最小的属性作为最优划分属性即可。 55 | 56 | ## 三种算法对比总结 57 | 58 | 下面是根据自己的理解整理的,不知道全不全,应该差不多了。 59 | 60 | ### ID.3 61 | 62 | 1. **最优划分属性选择方法**:信息增益 63 | 2. **分支数**:可多分支 64 | 3. **能否处理连续值特征**:不能 65 | 4. **缺点**:偏好与可取值数目多的属性 66 | 67 | ### C4.5 68 | 69 | 1. **最优划分属性选择方法**:增益率 70 | 2. **分支数**:可多分支 71 | 3. **能否处理连续值特征**:能,C4.5 决策树算法采用的**二分法**机制来处理连续属性。对于连续属性 a,首先将 n 个不同取值进行从小到大排序,选择相邻 a 属性值的平均值 t 作为候选划分点,划分点将数据集分为两类,因此有包含 n-1 个候选划分点的集合,分别计算出每个划分点下的信息增益,选择信息增益最大对应的划分点,仍然以信息增益最大的属性作为分支属性。 72 | 4. **缺点**:增益率对可取值数目少的属性有所偏好,因此C4.5算法并不是直接选择增益率最大的候选划分属性,而是使用了一个启发式:**先从候选划分属性中找出信息增益高于平均水平的属性,再从中选择增益率最高的。** 73 | 74 | ### CART 75 | 76 | 1. **最优划分属性选择方法**:基尼系数 77 | 2. **分支数**:二叉树 78 | 3. **能否处理连续值特征**:能,做法与C4.5一样。也可以用于回归,用于回归时通过最小化均方差能够找到最靠谱的分枝依据,回归树的具体做法可见机器学习的问题33。 79 | 4. **优点**:与ID3、C4.5不同,在ID3或C4.5的一颗子树中,离散特征只会参与一次节点的建立,但是在CART中之前处理过的属性在后面还可以参与子节点的产生选择过程。 80 | 81 | ## 参考资料 82 | 83 | [决策树算法原理 (CART决策树)](https://www.cnblogs.com/keye/p/10564914.html) 84 | 《机器学习》周志华 85 | [决策树模型 ID3/C4.5/CART算法比较](https://www.cnblogs.com/wxquare/p/5379970.html) 86 | 87 | -------------------------------------------------------------------------------- /机器学习/45_LDA算法原理.md: -------------------------------------------------------------------------------- 1 | ## 问题 2 | 3 | 线性判别分析(Linear Discriminant Analysis,LDA)是机器学习中常用的降维方法之一,本文旨在介绍LDA算法的思想,其数学推导过程可能会稍作简化。 4 | 5 | ## LDA的思想 6 | 7 | ● LDA是一种线性的、有监督的降维方法,即每个样本都有对应的类别标签(这点和PCA)。 8 | 9 | ● 主要思想:给定训练样本集,设法将样本投影到一条直线上,使得同类的样本的投影尽可能的接近、异类样本的投影尽可能地远离(即**最小化类内距离和最大化类间距离**)。 10 | 11 | > 下面分别通过《机器学习》和《百面机器学习》两本书中的图片先来直观地理解一下LDA的思想。 12 | 13 | ![](https://i.loli.net/2020/06/04/qOzCRLJ7d5Erx2F.png) 14 | 15 | ![](https://i.loli.net/2020/06/04/uETl7im9gFfwxnY.png) 16 | 17 | ● 为什么要将最大化类间距离和最小化类内距离同时作为优化目标呢? 18 | 19 | 先看上面第二张图的左图(a),对于两个类别,只采用了最大化类间距离,其结果中两类样本会有少许重叠;而对于右图(b),同时最大化类间距离和最小化类内距离,可见分类效果更好,同类样本的投影分布更加集中了。当然,对于二维的数据,可以采用将样本投影到直线上的方式,对于高维的数据,则是投影到一个低维的超平面上,这应该很好理解。 20 | 21 | 22 | 23 | ## LDA算法优化目标 24 | 25 | 由上面的介绍我们知道,LDA算法的思想就是最大化类间距离和最小化类内距离,其优化目标就很直观了,那怎么用数学方式来表示呢?要解决这个问题,就得先看看怎么描述类间距离和类内距离。 26 | 27 | **● 类间距离**(以二分类为示例) 28 | 29 | 假设有$C_{1}$、$C_{2}$两类样本,其均值分别为 $\mu_{1}=\frac{1}{N}\sum_{x\in C_{1}}x$ 和 $\mu_{2}=\frac{1}{N}\sum_{x\in C_{2}}x$ 。很显然,要使得两类样本类间距离最大,则 $\mu_{1}$ 、$\mu_{2}$ 的距离应尽可能地大,则类间距离可描述为 30 | $$ 31 | ||\omega^{T}\mu_{0}-\omega^{T}\mu_{1}||_{2}^{2},\ \ 其中,\omega为投影方向 32 | $$ 33 | **● 类内距离** 34 | 35 | 要使得样本在同类中距离最小,也就是最小化同类样本的方差,假设分别用 $D_{1}$、 $D_{2}$ 表示两类样本的投影方差,则有: 36 | $$ 37 | D_{1} = \sum_{x\in C_{1}}(\omega^{T}x-\omega^{T}\mu_{1})^{2}=\sum_{x\in C_{1}}\omega^{T}(x-\mu_{1})(x-\mu_{1})^{T}\omega \\ 38 | D_{2} = \sum_{x\in C_{2}}(\omega^{T}x-\omega^{T}\mu_{2})^{2}=\sum_{x\in C_{2}}\omega^{T}(x-\mu_{2})(x-\mu_{2})^{T}\omega 39 | $$ 40 | 因此,要使得类内距离最小,就是要最小化 $D_{1}+D_{2}$。 41 | 42 | **● 优化目标** 43 | 44 | 由上面分析,最大化类间距离和最小化类内距离,因此可以得到最大化目标: 45 | $$ 46 | J(\omega) = \frac{||\omega^{T}\mu_{0}-\omega^{T}\mu_{1}||_{2}^{2}}{D_{1}+D_{2}}\\\qquad\qquad\qquad\quad 47 | =\frac{||\omega^{T}\mu_{0}-\omega^{T}\mu_{1}||_{2}^{2}}{\sum_{x\in C_{i}}\omega^{T}(x-\mu_{i})(x-\mu_{i})^{T}\omega} 48 | $$ 49 | 为了化简上面公式,给出几个定义: 50 | 51 | **● 类间散度矩阵:** 52 | $$ 53 | S_{b}=(\mu_{1}-\mu_{2})(\mu_{1}-\mu_{2})^{T} 54 | $$ 55 | **● 类内散度矩阵:** 56 | $$ 57 | S_{\omega}=\Sigma_{1}+\Sigma_{2}=\sum_{x\in C_{1}}(x-\mu_{1})(x-\mu_{1})^{T}+\sum_{x\in C_{2}}(x-\mu_{2})(x-\mu_{2})^{T} 58 | $$ 59 | 因此最大化目标可以简写为: 60 | $$ 61 | J(\omega) = \frac{\omega^{T}S_{b}\omega}{\omega^{T}S_{\omega}\omega} 62 | $$ 63 | 64 | > 这是一个广义瑞利商,可以对矩阵进行标准化操作(具体证明就不展开啦),因此,通过标准化后总可以得到 $\omega^{T}S_{\omega}\omega=1$,又由于上面优化目标函数分子分母都是二次项,其解与 $\omega$ 的长度无关,只与方向有关,因此上面优化目标等价于以下最小化目标: 65 | 66 | 转化为最小化目标: 67 | $$ 68 | \underset{\omega}{min}\quad-\omega^{T}S_{b}\omega \\ 69 | s.t. \quad \omega^{T}S_{\omega}\omega=1 70 | $$ 71 | 由拉格朗日法,上式可得: 72 | $$ 73 | S_{b}\omega=\lambda S_{\omega}\omega \\ 74 | 即有,S_{\omega}^{-1}S_{b}\omega=\lambda \omega 75 | $$ 76 | 至此,我们的**优化目标就转化成了求矩阵 $S_{\omega}^{-1}S_{b}$ 的特征值,而投影方向就是这个特征值对应的特征向量**。 77 | 78 | > 由于 $(\mu_{1}-\mu_{2})^{T}\omega$ 是个标量(因为 $\mu_{1}-\mu_{2}$ 和 $\omega$ 同向时才能保证类间距离最大), 79 | > 80 | > 所以,对于 $S_{b}\omega=(\mu_{1}-\mu_{2})(\mu_{1}-\mu_{2})^{T}\omega$ 而言,可以看出 $S_{b}\omega$ 始终与 $(\mu_{1}-\mu_{2})$ 的方向一致 81 | 82 | 因此,如果只考虑 $\omega$ 的长度而不考虑方向,则由: 83 | $$ 84 | S_{\omega}^{-1}S_{b}\omega=\lambda \omega \qquad => \qquad \omega=S_{\omega}^{-1}(\mu_{1}-\mu_{2}) 85 | $$ 86 | **也就是说,我们只需求出样本的均值和类内的散度矩阵(即类内方差),即可求出投影方向。** 87 | 88 | 89 | 90 | ## LDA算法流程(推广至高维) 91 | 92 | 1.计算每类样本的均值向量 $\mu_{i}$。 93 | 94 | 2.计算类间散度矩阵 $S_{\omega}$ 和类内散度矩阵 $S_{b}$ 。 95 | 96 | 3.求矩阵 $S_{\omega}^{-1}S_{b}$ 的特征值即对应的特征向量,从大到小排序。 97 | 98 | 4.将特征值由大到小排列,取出前 k 个特征值对应的特征向量。 99 | 100 | 5.将 n 维样本映射到 k 维,实现降维处理。 101 | $$ 102 | x_{i}^{'}=\begin{bmatrix}\omega_{1}^{T}x_{i}\\\omega_{2}^{T}x_{i}\\\vdots \\\omega_{k}^{T}x_{i} \end{bmatrix}\\ 103 | $$ 104 | 105 | ## 总结 106 | 107 | ● LDA是线性的、有监督的降维方法,其优点是善于对有类别信息的数据进行降维处理(与PCA的不同)。 108 | 109 | ● LDA因为是线性模型,对噪声的鲁棒性较好,但由于模型简单,对数据特征的表达能力不足。 110 | 111 | ● LDA对数据的分布做了一些很强的假设,比如每个类别都是高斯分布、各个类别的协方差相等,实际中这些假设很难完全满足。 112 | 113 | > 关于LDA与PCA的区别,请看下回分解。 114 | 115 | 116 | 117 | ## 参考资料 118 | 119 | 周志华《机器学习》 120 | 121 | 葫芦娃《百面机器学习》 -------------------------------------------------------------------------------- /机器学习/46_PCA与LDA比较.md: -------------------------------------------------------------------------------- 1 | ## 问题 2 | 3 | 在前面总结的机器学习问题44和45中已经分别总结了 `PCA` 和 `LDA` 两种降维方法,接下来就对这两种方法一起做个对比和总结吧。 4 | 5 | ## PCA与LDA的比较 6 | 7 | ### 异同点 8 | 9 | #### ● 相同点: 10 | 11 | ① 均是降维方法 12 | 13 | ② 降维时均使用了矩阵特征分解的思想 14 | 15 | ③ 两者都假设数据符合高斯分布 16 | 17 | #### ● 不同点: 18 | 19 | ① PCA是无监督的降维方法,而LDA是有监督的降维方法 20 | 21 | ② LDA除了可以降维,还可以用于分类 22 | 23 | ③ LDA降维最多降到类别数 `k-1`的维数(k是样本类别的个数),而PCA没有这个限制。 24 | 25 | ④ LDA选择的是分类性能最好的投影方向,而PCA选择样本点投影具有最大方差的方向 26 | 27 | > ● 关于第3个点: 28 | > 29 | > 可能不太好理解,如果要搞清楚的话,可以参考文末的博客中 **第4小节-多类LDA原理** 中的最后一段。 30 | > 31 | > ● 关于第4点,可以这样理解: 32 | > 33 | > LDA在降维过程中最小化类内距离,即同类样本的方差尽可能小,同时最大化类间距离,即异类样本尽可能分离,这本身是也为分类任务服务的;而PCA是无监督的降维方法,其假设方差越大,信息量越多,因此会选择样本点投影具有最大方差的方向。 34 | 35 | 36 | 37 | ### 优缺点 38 | 39 | #### ● LDA优点: 40 | 41 | ① 降维过程中可以使用类别的先验知识(有监督的),而PCA是无监督的 42 | 43 | ② 在样本分类信息依赖均值而不是方差的时候,LDA算法优于PCA算法 44 | 45 | #### ● LDA缺点: 46 | 47 | ① LDA不适合对非高斯分布样本进行降维,PCA也有这个问题。 48 | 49 | ② LDA在样本分类信息依赖方差而不是均值的时候,降维效果不好。 50 | 51 | ③ LDA降维最多降到类别数k-1的维数,如果我们降维的维度大于k-1,则不能使用LDA。 52 | 53 | ④ LDA可能过度拟合数据。 54 | 55 | 56 | 57 | #### ● PCA优点: 58 | 59 | ①它是无监督学习算法,完全无参数限制。 60 | 61 | ② 在样本分类信息依赖方差而不是均值的时候,PCA算法优于LDA算法 62 | 63 | #### ● PCA缺点: 64 | 65 | ①特征值分解有一些局限性,比如变换的矩阵必须是方阵 66 | 67 | ②如果用户对观测对象有一定的先验知识,掌握了数据的一些特征,却无法通过参数化等方法对处理过程进行干预,可能会得不到预期的效果,效率也不高 68 | 69 | 70 | 71 | ## 参考资料 72 | 73 | [线性判别分析LDA原理总结](https://www.cnblogs.com/pinard/p/6244265.html) https://www.cnblogs.com/pinard/p/6244265.html -------------------------------------------------------------------------------- /机器学习/49_判别式模型和生成式模型.md: -------------------------------------------------------------------------------- 1 | ## 问题 2 | 3 | 判别式模型和生成式模型是机器学习中很重要的两个概念,也是笔试面试中很可能出现的问题,这里整理一遍是为了让自己更好地理解和区分这两者之间的不同。 4 | 5 | ## 概念 6 | 7 | 假设可观测到的变量集合为X,其他变量集合为Z,需要预测的变量集合为Y,则: 8 | 9 | **判别式模型:** 是指在给定X情况下,直接对**条件概率分布P(Y,Z|X)**进行建模来预测 Y。 10 | $$ 11 | P(Y|X)=\sum_{Z}P(Y,Z|X) 12 | $$ 13 | **生成式模型:** 是指先对**联合概率分布P(X, Y, Z)**进行建模,然后在给定X的情况下,通过计算边缘分布来预测 Y。 14 | $$ 15 | P(Y|X)=\frac{P(X,Y)}{P(X)}=\frac{\sum_{Z}P(X,Y,Z)}{\sum_{Y,Z}P(X,Y,Z)} 16 | $$ 17 | 18 | 19 | ## 比较 20 | 21 | 参考了一篇博客,总结自 Andrew Ng 在NIPS 2001年一篇关于判别式模型和生成式模型的比较的文章 22 | 23 | > On Discrimitive vs. Generative classifiers: A comparision of logistic regression and naive Bayes 24 | 25 | | 类别 | 判别式模型(Discriminative Model) | 生成式模型(Generative Model) | 26 | | :----------: | :----------------------------------------------------------- | :----------------------------------------------------------- | 27 | | **特点** | 寻找不同类别之间的最优分类面,反映的是**异类数据之间的差异** | 对后验概率建模,从统计的角度表示数据的分布情况,能够反映**同类数据本身的相似度** | 28 | | 联系 | 由生成式模型可以得到判别式模型, | 但由判别式模型得不到生成式模型 | 29 | | 本质区别 | 对**条件概率分布P(Y\|X)**建模 | 对**联合概率分布P(X,Y)**建模 | 30 | | **常见模型** | Linear Regression
Logistic Regression
SVM
KNN
神经网络
线性判别分析(LDA)
最大熵模型
条件随机场(CRF) | 贝叶斯网络
朴素贝叶斯
隐马尔科夫模型(HMM)
高斯混合模型(GMM)
文档主题生成模型(LDA)
pLSA | 31 | | 优点 | ①性能相对于生成式更简单,更容易学习
②适用较多类别的识别
③能清晰分辨出多类或一类与其他类之间的差异特征 | ①能用于数据不完整的情况
②研究单类问题比判别式灵活
③实际上带的东西要比判别式更丰富
④模型可以通过增量学习得到 | 32 | | 缺点 | **不能反映数据本身的特性** | 学习和计算过程比较复杂 | 33 | | 性能 | 较好(原因是利用了训练数据的类别标识信息) | 较差 | 34 | | 主要应用 | ①图像和文本分类 Image and document classification
②生物序列分析 Biosequence analysis
③时间序列预测 Time series prediction | ①NLP
②医学诊断(Medical Diagnosis) | 35 | 36 | ### 补充1 37 | 38 | *摘取另一个博主的观点*: [判别式模型与生成式模型](https://www.cnblogs.com/yejintianming00/p/9378810.html) 39 | 40 | | 类别 | 判别式模型(Discriminative Model) | 生成式模型(Generative Model) | 41 | | -------------- | -------------------------------- | ------------------------------ | 42 | | 模型错误率 | 较低 | 更高 | 43 | | 检测异常值 | 不能 | 能(因为模型学习了所有的分布) | 44 | | 利用无标签数据 | 不能 | 能(如DBN网络) | 45 | 46 | ### 补充2 47 | 48 | 摘取自《统计学习方法》 49 | 50 | **判别式模型的特点**:判别方法直接学习的是条件概率 $P(Y|X)$ 或决策函数 $f(X)$ ,直接面对预测,往往学习的准确率更高;由于直接学习 $P(Y|X)$ 或决策函数 $f(X)$ ,可以对数据进行各种程度上的抽象、定义特征并使用特征,因此可以简化学习问题。 51 | 52 | **生成式模型的特点**:生成式方法可以还原出联合概率分布 $P(X,Y)$ ,而判别式不能;生成式学习方法收敛速度更快,即当样本容量增加的时候,学到的模型可以更快收敛于真实模型(对此我的理解是在增加样本之前,模型已经学得数据的整体分布,因此当新样本进来后,很容易确定新样本所属的类别);当存在隐变量时,仍可以使用生成式模型,但是判别式模型不可使用 53 | 54 | #### 参考资料 55 | 56 | [Generative Model 与 Discriminative Model](https://www.cnblogs.com/xiapeiyong/archive/2009/05/15/1457888.html) 57 | [判别式模型与生成式模型](https://www.cnblogs.com/yejintianming00/p/9378810.html) -------------------------------------------------------------------------------- /机器学习/58_线性回归损失函数为什么要用平方形式.md: -------------------------------------------------------------------------------- 1 | ## 问题 2 | 3 | 线性回归损失函数为什么要用平方形式? 4 | 5 | ## 问题背景 6 | 7 | 这是在阿里一面中遇到的问题,当时我的回答是损失函数是是模型预测值与真实值之间的一种距离度量,我们可以计算出每个样本的预测值与真实值之间的距离,全部加起来就得到了所谓的损失函数。而距离的度量可以采用预测值与真实值之间差的绝对值,或者两者之差的平方,当然更高次的也行,只要你喜欢。正如问题所述,为什么我们一般使用的是两者之差的平方而不是两者只差的绝对值呢?其实这与模型的求解相关,举最简单的线性回归为例,如果采用的距离是两者之差的绝对值,那么求解的目标函数如下: 8 | $$ 9 | (\omega^*, b) = arg min_{(\omega, b)}\sum_{i=1}^{m}\left|{f(x_i)-y_i}\right| 10 | $$ 11 | 如果采用的距离是两者之差的平方,那么求解的目标函数如下: 12 | $$ 13 | (\omega^*, b) = arg min_{(\omega, b)}\sum_{i=1}^{m}({f(x_i)}-y_i)^2 14 | $$ 15 | 其中:$f(x_i) = \omega x_i + b$ 即预测值,$y_i$ 为真实值,$m$ 为样本总数,$\omega$ 和 $b$ 为要求解的参数 16 | 17 | 要求得使以上损失函数最小化对应的那个 $\omega$ 和 $b$ ,可将损失函数对 $\omega$ 和 $b$ 求导,并令导数为0。但是当采取的距离是两者之差的绝对值时,函数在0处不可导,且还增加的一个工作量是需要判断 $f(x_i)-y_i$ 正负号。而采用的距离是两者之差的平方时就没有这方面的问题,所以解决回归问题的时候一般使用平方损失。但理论上两者都可以使用,只是如果用两者之差的绝对值的话,那么需要判断和处理的东西多点,例如人为设定0处的导数为0等等。 18 | 19 | 但其实这样的回答是不对的,下面给出一个大佬的解答: 20 | 21 | ## 问题解答 22 | 23 | 其实是**因为最小化平方误差本质上等同于在误差服从高斯分布的假设下的最大似然估计。**(这句话确实不好理解,我也理解不了,就先这么记着吧,说不定啥时候就理解了呢……) 24 | 25 | 线性回归,简单点来说就是对于训练数据样本 $(x_i, y_i)$ ,预测值 $\hat{y_i} = \theta_0+\theta_1*x_i$ ,并构建损失函数: 26 | $$ 27 | J = \sum_{i=1}^{n}{(y_i-\hat{y_i})^2} 28 | $$ 29 | 损失函数表示的是每个训练数据点 $(x_i, y_i)$ 到拟合直线 $\hat{y_i} = \theta_0+\theta_1*x_i$ 的竖直距离的平方和,通过最小化这个损失函数来求得拟合直线的最佳参数 $\theta$ 。那么损失函数为什么要用均方误差的形式,而不是绝对值形式,一次方,三次方,或者四次方形式呢? 30 | 31 | 简单来说,是因为使用平方形式的时候,使用的是“最小二乘法”的思想,这里的二乘指的是平方的形式来度量预测值与真实值之间的距离,“最小”指的是求得的最佳参数 $\theta$ 要保证预测值与真实值之间距离的平方和最小。 32 | 33 | 最小二乘法以预测值与真实值距离的平方和作为损失函数,在误差服从正态分布的前提下,与极大似然估计的思想在本质上是相同的(哈哈哈不太理解……)。 34 | 35 | 我们设真实值与预测值之间的误差为: 36 | $$ 37 | \varepsilon_i = y_i - \hat{y_i} 38 | $$ 39 | 我们通常认为误差 $\varepsilon$ 服从标准正态分布$(\mu = 0, \sigma^2 = 1)$ ,即给定一个 $x_i$ ,模型输出真实值为 $y_i$ 的概率为: 40 | $$ 41 | p(y_i |x_i) = \frac{1}{\sqrt{2\pi}}*exp(-\frac{\varepsilon_i^2}{2}) 42 | $$ 43 | 进一步我们假设数据集中N个样本点之间相互独立,则给定所有 $x$ 输出所有真实值 $y$ 的概率即似然Likeihood,为所有 $p(y_i|x_i)$ 的累乘: 44 | $$ 45 | L(x,y) = \prod_{i=1}^{n}\frac{1}{\sqrt{2\pi}}*exp[-\frac{\varepsilon_i^2}{2}] 46 | $$ 47 | 取对数似然函数得: 48 | $$ 49 | log[L(x,y)] = - \frac{n}{2}log{2\pi}-\frac{1}{2}\sum_{i=1}^{n}\varepsilon_i^2 50 | $$ 51 | 去掉与 $\hat{y_i}$ 无关的第一项,然后转化为最小化负对数似然: 52 | $$ 53 | neg\_log[L(x,y)] = \frac{1}{2}\sum_{i=1}^{n}\varepsilon_i^2=\frac{1}{2}\sum_{i=1}^{n}(y_i-\hat{y_i})^2 54 | $$ 55 | 可以看到这个实际上就是均方差损失的形式。也就是说**在模型输出与真实值的误差服从高斯分布的假设下,最小化均方差损失函数与极大似然估计本质上是一致的**,因此在这个假设能被满足的场景中(比如回归),均方差损失是一个很好的损失函数选择;当这个假设没能被满足的场景中(比如分类),均方差损失不是一个好的选择,这也解释了为什么在分类问题中不使用均方误差作为损失函数而是使用交叉熵的问题。 56 | 57 | 当然回答上面下划线部分的问题还可以举的一个例子是:假设有一个样本 $[x_1]$ 的真实标签为 $[0,0,1]$ ,那么预测得到概率中第三类的概率最大即说明分类正确,例如为 $[0.2, 0.2, 0.6]$ ,但是平方误差却过于严格,比如当预测结果是 $[0, 0.4, 0.6]$ ,虽然两者在交叉熵上的结果是一样的,但是平方误差中却差别很大。 58 | 59 | ## 参考资料 60 | 61 | [1、线性回归损失函数为什么要用平方形式](https://blog.csdn.net/saltriver/article/details/57544704) 62 | [2、机器学习常用损失函数小结](https://zhuanlan.zhihu.com/p/77686118) 63 | 64 | ​ By Yee 65 | ​ 2020.05.12 -------------------------------------------------------------------------------- /机器学习/59_逻辑回归与线性回归之间的异同.md: -------------------------------------------------------------------------------- 1 | ## 问题 2 | 3 | **逻辑回归**相比于**线性回归**,有何异同 4 | 5 | ## 问题背景 6 | 7 | 逻辑回归与线性回归是机器学习中非常常用和基础的模型,面试当中也会经常被问到,而两者有着很多共同点,就连名字都差不多一样,所以会被问道两者的区别也是太正常不过了。我们都知道,回归是对连续值的预测,那么“逻辑回归”是回归问题吗? 8 | 9 | 我们首先聊聊**逻辑回归这个名字的由来**:对逻辑回归公式进行整理,我们可以得到 $log{\frac{p}{1-p}} = \omega^T x+b$,其中$p = P(y=1|x)$ 也就是将给定输入`x`预测为正类的概率。如果将一个事件的几率(odd)定义为该事件发生的概率与不发生的概率的比值$\frac{p}{1-p}$,那么逻辑回归就可以看做是对 $y = 1 | x$ 这一事件的对数几率的线性回归,于是“逻辑回归”这个称谓就延续了下来。 10 | 11 | ## 问题解答 12 | 13 | **两者最本质的区别**:逻辑回归处理的是分类问题,而线性回归处理的是回归问题。 14 | 15 | **两者最大的区别**:逻辑回归将`y`视为因变量而不是$\frac{p}{1-p}$,而`y`是类别标签,如二分类中`y`可以是 0 或 1 ,是一个离散值,但线性回归中的因变量是连续值。 16 | 17 | **两者相同之处**: 18 | 19 | 1. 都是使用**极大似然估计法来估计模型参数**:这里可能会有点疑问是说线性回归中明明是使用均方误差即最小二乘法来估计的,不是极大似然估计呀。其实实际上在预测值与真实值之间的误差满足正态分布的假设下,最小化均方误差与极大似然估计在本质上是一样的。 20 | 2. 二者在求解超参数的过程中,**都可以使用梯度下降的方法**,这也是监督学习中一个常见的相似之处 21 | 22 | 以上内容参考自《百面机器学习》的逻辑回归一节 23 | 24 | 25 | 26 | ## 拓展 27 | 28 | 这里的拓展主要是线性回归与逻辑回归的模型定义与求解过程。 29 | 30 | ### 线性回归 31 | 32 | 线性回归试图学得 33 | $$ 34 | f(x_i) = \omega x_i + b,使得f(x_i)\approx y_i \\ 其中y_i为真实值,而f(x_i)为预测值 35 | $$ 36 | 那怎么去确定参数 $\omega$ 和 $b$ 呢,显然关键在于怎么去衡量 $f(x_i)$ 与 $y_i$ 之间的距离,而在回归任务中一般使用均方误差,因此我们的目标变为让均方误差最小化: 37 | $$ 38 | (\omega^*, b) = arg min_{(\omega, b)}\sum_{i=1}^{m}({f(x_i)}-y_i)^2 39 | $$ 40 | 接下来的模型参数求解可以对 $\omega$ 与 $b$ 进行求导或者使用梯度下降法。 41 | 42 | ### 逻辑回归 43 | 44 | 在线性回归的基础上,逻辑回归可以理解为只需找到一个单调可微函数将线性模型的预测值与把该输入归类为正类的概率$ p = P(y = 1 | x)$ 联系起来即可,即将线性回归得到的连续值映射为二分类的类别0或1,因此我们自然而然就想到了sigomid函数,它的函数形式如下: 45 | $$ 46 |    y = \frac{1}{1+e^{-z}} \\ 我们令z = \omega^Tx + b,令y为把该输入x归类为为正类的概率p,便可以得到:\\ p = \frac{1}{1+e^{-(\omega^T+b)}} 47 | $$ 48 | 对以上公式稍做变换就可以得到 49 | $$ 50 | p = P(y=1|x)= \frac{e^{\omega^Tx+b}}{1+e^{\omega^Tx+b}} \\ 1-p = P(y=0|x)= \frac{1}{1+e^{\omega^Tx+b}} \\ log{\frac{p}{1-p}} = \omega^T x+b 51 | $$ 52 | 下面使用极大似然估计来估计模型参数, 53 | 54 | ![](/media/yee/Files/找工作相关学习资料/面试问题答案库/机器学习面试问题/链接图片/557264115.jpg) 55 | 56 | 以上内容参考周志华的西瓜书《机器学习》和李航的《统计学习方法》 57 | 58 | ​ By Yee 59 | ​ 2020.05.08 -------------------------------------------------------------------------------- /计算机视觉/02_过拟合和欠拟合的表现与解决方法.md: -------------------------------------------------------------------------------- 1 | ## 问题 2 | 3 | 过拟合和欠拟合的表现和解决方法。 4 | 5 | 其实除了欠拟合和过拟合,还有一种是**适度拟合**,适度拟合就是我们模型训练想要达到的状态,不过适度拟合这个词平时真的好少见,在做酷狗音乐的笔试题时还懵逼了一会,居然还真的有这样的说法。 6 | 7 | 这应该是基础中的基础了,笔试题都做烂了。那就当做今天周末,继续放个假吧…… 8 | 9 | ![](https://i.loli.net/2020/05/16/m1MXWUG6RZEfB2H.jpg) 10 | 11 | ## 过拟合 12 | 13 | ### 过拟合的表现 14 | 15 | 模型在训练集上的表现非常好,但是在测试集、验证集以及新数据上的表现很差,损失曲线呈现一种**高方差**状态。(高方差指的是训练集误差较低,而测试集误差比训练集大较多) 16 | 17 | img 18 | 19 | ### 过拟合的原因 20 | 21 | 从两个角度去分析: 22 | 23 | 1. **模型的复杂度**:模型过于复杂,把噪声数据的特征也学习到模型中,导致模型泛化性能下降 24 | 2. **数据集规模大小**:数据集规模相对模型复杂度来说太小,使得模型过度挖掘数据集中的特征,把一些不具有代表性的特征也学习到了模型中。例如训练集中有一个叶子图片,该叶子的边缘是锯齿状,模型学习了该图片后认为叶子都应该有锯齿状边缘,因此当新数据中的叶子边缘不是锯齿状时,都判断为不是叶子。 25 | 26 | ### 过拟合的解决方法 27 | 28 | 1. **获得更多的训练数据**:使用更多的训练数据是解决过拟合问题最有效的手段,因为更多的样本能够让模型学习到更多更有效的特征,减少噪声的影响。 29 | 30 | 当然直接增加实验数据在很多场景下都是没那么容易的,因此可以通过**数据扩充技术**,例如对图像进行平移、旋转和缩放等等。 31 | 32 | 除了根据原有数据进行扩充外,还有一种思路是使用非常火热的**生成式对抗网络 GAN **来合成大量的新训练数据。 33 | 34 | 还有一种方法是使用**迁移学习技术**,使用已经在更大规模的源域数据集上训练好的模型参数来初始化我们的模型,模型往往可以更快地收敛。但是也有一个问题是,源域数据集中的场景跟我们目标域数据集的场景差异过大时,可能效果会不太好,需要多做实验来判断。 35 | 36 | 2. **降低模型复杂度**:在深度学习中我们可以减少网络的层数,改用参数量更少的模型;在机器学习的决策树模型中可以降低树的高度、进行剪枝等。 37 | 38 | 3. **正则化方法**如 L2 将权值大小加入到损失函数中,根据奥卡姆剃刀原理,拟合效果差不多情况下,模型复杂度越低越好。至于为什么正则化可以减轻过拟合这个问题可以看看[这个博客](https://blog.csdn.net/qq_37344125/article/details/104326946),挺好懂的.。 39 | 40 | **添加BN层**(这个我们专门在BN专题中讨论过了,BN层可以一定程度上提高模型泛化性能) 41 | 42 | 使用**dropout技术**(dropout在训练时会随机隐藏一些神经元,导致训练过程中不会每次都更新(**预测时不会发生dropout**),最终的结果是每个神经元的权重w都不会更新的太大,起到了类似L2正则化的作用来降低过拟合风险。) 43 | 44 | 4. **Early Stopping**:Early stopping便是一种迭代次数截断的方法来防止过拟合的方法,即在模型对训练数据集迭代收敛之前停止迭代来防止过拟合。 45 | 46 | Early stopping方法的具体做法是:在每一个Epoch结束时(一个Epoch集为对所有的训练数据的一轮遍历)计算validation data的accuracy,当accuracy不再提高时,就停止训练。这种做法很符合直观感受,因为accurary都不再提高了,在继续训练也是无益的,只会提高训练的时间。那么该做法的一个重点便是怎样才认为validation accurary不再提高了呢?并不是说validation accuracy一降下来便认为不再提高了,因为可能经过这个Epoch后,accuracy降低了,但是随后的Epoch又让accuracy又上去了,所以不能根据一两次的连续降低就判断不再提高。一般的做法是,在训练的过程中,记录到目前为止最好的validation accuracy,当连续10次Epoch(或者更多次)没达到最佳accuracy时,则可以认为accuracy不再提高了。 47 | 48 | 5. **集成学习方法**:集成学习是把多个模型集成在一起,来降低单一模型的过拟合风险,例如Bagging方法。 49 | 50 | 如DNN可以用Bagging的思路来正则化。首先我们要对原始的m个训练样本进行有放回随机采样,构建N组m个样本的数据集,然后分别用这N组数据集去训练我们的DNN。即采用我们的前向传播算法和反向传播算法得到N个DNN模型的W,b参数组合,最后对N个DNN模型的输出用加权平均法或者投票法决定最终输出。不过用集成学习Bagging的方法有一个问题,就是我们的DNN模型本来就比较复杂,参数很多。现在又变成了N个DNN模型,这样参数又增加了N倍,从而导致训练这样的网络要花更加多的时间和空间。因此一般N的个数不能太多,比如5-10个就可以了。 51 | 52 | 6. **交叉检验**,如S折交叉验证,通过交叉检验得到较优的模型参数,其实这个跟上面的Bagging方法比较类似,只不过S折交叉验证是随机将已给数据切分成S个互不相交的大小相同的自己,然后利用S-1个子集的数据训练模型,利用余下的子集测试模型;将这一过程对可能的S种选择重复进行;最后选出S次评测中平均测试误差最小的模型。 53 | 54 | ## 欠拟合 55 | 56 | ### 欠拟合的表现 57 | 58 | 模型无论是在训练集还是在测试集上的表现都很差,损失曲线呈现一种**高偏差**状态。(高偏差指的是训练集和验证集的误差都较高,但相差很少) 59 | 60 | img 61 | 62 | ### 欠拟合的原因 63 | 64 | 同样可以从两个角度去分析: 65 | 66 | 1. **模型过于简单**:简单模型的学习能力比较差 67 | 2. **提取的特征不好**:当特征不足或者现有特征与样本标签的相关性不强时,模型容易出现欠拟合 68 | 69 | ### 欠拟合的解决方法 70 | 71 | 1. **增加模型复杂度**:如线性模型增加高次项改为非线性模型、在神经网络模型中增加网络层数或者神经元个数、深度学习中改为使用参数量更多更先进的模型等等。 72 | 2. **增加新特征**:可以考虑特征组合等特征工程工作(这主要是针对机器学习而言,特征工程还真不太了解……) 73 | 3. 如果损失函数中加了正则项,可以考虑**减小正则项的系数** $\lambda$ 74 | 75 | ## 参考资料 76 | 77 | [过拟合与欠拟合及方差偏差](https://www.jianshu.com/p/f2489ccc14b4) (这个博客总结地很好,可以看看) 78 | 《百面机器学习》 79 | [机器学习+过拟合和欠拟合+方差和偏差](https://blog.csdn.net/u012197749/article/details/79766317) 80 | [如何判断欠拟合、适度拟合、过拟合](https://blog.csdn.net/GL3_24/article/details/90294490) 81 | 82 | -------------------------------------------------------------------------------- /计算机视觉/05_NMS详细工作机制及代码实现.md: -------------------------------------------------------------------------------- 1 | ## 问题 2 | 3 | 看到一句话:NMS都不懂,还做什么Detection ! 虎躯一震……懂是大概懂,但代码能写出来吗??? 4 | 5 | 在目标检测网络中,产生 proposal 后使用分类分支给出每个框的每类置信度,使用回归分支修正框的位置,最终会使用 NMS 方法去除**同个类别**当中 IOU 重叠度较高且 scores 即置信度较低的那些检测框。 6 | 7 | 下图就是在目标检测中 NMS 的使用效果:emmm大概就是能让你更无遮挡地看到美女的脸吧hhhh 8 | 9 | ![《手撕非极大值抑制算法NMS与soft-NMS》](https://i.loli.net/2020/05/15/gtXrQCvwy25bOhM.jpg) 10 | 11 | ## 背景知识 12 | 13 | NMS (Non-maximum suppression) 非极大值抑制,即抑制不是极大值的检测框,根据什么去抑制?在目标检测领域,当然是根据 IOU (Intersection over Union) 去抑制。下图是绿色检测框与红色检测框的 IOU 计算方法: 14 | 15 | ![img](https://i.loli.net/2020/05/15/GHivYzUtdSJrZNE.png) 16 | 17 | 18 | 19 | ## NMS 原理及示例 20 | 21 | 注意 NMS 是针对一个特定的类别进行操作的。例如假设一张图中有要检测的目标有“人脸”和“猫”,没做NMS之前检测到10个目标框,每个目标框变量表示为: $[x_1,y_1,x_2,y_2,score_1,score_2]$ ,其中 $(x_1,y_1)$ 表示该框左上角坐标,$(x_2,y_2)$ 表示该框右下角坐标,$score_1$ 表示"人脸"类别的置信度,$score_2$ 表示"猫"类别的置信度。当 $score_1$ 比 $score_2$ 大时,将该框归为“人脸”类别,反之归为“猫”类别。最后我们假设10个目标框中有6个被归类为“人脸”类别。 22 | 23 | 接下来演示如何对“人脸”类别的目标框进行 NMS 。 24 | 25 | 首先对6个目标框按照 $score_1$ 即置信度降序排序: 26 | 27 | | 目标框 | score_1 | 28 | | :----: | :-----: | 29 | | A | 0.9 | 30 | | B | 0.85 | 31 | | C | 0.7 | 32 | | D | 0.6 | 33 | | E | 0.4 | 34 | | F | 0.1 | 35 | 36 | (1) 取出最大置信度的那个目标框 A 保存下来 37 | (2) 分别判断 B-F 这5个目标框与 A 的重叠度 IOU ,如果 IOU 大于我们预设的阈值(一般为 0.5),则将该目标框丢弃。假设此时丢弃的是 C和 F 两个目标框,这时候该序列中只剩下 B D E 这三个。 38 | (3) 重复以上流程,直至排序序列为空。 39 | 40 | ## 代码实现 41 | 42 | ```python 43 | # bboxees维度为 [N, 4],scores维度为 [N, 1],均为np.array() 44 | def single_nms(self, bboxes, scores, thresh = 0.5): 45 | # x1、y1、x2、y2以及scores赋值 46 | x1 = bboxes[:, 0] 47 | y1 = bboxes[:, 1] 48 | x2 = bboxes[:, 2] 49 | y2 = bboxes[:, 3] 50 | 51 | # 计算每个检测框的面积 52 | areas = (x2 - x1 + 1) * (y2 - y1 + 1) 53 | 54 | # 按照 scores 置信度降序排序, order 为排序的索引 55 | order = scores.argsort() # argsort为python中的排序函数,默认升序排序 56 | order = order[::-1] # 将升序结果翻转为降序 57 | 58 | # 保留的结果框索引 59 | keep = [] 60 | 61 | # torch.numel() 返回张量元素个数 62 | while order.size > 0: 63 | if order.size == 1: 64 | i = order[0] 65 | keep.append(i) 66 | break 67 | else: 68 | i = order[0] # 在pytorch中使用item()来取出元素的实值,即若只是 i = order[0],此时的 i 还是一个 tensor,因此不能赋值给 keep 69 | keep.append(i) 70 | 71 | # 计算相交区域的左上坐标及右下坐标 72 | xx1 = np.maximum(x1[i], x1[order[1:]]) 73 | yy1 = np.maximum(y1[i], y1[order[1:]]) 74 | xx2 = np.minimum(x2[i], x2[order[1:]]) 75 | yy2 = np.minimum(y2[i], y2[order[1:]]) 76 | 77 | # 计算相交的面积,不重叠时为0 78 | w = np.maximum(0.0, xx2 - xx1 + 1) 79 | h = np.maximum(0.0, yy2 - yy1 + 1) 80 | inter = w * h 81 | 82 | # 计算 IOU = 重叠面积 / (面积1 + 面积2 - 重叠面积) 83 | iou = inter / (areas[i] + areas[order[1:]] - inter) 84 | 85 | # 保留 IOU 小于阈值的 bboxes 86 | inds = np.where(iou <= thresh)[0] 87 | if inds.size == 0: 88 | break 89 | order = order[inds + 1] # 因为我们上面求iou的时候得到的结果索引与order相比偏移了一位,因此这里要补回来 90 | return keep # 这里返回的是bboxes中的索引,根据这个索引就可以从bboxes中得到最终的检测框结果 91 | ``` 92 | 93 | ## 参考资料 94 | 95 | [NMS算法详解(附Pytorch实现代码)](https://zhuanlan.zhihu.com/p/54709759) 96 | [非极大值抑制(Non-Maximum Suppression,NMS)](https://www.bbsmax.com/A/A2dmV1YOze/) 97 | 98 | ​ By Yee 99 | ​ 2020.05.16 -------------------------------------------------------------------------------- /计算机视觉/06_三种常见的激活函数.md: -------------------------------------------------------------------------------- 1 | ## 问题 2 | 3 | 在笔试问答题或面试中偶尔有涉及到激活函数的问题,这里简单总结一下深度学习中常见的三种激活函数sigmoid、tanh和ReLU,以及它们各自的特点和用途。 4 | 5 | ## 激活函数 6 | 7 | ### 激活函数的作用是什么? 8 | 9 | 激活函数的主要作用是在神经网络中**引入非线性因素**。 10 | 11 | ### 常见的三种激活函数 12 | 13 | | | sigmoid | tanh | ReLU | 14 | | :------: | :----------------------------------------------------------: | :----------------------------------------------------: | :----------------------------------------------------------: | 15 | | 公式 | $f(x)=\frac{1}{1+e^{-x}}$ | $f(x)=\frac{e^x-e^{-x}}{e^x+e^{-x}}$ | $f(x)=max(0,x)$ | 16 | | 导数 | $f'(x)=f(x)(1-f(x))$ | $f'(x)=1-f^2(x)$ | $f'(x)=\begin{cases}1,x>0\\0,x\leq0\end{cases}$ | 17 | | 梯度消失 | 容易造成 | 也容易造成,但优于sigmoid | 可以减缓,优于前两者 | 18 | | 常见应用 | 二分类任务 | **RNN网络** | CNN网络 | 19 | | 优点 | 函数平滑,容易求导 | ①函数平滑,容易求导
②输出关于零点对称 | ①求导更快,收敛更快
②有效缓解了梯度消失问题
③增加网络的稀疏性 | 20 | | 缺点 | ①容易造成梯度消失
②存在幂运算,计算量大
③其输出不关于零点对称 | ①容易造成梯度消失
②同样存在计算量大的问题 | 容易造成神经元的“死亡” | 21 | | 图形 | ![](https://i.loli.net/2020/06/08/HIX7TKyU2MsqlbV.png) | ![](https://i.loli.net/2020/06/08/9DEFnfop1qmNM7T.png) | ![](https://i.loli.net/2020/06/08/Nc2aBh3O5pEdk4Y.png) | 22 | 23 | 24 | 25 | ## 拓展 26 | 27 | ### 相比于sigmoid函数,tanh激活函数输出关于“零点”对称的好处是什么? 28 | 29 | 对于sigmoid函数而言,其输出始终为正,这会**导致在深度网络训练中模型的收敛速度变慢**,因为在反向传播链式求导过程中,权重更新的效率会降低(具体推导可以参考[这篇文章](https://www.zhihu.com/question/50396271?from=profile_question_card))。 30 | 31 | 此外,sigmoid函数的输出均大于0,作为下层神经元的输入会导致下层输入不是0均值的,随着网络的加深可能会使得原始数据的分布发生改变。而在深度学习的网络训练中,经常需要将数据处理成零均值分布的情况,以提高收敛效率,因此tanh函数更加符合这个要求。 32 | 33 | sigmoid函数的输出在[0,1]之间,比较适合用于二分类问题。 34 | 35 | ### 为什么RNN中常用tanh函数作为激活函数而不是ReLU? 36 | 37 | 详细分析可以参考[这篇文章](https://www.zhihu.com/question/61265076/answer/186347780)。下面简单用自己的话总结一下: 38 | 39 | RNN中将tanh函数作为激活函数本身就存在梯度消失的问题,而ReLU本就是为了克服梯度消失问题而生的,那为什么不能**直接**(注意:这里说的是直接替代,事实上通过**截断优化**ReLU仍可以在RNN中取得很好的表现)用ReLU来代替RNN中的tanh来作为激活函数呢?**这是因为ReLU的导数只能为0或1,而导数为1的时候在RNN中很容易造成梯度爆炸问题**。 40 | 41 | **为什么会出现梯度爆炸的问题呢?**因为在RNN中,每个神经元在不同的时刻都共享一个参数W(这点与CNN不同,CNN中每一层都使用独立的参数$W_i$),因此在前向和反向传播中,每个神经元的输出都会作为下一个时刻本神经元的输入,从某种意义上来讲相当于对其参数矩阵W作了连乘,如果W中有其中一个特征值大于1,则多次累乘之后的结果将非常大,自然就产生了梯度爆炸的问题。 42 | 43 | **那为什么ReLU在CNN中不存在连乘的梯度爆炸问题呢?**因为在CNN中,每一层都有不同的参数$W_i$,有的特征值大于1,有的小于1,在某种意义上可以理解为抵消了梯度爆炸的可能。 44 | 45 | ### 如何解决ReLU神经元“死亡”的问题? 46 | 47 | ①采用Leaky ReLU等激活函数 ②设置较小的学习率进行训练 ③使用momentum优化算法动态调整学习率 48 | 49 | 50 | 51 | ## 参考资料 52 | 53 | [最全最详细的常见激活函数总结(sigmoid、Tanh、ReLU等)及激活函数面试常见问题总结](https://blog.csdn.net/neo_lcx/article/details/100122938) -------------------------------------------------------------------------------- /计算机视觉/07_ReLU函数在0处不可导,为什么还能用.md: -------------------------------------------------------------------------------- 1 | ## 问题 2 | 3 | ReLU函数在0处不可导,为什么在深度学习网络中还这么常用? 4 | 5 | ## 问题背景 6 | 7 | 这是在阿里的机器学习岗一面的时候问的一个问题,最开始的问题是“为什么机器学习中解决回归问题的时候一般使用平方损失(即均方误差)?”。 8 | 9 | 当时我的回答是损失函数是是模型预测值与真实值之间的一种距离度量,我们可以计算出每个样本的预测值与真实值之间的距离,全部加起来就得到了所谓的损失函数。而距离的度量可以采用预测值与真实值之间差的绝对值,或者两者之差的平方,当然更高次的也行,只要你喜欢。正如问题所述,为什么我们一般使用的是两者之差的平方而不是两者只差的绝对值呢?其实这与模型的求解相关,举最简单的线性回归为例,如果采用的距离是两者之差的绝对值,那么求解的目标函数如下: 10 | $$ 11 | (\omega^*, b) = arg min_{(\omega, b)}\sum_{i=1}^{m}\left|{f(x_i)-y_i}\right| 12 | $$ 13 | 如果采用的距离是两者之差的平方,那么求解的目标函数如下: 14 | $$ 15 | (\omega^*, b) = arg min_{(\omega, b)}\sum_{i=1}^{m}({f(x_i)}-y_i)^2 16 | $$ 17 | 其中:$f(x_i) = \omega x_i + b$ 即预测值,$y_i$ 为真实值,$m$ 为样本总数,$\omega$ 和 $b$ 为要求解的参数 18 | 19 | 要求得使以上损失函数最小化对应的那个 $\omega$ 和 $b$ ,可将损失函数对 $\omega$ 和 $b$ 求导,并令导数为0。但是当采取的距离是两者之差的绝对值时,函数在0处不可导,且还增加的一个工作量是需要判断 $f(x_i)-y_i$ 正负号。而采用的距离是两者之差的平方时就没有这方面的问题,所以解决回归问题的时候一般使用平方损失。但理论上两者都可以使用,只是如果用两者之差的绝对值的话,那么需要判断和处理的东西多点,例如人为设定0处的导数为0等等。 20 | 21 | **(以上是我当时给面试官的答案,目前还没验证对错,权当参考)** 22 | 23 | 回答完以上问题之后,面试官就自然而然地引出了本节要讨论的问题“ReLU函数在0处不可导,为什么在深度学习网络中还这么常用?“ 24 | 25 | ## 问题解答 26 | 27 | ReLU函数在0处确实不可导,但是在实际中却被大量使用也是事实,为什么?因为真的好用,10亿AI调参侠都在用,用了都说好。但它在0处不可导,怎么办?**其实我们可以人为提供一个伪梯度,例如给它定义在0处的导数为0,其实tensorflow在实现ReLU的时候也是定义ReLU在0处的导数为0的。** 28 | 29 | 另外还有一个方法是使用 $ln(1+e^x)$ 来近似,这个函数是连续的,它在0点的导数是0.5。也就是相当于relu在0点的导数取为0.5,也正好是0和1的均值。 30 | 31 | 这里参考的资料有: 32 | [一、relu不可微为什么可用于深度学习](https://blog.csdn.net/ningyanggege/article/details/82493023) 33 | [二、激活函数RELU在0点的导数是多少?](http://sofasofa.io/forum_main_post.php?postid=1003784) 34 | 35 | ## 拓展 36 | 37 | ![](/media/yee/Files/找工作相关学习资料/面试问题答案库/计算机视觉面试问题/链接图片/20190311150821756.png) 38 | 39 | **ReLU的好用体现在哪呢?下面阐述下使用ReLU函数的优势** 40 | 41 | 1. ReLU函数的形式非常简洁,`ReLU = max(0, x)`,是由两段线性函数(大于0部分是线性的,小于0部分也是线性的,但是组合起来后却是非线性的)组成的非线性函数,函数形式看似简单,但ReLU的组合却可以逼近任何函数。 42 | 2. 其实ReLU提出的最大作用是解决sigmoid函数导致的**梯度消失**问题的(这里对于梯度消失就不拓展,留作下个问题再具体探讨),所以ReLU的优势大部分是与它的死对头sigmoid函数对比体现出来的,对比这两个函数的图形可以看出:ReLU有单侧抑制,即Relu会使一部分神经元的输出为0,这样就造成了网络的稀疏性,并且减少了参数的相互依存关系,缓解了过拟合问题的发生。另外这也更符合生物神经元的特征。 43 | 3. ReLU的运算速度快,这个很明显了,max肯定比幂指数快的多。 44 | 45 | 46 | 47 | **当然ReLU函数也是有缺点的**:可能会导致神经元死亡,权重无法更新的情况。这种神经元的死亡是不可逆转的死亡 48 | 49 | **解释**:训练神经网络的时候,一旦学习率没有设置好,第一次更新权重的时候,输入是负值,那么这个含有ReLU的神经节点就会死亡,再也不会被激活。因为:ReLU的导数在x>0的时候是1,在x<=0的时候是0。如果x<=0,那么ReLU的输出是0,那么反向传播中梯度也是0,权重就不会被更新,导致神经元不再学习。 50 | 也就是说,这个ReLU激活函数在训练中将不可逆转的死亡,导致了训练数据多样化的丢失。在实际训练中,如果学习率设置的太高,可能会发现网络中40%的神经元都会死掉,且在整个训练集中这些神经元都不会被激活。所以,设置一个合适的较小的学习率,会降低这种情况的发生。为了解决神经元节点死亡的情况,有人提出了Leaky ReLU、P-ReLu、R-ReLU、ELU等激活函数。 51 | 52 | 这里参考的资料有: 53 | [一、激活函数及其作用以及梯度消失、爆炸、神经元节点死亡的解释](https://blog.csdn.net/qq_17130909/article/details/80582226) 54 | 55 | ​ By Yee 56 | ​ 2020.05.07 57 | 58 | -------------------------------------------------------------------------------- /计算机视觉/08_Pooling层的作用以及如何进行反向传播.md: -------------------------------------------------------------------------------- 1 | ## 问题 2 | 3 | CNN网络在反向传播中需要逐层向前求梯度,然而**pooling层没有可学习的参数**,那它是如何进行反向传播的呢? 4 | 5 | 此外,CNN中为什么要加pooling层,它的作用是什么? 6 | 7 | ## Pooling层 8 | 9 | CNN一般采用average pooling或max pooling来进行池化操作,而池化操作会改变feature map的大小,例如大小为64×64的feature map使用2×2的步长池化后,feature map大小为32×32。因此,这会使得在反向传播中,pooling层的梯度无法与前一层相对应。 10 | 11 | 那怎么解决这个问题呢?其实也很简单,可以理解为就是pooling操作的一个逆过程,把一个像素的梯度传递给4个像素,保证传递的loss(或梯度)总和不变。下面分别来看average pooling和max pooling的反向传播操作过程。 12 | 13 | ### average pooling 14 | 15 | average pooling在前向传播中,就是把一个patch中的值取平均传递给下一层的一个像素。因此,**在反向传播中,就是把某个像素的值平均分成 n 份分配给上一层**。(!!注意这里是分成 n 份,而不是将该元素的值复制 n 份,不然会使得loss之和变为原来的 n 倍,造成梯度爆炸。) 16 | 17 | ![](https://i.loli.net/2020/05/15/vMwAtSe7fy5dXj4.jpg) 18 | 19 | ### max pooling 20 | 21 | max pooling在前向传播中,把一个patch中最大的值传递给下一层,其他值会被舍弃掉。因此,**在反向传播中,就是将当前梯度直接传递给前一层的某个像素,而让同一个patch中的其他像素值为0**。 22 | 23 | 所以,max pooling和average pooling不同的是,**max pooling在前向传播的时候要记录池化操作时哪个像素的值是最大的**,即max_id,在反向传播中才能将其对应起来。 24 | 25 | ![](https://i.loli.net/2020/05/15/TArNvOntfCB9Goj.jpg) 26 | 27 | 28 | 29 | > 总结: pooling层没有可学习的参数,在CNN的反向传播中,pooling层需要做的仅仅是将误差传递到上一 层,而没有计算梯度的过程。 30 | 31 | 32 | 33 | ## Pooling层的作用 34 | 35 | 两种pooling层的原理其实很容易就理解了,那它的作用又是什么呢, CNN中为什么要加pooling层?下面汇总一下几位大佬的解释: 36 | 37 | 1、**增加非线性** 38 | 39 | 2、**保留主要的特征同时减少参数(降维,效果类似PCA)和计算量,防止过拟合,提高模型泛化能力** 40 | 41 | 3、**invariance(不变性),这种不变性包括translation(平移),rotation(旋转),scale(尺度)** 42 | 43 | 44 | 45 | #### **①translation invariance(平移不变性):** 46 | 47 | 例如下面一个数字识别的例子,左边下图(大小为16×16)中的数字 1 比上图中的向右偏了一个单位,但是经过max pooling层之后,都变成了8×8的feature map。**平移不变性体现在,max pooling之后,原图中的a(或b)最终都会映射到相同的位置**(这句话的应该可以理解为原来feature map中的特征保持不变?比如a和b的位置不会错开,而是保持了相对位置从而保持了原来的主要特征)。 48 | 49 | 此外,图像主要的特征捕获到了,同时又将问题的规模从16×16降到了8×8(降维)。 50 | 51 | ![](https://i.loli.net/2020/05/15/hNB69TeSsXroWq3.jpg) 52 | 53 | #### ②rotation invariance(旋转不变性): 54 | 55 | 下图表示汉字“一”的识别,第一张相对于x轴有倾斜角,第二张是平行于x轴,两张图片相当于做了旋转,经过多次max pooling后具有相同的特征。 56 | 57 | ![](https://i.loli.net/2020/05/15/XtrVphe8WIC9x6n.jpg) 58 | 59 | **③scale invariance(尺度不变性):** 60 | 61 | 下图表示数字“0”的识别,第一张的“0”比较大,第二张的“0”进行了较小,相当于作了缩放,同样地,经过多次max pooling后具有相同的特征。 62 | 63 | ![](https://i.loli.net/2020/05/15/uGUTXKp6QmNlsZ2.jpg) 64 | 65 | 66 | 67 | **对③scale invariance(尺度不变性)的补充理解:**(来自另一位大佬,作为参考) 68 | 69 | **增大了感受野**!!! 怎么理解?比如上图中16×16的“0”,经过max pooling之后,可以用4×4的图表示了。 70 | 71 | 另外我们知道,CNN中利用卷积核进行卷积操作后,图像的的感受野会增大,那是不是一开始就用和图像大小一样的卷积核,获得的感受野更大,这样就更好呢?不是。因为卷积层越深模型的表征能力越强,如果直接用图像大小的卷积核就会得到1×1的feature map,一下子降维这么多,会导致很多重要信息丢失。 72 | 73 | 那如果多次卷积到最后也是要降维到1×1大小,信息不是一样丢失了吗?跟直接一次降维到1×1有什么区别吗?有区别的。因为如果每次只降维一些,逐渐降维虽然信息每次都会丢失一些,但每次卷积后表征的能力就会更强一些,到最后降到1×1的时候相比于直接降到1×1还是会强一些的。 74 | 75 | #### pooling的缺点: 76 | 77 | pooling能够增大感受野,让卷积能看到更多的信息,但是在降维的过程中也会丢失一部分信息(只留下了它认为重要的信息)。比如对segmentation要求的精度location会有一定的影响。 78 | 79 | 80 | 81 | ## 其他的pooling方法 82 | 83 | ### overlapping pooling(重叠池化) 84 | 85 | 重叠池化,就是相邻池化窗口之间会有重叠,即窗口大小大于步长sizeX>stride。 86 | 87 | ### Spatial Pyramid Pooling(空间金字塔池化) 88 | 89 | 空间金字塔池化的思想来源于SPPNet,用大小不同的池化窗口来作用于feature map,得到1×1、2×2和4×4的池化结果,如下图所见,假设卷积层有256个filter,那么可以得到1个256维的特征、4个256维的特征、16个256维的特征。 90 | 91 | 注意:这里的1×1、2×2和4×4不是池化窗口本身的大小,而是池化后将feature map分别划分为1×1、2×2和4×4个相同大小的子区域,而要得到这样的结果,就**需要根据图像的大小动态地计算出池化窗口的大小和步长**。 92 | $$ 93 | 计算方法:假设conv层输出为a*a,要得到n*n的池化结果,则有:\\ \\sizeX=\frac{a}{n},\ \ stride=\frac{a}{n} 94 | $$ 95 | 96 | > 若 $\frac{a}{n}$ 刚好取得整数,自然没有问题,例如假设a=13,要得到1×1pooling结果,只需令sizeX=13,stride=13即可。 97 | > 98 | > 但是当 $\frac{a}{n}$ 不能取整时,例如要得到2×2pooling结果,论文中给的sizeX=7,stride=6。(应该是对窗口大小sizeX稍作调整吧,然后采用重叠池化overlapping pooling的方法进行操作) 99 | 100 | **作用:CNN中加入SPP层之后,可以让CNN处理任意大小的输入,因而模型可以变得更加灵活。** 101 | 102 | 103 | 104 | ![](https://i.loli.net/2020/05/16/B81LE2Ijgsn5cF7.png) 105 | 106 | 107 | 108 | ### 参考资料 109 | 110 | [深度学习笔记(3)——CNN中一些特殊环节的反向传播](https://blog.csdn.net/qq_21190081/article/details/72871704) 111 | 112 | [CNN网络的pooling层有什么用?](https://www.zhihu.com/question/36686900/answer/130890492) 113 | 114 | [深度学习---之pooling层的作用与缺陷](https://blog.csdn.net/zxyhhjs2017/article/details/78607469) 115 | 116 | [池化方法总结](https://blog.csdn.net/danieljianfeng/article/details/42433475) 117 | 118 | -------------------------------------------------------------------------------- /计算机视觉/09_为什么输入网络前要对图像做归一化.md: -------------------------------------------------------------------------------- 1 | ## 问题 2 | 3 | 在将图像输入到深度学习网络之前,一般先对图像进行预处理,即图像归一化,为什么需要这么做呢? 4 | 5 | ## 问题背景 6 | 7 | 在面试的时候,面试官先问的问题是“机器学习中为什么要做特征归一化”,我的回答是“特征归一化可以消除特征之间量纲不同的影响,不然分析出来的结果显然会倾向于数值差别比较大的特征,另外从梯度下降的角度理解,数据归一化后,最优解的寻优过程明显会变得平缓,更容易正确的收敛到最优解”。接着面试官又问“图像的像素值都是在0到255之间,并不存在量纲的差别,那为什么还需要做归一化呢?”是啊,为什么还要呢,被问住了…… 8 | 9 | ## 拓展 10 | 11 | 既然是从机器学习特征归一化引出的图像的归一化问题,那么我们先讨论下“为什么要对数值型特征做归一化?”吧。 12 | 13 | 很多资料例如《百面机器学习》都是从梯度下降的角度来分析这个问题的,讨论地还不错的一篇是这个[知乎的回答](https://zhuanlan.zhihu.com/p/27627299),已经写得比较清晰了,所以这里就不再整理了,直接点开链接看。 14 | 15 | 不过这个回答里面未归一化时的损失函数等高线图中椭圆的方向应该是横向的而不是纵向的,因为 $\theta_2$ 前的系数比 $\theta_1$ 的大,所以在损失函数等高图上 $\theta_2$ 的变化范围比 $\theta_1$ 小才对,另外对于为什么圆形的等高线相对于椭圆形的等高线,更新方向更加平滑,所以更快也更容易收敛到最优解呢?一句话简单解释就是因为归一化后,等高图大致为圆形,更新方向与等高线垂直,所以理想的更新方向是直指圆心的一条直线。 16 | 17 | 所以对于“为什么机器学习中要进行特征归一化”这个问题,总结起来可以从三个点去回答: 18 | 19 | 1. **消除特征之间量纲的影响**,使得不同特征之间具有可比性 20 | 2. 在使用随机梯度下降求解的模型中,**能加快模型收敛速度** 21 | 3. **归一化还有可能提高精度**:一些分类器需要计算样本之间的距离(如欧氏距离),例如KNN。如果一个特征值域范围非常大,那么距离计算就主要取决于这个特征,从而与实际情况相悖(比如这时实际情况是值域范围小的特征更重要)。 22 | 23 | ## 问题解答 24 | 25 | 对于这个问题,网上看了很多博客,没有找到一个很全面权威的解释,所以这里把几个自认为讲得比较合理的解释列出来: 26 | 27 | 1. 灰度数据表示有两种方法:一种是uint8类型、另一种是double类型。其中uint8类型数据的取值范围为 [0,255],而double类型数据的取值范围为[0,1],两者正好相差255倍。对于double类型数据,其取值大于1时,就会表示为白色,不能显示图像的信息,故当运算数据类型为double时,为了显示图像要除255。 28 | 29 | 2. 图像深度学习网络也是使用gradient descent来训练模型的,使用gradient descent的都要在数据预处理步骤进行数据归一化,主要原因是,根据反向传播公式: 30 | $$ 31 | \frac{\partial J}{\omega_{11}} = x_1*后面层梯度的乘积 32 | $$ 33 | 如果输入层 x 很大,在反向传播时候传递到输入层的梯度就会变得很大。梯度大,学习率就得非常小,否则会越过最优。在这种情况下,学习率的选择需要参考输入层数值大小,而直接将数据归一化操作,能很方便的选择学习率。在未归一化时,输入的分布差异大,所以各个参数的梯度数量级不相同,因此,它们需要的学习率数量级也就不相同。对 w1 适合的学习率,可能相对于 w2 来说会太小,如果仍使用适合 w1 的学习率,会导致在 w2 方向上走的非常慢,会消耗非常多的时间,而使用适合 w2 的学习率,对 w1 来说又太大,搜索不到适合 w1 的解。 34 | 35 | 3. 通过标准化后,实现了数据中心化,数据中心化符合数据分布规律,能增加模型的泛化能力 36 | 37 | ## 问题深入 38 | 39 | 那么深度学习中在训练网络之前应该怎么做图像归一化呢?有两种方法: 40 | 41 | 1. **归一化到 0 - 1**:因为图片像素值的范围都在0~255,图片数据的归一化可以简单地除以255. 。 (注意255要加 . ,因为是要归一化到double型的 0-1 ) 42 | 2. **归一化到 [-1, 1]**:在深度学习网络的代码中,将图像喂给网络前,会先统计训练集中图像RGB这3个通道的均值和方差,如:`mean=[123.675, 116.28, 103.53], std=[58.395, 57.12, 57.375]`,接着对各通道的像素做减去均值并除以标准差的操作。不仅在训练的时候要做这个预处理,在测试的时候,同样是使用在训练集中算出来的均值与标准差进行的归一化。 43 | 44 | 注意两者的区别:归一化到 [-1, 1] 就不会出现输入都为正数的情况,如果输入都为正数,会出现什么情况呢?:根据求导的链式法则,w的局部梯度是X,当X全为正时,由反向传播传下来的梯度乘以X后不会改变方向,要么为正数要么为负数,也就是说w权重的更新在一次更新迭代计算中要么同时减小,要么同时增大。 45 | 46 | ![img](https://i.loli.net/2020/05/10/NFAOnSPDhZqyfIY.png) 47 | 48 | 其中,w的更新方向向量只能在第一和第三象限。假设最佳的w向量如蓝色线所示,由于输入全为正,现在迭代更新只能沿着红色路径做zig-zag运动,更新的效率很慢。 49 | 50 | 基于此,当输入数据减去均值后,就会有负有正,会消除这种影响。 51 | 52 | ### 参考资料: 53 | 54 | [1.为什么要对数据进行归一化处理? ](https://zhuanlan.zhihu.com/p/27627299) 55 | [2. 深度学习中图像为什么要归一化?](https://www.zhihu.com/question/293640354) 56 | [3. 深度学习中,为什么需要对数据进行归一化](https://blog.csdn.net/qq_32172681/article/details/100876348) 57 | [4. 深度学习的输入数据集为什么要做均值化处理](https://blog.csdn.net/hai008007/article/details/79718251) 58 | 59 | ​ By Yee 60 | ​ 2020.05.10 -------------------------------------------------------------------------------- /计算机视觉/11_梯度消失和爆炸以及解决方法.md: -------------------------------------------------------------------------------- 1 | ## 问题 2 | 3 | 梯度消失无论是笔试还是面试都是常客了,其实对应于梯度消失,还有一个梯度爆炸的概念,这又是什么导致的呢?下面我们将根据公式推导来解释何为梯度消失与梯度爆炸。 4 | 5 | ## 梯度消失和梯度爆炸的表现 6 | 7 | 网络层数越多,模型训练的时候便越容易出现 梯度消失(gradient vanish) 和 梯度爆炸(gradient explod) 这种梯度不稳定的问题。假设现在有一个含有3层隐含层的神经网络: 8 | 9 | ![这里写图片描述](https://i.loli.net/2020/05/21/SArqy1bp56euwIV.png) 10 | 11 | **梯度消失发生时的表现是:**靠近输出层的 hidden layer 3 的权值更新正常,但是靠近输入层的 hidden layer 1 的权值更新非常慢,导致其权值几乎不变,仍接近于初始化的权值。这就导致 hidden layer 1 相当于只是一个映射层,对所有的输入做了一个函数映射,这时的深度学习网络的学习等价于只有后几层的隐含层网络在学习。 12 | 13 | **梯度爆炸发生时的表现是:**当初始的权值太大,靠近输入层的 hidden layer 1 的权值变化比靠近输出层的 hidden layer 3 的权值变化更快。 14 | 15 | 所以梯度消失和梯度爆炸都是出现在靠近输入层的参数中。 16 | 17 | ## 产生梯度消失与梯度爆炸的根本原因 18 | 19 | ### 梯度消失分析 20 | 21 | 下图是我画的一个非常简单的神经网络,每层都只有一个神经元,且神经元所用的激活函数 $\sigma$ 为 sigmoid 函数,$Loss$ 表示损失函数,前一层的输出与后一层的输入关系如下: 22 | $$ 23 | y_i = \sigma(z_i) = \sigma(w_i*x_i+b_i), \quad其中x_i = y_{i-1} 24 | $$ 25 | ![](https://i.loli.net/2020/05/21/aplXMCsvPw4rh2Q.jpg) 26 | 27 | 因此,根据反向传播的链式法则,损失函数相对于参数 $b_1$ 的梯度计算公式如下: 28 | $$ 29 | \frac{\partial Loss}{\partial b_1} = \frac{\partial Loss}{\partial y_4}*\frac{\partial y_4}{\partial z_4}*\frac{\partial z_4}{\partial x_4}*\frac{\partial x_4}{\partial z_3}*\frac{\partial z_3}{\partial x_3}*\frac{\partial x_3}{\partial z_2}*\frac{\partial z_2}{\partial x_2}*\frac{\partial x_2}{\partial z_1}*\frac{\partial z_1}{\partial b_1} \\ 30 | = \frac{\partial Loss}{\partial y_4}*\partial{'}(z_4)*w_4*\partial{'}(z_3)*w_3*\partial{'}(z_2)*w_2*\partial{'}(z_1) 31 | $$ 32 | 而 sigmoid 函数的导数 $\sigma{'}(x)$ 如下图所示: 33 | 34 | ![这里写图片描述](https://i.loli.net/2020/05/21/eca5HdVqL9EBmuU.png) 35 | 36 | 即 $\sigma{'}(x)\le \frac{1}{4}$ ,而我们一般会使用标准方法来初始化网络权重,即使用一个均值为 0 标准差为 1 的高斯分布,因此初始化的网络参数 $w_i$ 通常都小于 1 ,从而有 $|\sigma{'}(z_i)*w_i|\le \frac{1}{4}$ 。根据公式(2)的计算规律,层数越多,越是前面的层的参数的求导结果越小,于是便导致了梯度消失情况的出现。 37 | 38 | ### 梯度爆炸分析 39 | 40 | 在分析梯度消失时,我们明白了导致其发生的主要原因是 $|\sigma{'}(z_i)*w_i|\le \frac{1}{4}$ ,经链式法则反向传播后,越靠近输入层的参数的梯度越小。而导致梯度爆炸的原因是:$|\sigma{'}(z_i)*w_i|>1$,当该表达式大于 1 时,经链式法则的指数倍传播后,前面层的参数的梯度会非常大,从而出现梯度爆炸。 41 | 42 | 但是要使得$|\sigma{'}(z_i)*w_i|>1$,就得 $|w_i| > 4$才行,按照 $|\sigma{'}(w_i*x_i+b_i)*w_i|>1$,可以计算出 $x_i$ 的数值变化范围很窄,仅在公式(3)的范围内,才会出现梯度爆炸,**因此梯度爆炸问题在使用 sigmoid 激活函数时出现的情况较少,不容易发生。** 43 | 44 | ![这里写图片描述](https://i.loli.net/2020/05/21/dIgKDuw976leTF4.png) 45 | 46 | ## 怎么解决 47 | 48 | 如上分析,**造成梯度消失和梯度爆炸问题是网络太深,网络权值更新不稳定造成的,本质上是因为梯度反向传播中的连乘效应**。另外一个原因是当激活函数使用 sigmoid 时,梯度消失问题更容易发生,因此可以考虑的解决方法如下: 49 | 50 | 1. 压缩模型层数 51 | 2. 改用其他的激活函数如 ReLU 52 | 3. 使用 BN 层 53 | 4. 使用 ResNet 的短路连接结构 54 | 55 | ## 参考资料 56 | 57 | [激活函数及其作用以及梯度消失、爆炸、神经元节点死亡的解释](https://blog.csdn.net/qq_17130909/article/details/80582226) -------------------------------------------------------------------------------- /计算机视觉/14_CNN在图像上表现好的原因.md: -------------------------------------------------------------------------------- 1 | ## 问题 2 | 3 | 一提到计算机视觉,第一反应就是 CNN ,但是大家有没有想过,为什么图像识别领域的网络结构都是使用的 CNN 呢,或者说 CNN 网络有哪些特点可以使其在图像识别领域表现良好?这个问题对于我们而言都习惯到理所当然了,所以面试官要是突然问这种问题,估计很多同学都得懵逼一会。 4 | 5 | ## 分析 6 | 7 | 网上没有找着相关的问题答案,所以下面的分析仅仅是我个人的观点,大家交流交流,有其他见解的话非常欢迎提出来! 8 | 9 | 为什么图像识别领域要使用 CNN ,其实潜在意思是在问 CNN 中的卷积层与全连接层相比好在哪里?为什么这么说,因为在卷积神经网络之前,一般的网络都采用的是全连接的方式,前一层的每一个单元都对下一层的每一个单元有影响,而 CNN 中虽然存在全连接层,但是更核心的是前面用于提取特征的卷积层,**所以这个问题便转换成了卷积层和全连接层的比较问题。** 10 | 11 | ## 卷积层和全连接层的区别 12 | 13 | 卷积层相比于全连接层,主要有两个特点: 14 | 15 | 1. **局部连接:**全连接层是一种稠密连接方式,而卷积层却只使用卷积核对局部进行处理,这种处理方式其实也刚好对应了图像的特点。在视觉识别中,关键性的图像特征、边缘、角点等只占据了整张图像的一小部分,相隔很远的像素之间存在联系和影响的可能性是很低的,而局部像素具有很强的相关性。 16 | 2. **共享参数:**如果借鉴全连接层的话,对于1000×1000大小的彩色图像,一层全连接层便对应于三百万数量级维的特征,即会导致庞大的参数量,不仅计算繁重,还会导致过拟合。而卷积层中,卷积核会与局部图像相互作用,是一种稀疏连接,大大减少了网络的参数量。另外从直观上理解,依靠卷积核的滑动去提取图像中不同位置的相同模式也刚好符合图像的特点,不同的卷积核提取不同的特征,组合起来后便可以提取到高级特征用于最后的识别检测了。 17 | 18 | 所以 CNN 应用的条件一般是要求卷积对象有局部相关性,这正是图像所具备的,因此图像领域都是使用 CNN 就解释地通了。 19 | 20 | ## 参考资料 21 | 22 | [CNN为什么能使用在NLP?](https://www.zhihu.com/question/38544669) 23 | [(二)计算机视觉四大基本任务(分类、定位、检测、分割)](https://zhuanlan.zhihu.com/p/31727402) -------------------------------------------------------------------------------- /计算机视觉/22_CNN网络各种层的FLOPs和参数量paras计算.md: -------------------------------------------------------------------------------- 1 | ## FLOPs 2 | 3 | > **这里先注意一下FLOPs的写法,不要弄混了:** 4 | > **FLOPS(全大写)**:是floating point operations per second的缩写,意指每秒浮点运算次数,理解为计算速度,是一个衡量硬件性能的指标。 5 | > **FLOPs(s小写)**:,是floating point operations的缩写(s表复数),意指浮点运算数,理解为计算量,可以用来衡量算法/模型的复杂度,也就是我们这里要讨论的。 6 | 7 | ### 标准卷积层的FLOPs 8 | 9 | 考虑bias:$(2*C_{int}*k^2)*C_{out}*H*W$ 10 | 不考虑bias:$(2*C_{int}*k^2-1)*C_{out}*H*W$ 11 | 12 | 参数定义(下同):$C_{int}$为输入通道数,k为卷积核边长, $C_{out}$为输出通道数,H*W为输出特征图的长宽。 13 | 14 | 其实卷积层在实现的时候可以选择加bias或者不加,在很多的框架当中是一个可以选择的参数,为了严谨,这里特地提一下。 15 | 16 | 怎么理解上面的公式呢?以不考虑bias为例。我们先计算输出的feature map中的一个pixel的计算量,然后再乘以feature map的规模大小即可,所以我们主要分析下上面公式中的括号部分: 17 | $$ 18 | (2*C_{int}*k^2-1) = C_{int}*k^2 + C_{int}*k^2-1 19 | $$ 20 | 21 | 22 | 可以看到我们把它分成了两部分,**第一项是乘法运算数,第二项是加法运算数,因为n个数相加,要加n-1次,所以不考虑bias,会有一个-1,如果考虑bias,刚好中和掉。** 23 | 24 | ### 深度可分离卷积的FLOPs 25 | 26 | 深度可分离卷积分成两部分,一部分是分通道卷积,另一部分是1*1卷积。(如果不知道深度可分离卷积的朋友可以先看下[这个博客](https://baijiahao.baidu.com/s?id=1634399239921135758&wfr=spider&for=pc),这是一种可大大减少计算量的卷积方法) 27 | 28 | 这里的讨论以考虑bias为准: 29 | 第一部分:$(2*k^2 )*H*W*C_{int}$ 30 | 第二部分:$2*C_{int}*H*W*C_{out}$ 31 | 32 | 最终的结果就是两部分相加。 33 | 34 | ### 池化层的FLOPS 35 | 这里又分为全局池化和一般池化两种情况: 36 | #### 全局池化 37 | 针对输入所有值进行一次池化操作,不论是max、sum还是avg,都可以简单地看做是只需要对每个值算一次。 38 | 39 | 所以结果为:$H_{int}*W_{int}*C_{int}$ 40 | #### 一般池化 41 | 答案是:$k^2*H_{out}*W_{out}*C_{out}$ 42 | 43 | 注意池化层的:$C_{out} = C_{int}$ 44 | ### 全连接层的FLOPs 45 | 考虑bias:$(2*I)*O$ 46 | 不考虑bias:$(2*I-1)*O$ 47 | 48 | 分析同理,括号内是一个输出神经元的计算量,拓展到O个输出神经元。(如果该全连接层的输入是卷积层的输出,需要先将输出展开成一列向量) 49 | ### 激活层的FLOPs 50 | #### ReLU 51 | ReLU一般都是跟在卷积层的后面,这里假设卷积层的输出为$H*W*C$,因为ReLU函数的计算只涉及到一个判断,因此计算量就是$H*W*C$ 52 | 53 | #### sigmoid 54 | 根据sigmoid的公式可以知道,每个输入都需要经历4次运算,因此计算量是$H*W*C*4$(参数含义同ReLU) 55 | ___ 56 | ## 参数量 57 | ### 卷积层的参数量 58 | 卷积层的参数量与输入特征图大小无关 59 | 60 | 考虑bias:$(k^2*C_{int}+1)*C_{out}$ 61 | 不考虑bias:$(k^2*C_{int})*C_{out}$ 62 | 63 | ### 深度可分离卷积的参数量 64 | 不考虑bias: 65 | 第一部分:$k^2*C_{int}$ 66 | 第二部分:$(1*1*C_{int})*C_{out}$ 67 | 最终结果为两者相加。 68 | 69 | ### 池化层的参数量 70 | 池化层没有需要学习的参数,所以参数量为0。 71 | ### 全连接层的参数量 72 | 考虑bias:$I*O+1$ 73 | ___ 74 | 如果有写的不对的地方请各位大佬在评论区指出来,一起学习! 75 | ___ 76 | ## 参考资料 77 | [CNN 模型所需的计算力(flops)和参数(parameters)数量是怎么计算的?](https://www.zhihu.com/question/65305385/answer/451060549) -------------------------------------------------------------------------------- /计算机视觉/23_弄明白感受野大小的计算问题.md: -------------------------------------------------------------------------------- 1 | ## 感受野Receptive field (RF)的概念 2 | 3 | 卷及神经网络中每一层输出的特征图(feature map)中的每一个像素映射到原始输入图像的区域大小。 4 | 5 | ## 卷积输入输出的大小关系 6 | 根据感受野的概念,大家可以体会到感受野的计算应该与卷积的计算是相反的过程,所以先回顾下卷积输入输出的大小关系公式:(以高度为例) 7 | $$ 8 | Height_{out} = (Height_{in} - F+2*P)/S + 1 9 | $$ 10 | 其中`F`为滤波器的边长,`P`为padding的大小,`S`为步长。 11 | 12 | ## 感受野的计算 13 | 14 | 上面说了感受野的计算是与卷积计算相反的过程,卷积是从低层慢慢一层层卷积到高层的,所以感受野的计算采用的是`Top-Down`方式,即从最高层向低层迭代计算。具体步骤为: 15 | 1. 设要计算感受野的这层为第N层 16 | 2. 第`N`层到第`N-1`层的感受野就是对第`N-1`层进行卷积时使用的滤波器大小,这里我们设为$RF_{N-1}$。 17 | 3. 接着计算第`N`层到第`N-2`层的感受野大小,公式是: 18 | $$RF_{N-2} = (RF_{N-1} -1)*stride + kernel\_size$$ **(需要注意的是这里的`stride`和`kernel_size`是第`N-2`层的)** 19 | 5. 一直迭代第3步直至输入层,即可算出第N层的感受野 20 | 21 | 这里大家注意下第3步中的公式,体会下是不是刚好与上面卷积输入输出的关系刚好反过来,$RF_{N-2}$对应$Height_{in}$,$RF_{N-1}$对应$Height_{out}$。 22 | 23 | 唯一的区别是不需要管padding,这也说明了感受野其实是包括padding在内的,所以你会发现算出来的感受野大小可能会比原始图像的大小还要大。 24 | 25 | 如上一段所说,**其实这样的计算方法是有一点问题的,没有考虑padding和pooling部分,要做修改。但这种方法还是可以应对面试时候的感受野计算问题的**,若是研究过程中要计算非常准确的感受野大小的话,还是得再深入研究下,大家可以看看下面的两个参考资料。 26 | 27 | ## 参考资料 28 | [如何计算感受野(Receptive Field)——原理](https://zhuanlan.zhihu.com/p/31004121) 29 | [Calculate Receptive Field for VGG16](http://zike.io/posts/calculate-receptive-field-for-vgg-16/) -------------------------------------------------------------------------------- /计算机视觉/24_卷积网络中的卷积与互相关的那点事.md: -------------------------------------------------------------------------------- 1 | ## 卷积层的来源与作用 2 | 3 | 深度学习的计算机视觉是基于卷积神经网络实现的,卷积神经网络的与传统的神经网络(可以理解为多层感知机)的主要区别是卷积神经网络中除了全连接层外还有卷积层和pooling层等。 4 | 5 | 卷积层算是图像处理中非常基础的东西,它其实也是全连接层演变来的,卷积可视为**局部连接**和**共享参数**的全连接层。 6 | 7 | **局部连接**:在全连接层中,每个输出通过权值(weight)和所有输入相连。而在视觉识别中,关键性的图像特征、边缘、角点等只占据了整张图像的一小部分,图像中相距很远的两个像素之间有相互影响的可能性很小。因此,在卷积层中,每个输出神经元在通道方向保持全连接,而在空间方向上只和一小部分输入神经元相连。 8 | 9 | **共享参数**: 使用同一组权值去遍历整张图像,用于发现整张图像中的同一种特征例如角点、边缘等。不同的卷积核用于发现不同的特征。共享参数是深度学习一个重要的思想,其在减少网络参数的同时仍然能保持很高的网络容量(capacity)。卷积层在空间方向共享参数,而循环神经网络(recurrent neural networks)在时间方向共享参数。 10 | 11 | **卷积层的作用**:通过卷积,我们可以捕获图像的局部信息。通过多层卷积层堆叠,各层提取到特征逐渐由边缘、纹理、方向等低层级特征过度到文字、车轮、人脸等高层级特征。 12 | 13 | ## 卷积与互相关 14 | 但是其实深度学习各框架的`conv2`卷积层的API对卷积运算的实现其实使用的是**互相关运算**,即下图: 15 | 16 | ![在这里插入图片描述](https://i.loli.net/2020/05/18/JT9WzRIvteZjuGC.png) 17 | 18 | 上述的运算过程可以写成公式: 19 | 20 | ![在这里插入图片描述](https://i.loli.net/2020/05/18/BkPZM1WwXHDpbQu.png) 21 | 22 | `h[u, v]`表示filter的权重;`k`表示neighbor的个数,如当`k=1`时表示的是`3*3`的滤波器 23 | 24 | 从公式中可以看出这个运算是从上往下,从左到右的对点相乘再相加。所以这个公式代表的运算是互相关。 25 | ___ 26 | 下面我们回顾下卷积的定义,设`f(x)`和`g(x)`是在R上的可积函数,作积分: 27 | 28 | ![在这里插入图片描述](https://i.loli.net/2020/05/18/16hczmsOuo5fjTK.png) 29 | 30 | 改成离散函数形式如下: 31 | 32 | ![在这里插入图片描述](https://i.loli.net/2020/05/18/QHBcoxKuPyDJ2wj.png) 33 | 34 | 这个公式与互相关的公式很相似,但是注意符号改变了,这就导致运算的方向变成了从下往上,从右到左,与互相关的运算顺序刚好相反。 35 | 36 | 所以真正的卷积运算应该是先让卷积核绕自己的核心元素顺时针旋转180度(或者理解为左右翻转再上下翻转),再与图像中的像素做对点相乘再相加运算。 37 | 38 | 然而图像处理中的大部分卷积核都是中心对称的,所以这时候的互相关运算与卷积运算结果是一样的,这也许是最开始称为卷积的原因吧。 39 | 40 | 另外`CNN`中的卷积核权值参数是学出来的,所以其实卷积和互相关没啥区别 41 | 42 | ## 将卷积运算转为矩阵相乘 43 | 这里先扯一扯之前没怎么听过但突然看到的**量化**概念。 44 | 45 | 传统优化矩阵乘的思想有基于算法分析的,也有基于软件优化的方法如改进访存局部性、利用向量指令等,这两个方法都是基于对计算机运行特性进行的改进。 46 | 47 | 而随着深度学习技术的演进,神经网络技术出现了一个重要的方向——**神经网络量化**,。量化技术的出现使得我们可以在深度学习领域使用一些特别的方法来优化矩阵乘,例如facebook开源的专门用于量化神经网络的计算加速库`QNNPACK`。 48 | 49 | 神经网络计算一般都是以单精度浮点(Floating-point 32, `FP32`)为基础。而网络算法的发展使得神经网络对计算和内存的要求越来越大,以至于移动设备根本无力承受。为了提升计算速度,**量化**(Quantization)被引入到神经网络中,主流的方法是将神经网络算法中的权重参数和计算都从`FP32`转换到`INT8`。这里有个相关的论文:[CVPR2020 | 8 比特数值也能训练模型?商汤提出训练加速新算法 ](http://bbs.cvmart.net/topics/1980)(反正我是看不明白,就是提提相关概念) 50 | ___ 51 | 52 | 下面讲解传统的卷积运算怎么转化为矩阵乘: 53 | 54 | 传统的卷积核依次滑动的计算方法很难加速。转化为矩阵乘法之后,就可以调用各种线性代数运算库,`CUDA`里面的矩阵乘法实现。这些矩阵乘法都是极限优化过的,比暴力计算快很多倍。下面的两个图充分说明了转化为矩阵乘法后的具体过程: 55 | ![在这里插入图片描述](https://i.loli.net/2020/05/18/dNVHwc35fZxDM68.jpg) 56 | 57 | ![在这里插入图片描述](https://i.loli.net/2020/05/18/yLEnoH2rAtQSNcX.jpg) 58 | 59 | 细细体会! 60 | 61 | ## 参考资料 62 | 63 | [(二)计算机视觉四大基本任务(分类、定位、检测、分割)](https://zhuanlan.zhihu.com/p/31727402) 64 | [卷积与互相关计算](https://blog.csdn.net/qq_19094871/article/details/103117936) 65 | [通用矩阵乘(GEMM)优化算法](https://jackwish.net/2019/gemm-optimization.html) 66 | [【算法】卷积(convolution)/滤波(filter)和互相关(cross-correlation)以及实现](https://zhuanlan.zhihu.com/p/30086163) 67 | [如何通俗易懂地解释卷积?](https://www.zhihu.com/question/22298352) -------------------------------------------------------------------------------- /计算机视觉/32_为什么用F1-score.md: -------------------------------------------------------------------------------- 1 | ## 问题 2 | 3 | 为什么使用F1 score?(这里主要讨论为何使用 F1 score 而不是算术平均) 4 | 5 | 6 | 7 | ## F1 score 8 | 9 | F1 score是分类问题中常用的评价指标,定义为精确率(Precision)和召回率(Recall)的调和平均数。 10 | $$ 11 | F1=\frac{1}{\frac{1}{Precision}+\frac{1}{Recall}}=\frac{2×Precision×Recall}{Precision+Recall} 12 | $$ 13 | 14 | > 补充一下精确率和召回率的公式: 15 | > 16 | > > TP( True Positive):真正例 17 | > > 18 | > > FP( False Positive):假正例 19 | > > 20 | > > FN(False Negative):假反例 21 | > > 22 | > > TN(True Negative):真反例 23 | > 24 | > **精确率(Precision):** $Precision=\frac{TP}{TP+FP}$ 25 | > 26 | > **召回率(Recall):** $Recall=\frac{TP}{TP+FN}$ 27 | > 28 | > > 精确率,也称为查准率,衡量的是**预测结果为正例的样本中被正确分类的正例样本的比例**。 29 | > > 30 | > > 召回率,也称为查全率,衡量的是**真实情况下的所有正样本中被正确分类的正样本的比例。** 31 | > 32 | > 33 | 34 | 35 | 36 | F1 score 综合考虑了精确率和召回率,其结果更偏向于 Precision 和 Recall 中较小的那个,即 Precision 和 Recall 中较小的那个对 F1 score 的结果取决定性作用。例如若 $Precision=1,Recall \approx 0$,由F1 score的计算公式可以看出,此时其结果主要受 Recall 影响。 37 | 38 | 如果对 Precision 和 Recall 取算术平均值($\frac{Precision+Recall}{2}$),对于 $Precision=1,Recall \approx 0$,其结果约为 0.5,而 F1 score 调和平均的结果约为 0。**这也是为什么很多应用场景中会选择使用 F1 score 调和平均值而不是算术平均值的原因,因为我们希望这个结果可以更好地反映模型的性能好坏,而不是直接平均模糊化了 Precision 和 Recall 各自对模型的影响。** 39 | 40 | 41 | 42 | > 补充另外两种评价方法: 43 | 44 | **加权调和平均:** 45 | 46 | 上面的 F1 score 中, Precision 和 Recall 是同等重要的,而有的时候可能希望我们的模型更关注其中的某一个指标,这时可以使用加权调和平均: 47 | $$ 48 | F_{\beta}=(1+\beta^{2})\frac{1}{\frac{1}{Precision}+\beta^{2}×\frac{1}{Recall}}=(1+\beta^{2})\frac{Precision×Recall}{\beta^{2}×Precision+Recall} 49 | $$ 50 | 当 $\beta > 1$ 时召回率有更大影响, $\beta < 1$ 时精确率有更大影响, $\beta = 1$ 时退化为 F1 score。 51 | 52 | 53 | 54 | **几何平均数:** 55 | $$ 56 | G=\sqrt{Precision×Recall} 57 | $$ 58 | 59 | 60 | ## 参考资料 61 | 62 | [为什么要用f1-score而不是平均值](https://www.cnblogs.com/walter-xh/p/11140715.html) https://www.cnblogs.com/walter-xh/p/11140715.html 63 | 64 | -------------------------------------------------------------------------------- /计算机视觉/38_了解全卷积网络FCN.md: -------------------------------------------------------------------------------- 1 | ## 问题 2 | 3 | FCN 是语义分割的开山之作,没理由不了解。最近发现学习最快的方式还是看别人的教学视频,很多时候博客看一大推,没几个能讲地明白的,确实啊,视频都得用四五十分钟的东西,一个博客得写得多详细才能覆盖到方方面面呀,以后还是先借助视频来学习,然后再根据源码进一步深入理解吧。 4 | 5 | ## 视频资源 6 | 7 | 这是 B 站找到的有关全卷积网络 FCN 的讲解视频,先看看这个详细了解下。 8 | 9 | [【深度之眼paper专题】语义分割的“开山之作”——FCN!](https://www.bilibili.com/video/BV16K411W782?from=search&seid=8393040243981426012) 10 | 11 | ## 网络结构 12 | 13 | ![](https://i.loli.net/2020/06/26/xrJyNBYLMZF74Sc.png) 14 | 15 | 下采样后到恢复成原始输入大小的输出之间靠的是反卷积,反卷积的概念我们在其他文章也提到了,请移步到 “48_各种卷积方式串讲.md”。 16 | 17 | 总得来说,网络结构是:**下采样**(卷积+池化,使用的是VGG)——**反卷积**(反卷积之前还有一个**跳跃链接**以利用不同层的信息的操作) -------------------------------------------------------------------------------- /计算机视觉/40_解释 ROI Pooling 和 ROI Align 的区别.md: -------------------------------------------------------------------------------- 1 | ## 问题 2 | 3 | ROI Pooling 和 ROI Align 的区别是什么 4 | 5 | ## ROI Pooling 和 ROI Align 是什么 6 | 7 | 如果你对目标检测网络 Faster R-CNN 和实例分割网络 Mask R-CNN 网络比较熟悉的话,那你应该也对这个话题非常熟悉。 8 | 9 | 在区域建议网络 RPN 得到候选框 ROI 之后,需要提取该 ROI 中的固定数目的特征(例如Faster R-CNN中的 7*7 )输入到后面的分类网络以及边界回归网络的全连接层中。Faster R-CNN中使用的方法是 ROI Pooling,而对于像素位置精细度要求更高的 Mask R-CNN 对 ROI Pooling 进行改进,变成 ROI Align。 10 | 11 | **ROI Pooling和ROIAlign最大的区别是:前者使用了两次量化操作,而后者并没有采用量化操作,使用了双线性插值算法,具体的解释如下所示。** 12 | 13 | ## ROI Pooling 技术细节 14 | 15 | img 16 | 17 | 如上图所示,为了得到固定大小(7X7)的feature map,我们需要做两次量化操作: 18 | 19 | 1. 图像坐标 — feature map坐标 20 | 2. feature map坐标 — ROI feature坐标。 21 | 22 | 我们来说一下具体的细节,如图我们输入的是一张800x800的图像,在图像中有两个目标(猫和狗),狗的BB大小为665x665,经过VGG16网络后,我们可以获得对应的feature map,如果我们对卷积层进行Padding操作,我们的图片经过卷积层后保持原来的大小,但是由于池化层的存在,我们最终获得feature map 会比原图缩小一定的比例,这和Pooling层的个数和大小有关。在该VGG16中,我们使用了5个池化操作,每个池化操作都是2Pooling,因此我们最终获得feature map的大小为800/32 x 800/32 = 25x25(是整数),但是将狗的BB对应到feature map上面,我们得到的结果是665/32 x 665/32 = 20.78 x 20.78,结果是浮点数,含有小数,但是我们的像素值可没有小数,那么作者就对其进行了量化操作(即取整操作),即其结果变为20 x 20,在这里引入了第一次的量化误差;然而我们的feature map中有不同大小的ROI,但是我们后面的网络却要求我们有固定的输入,因此,我们需要将不同大小的ROI转化为固定的ROI feature,在这里使用的是7x7的ROI feature,那么我们需要将20 x 20的ROI映射成7 x 7的ROI feature,其结果是 20 /7 x 20/7 = 2.86 x 2.86,同样是浮点数,含有小数点,我们采取同样的操作对其进行取整吧,在这里引入了第二次量化误差。其实,这里引入的误差会导致图像中的像素和特征中的像素的偏差,即将feature空间的ROI对应到原图上面会出现很大的偏差。原因如下:比如用我们第二次引入的误差来分析,本来是2,86,我们将其量化为2,这期间引入了0.86的误差,看起来是一个很小的误差呀,但是你要记得这是在feature空间,我们的feature空间和图像空间是有比例关系的,在这里是1:32,那么对应到原图上面的差距就是0.86 x 32 = 27.52。这个差距不小吧,这还是仅仅考虑了第二次的量化误差。这会大大影响整个检测算法的性能,因此是一个严重的问题。 23 | 24 | ## ROI Align 技术细节 25 | 26 | ![img](https://i.loli.net/2020/06/21/r3tGoY8P7LeCsUl.png) 27 | 28 | 如上图所示,为了得到为了得到固定大小(7X7)的feature map,ROIAlign技术并没有使用量化操作,即我们不想引入量化误差,比如665 / 32 = 20.78,我们就用20.78,不用什么20来替代它,比如20.78 / 7 = 2.97,我们就用2.97,而不用2来代替它。这就是ROIAlign的初衷。那么我们如何处理这些浮点数呢,我们的解决思路是使用“双线性插值”算法。双线性插值是一种比较好的图像缩放算法,它充分的利用了原图中虚拟点(比如20.56这个浮点数,像素位置都是整数值,没有浮点值)四周的四个真实存在的像素值来共同决定目标图中的一个像素值,即可以将20.56这个虚拟的位置点对应的像素值估计出来。如下图所示,蓝色的虚线框表示卷积后获得的feature map,黑色实线框表示ROI feature,最后需要输出的大小是2x2,那么我们就利用双线性插值来估计这些蓝点(虚拟坐标点,又称双线性插值的网格点)处所对应的像素值,最后得到相应的输出。这些蓝点是2x2Cell中的随机采样的普通点,作者指出,这些采样点的个数和位置不会对性能产生很大的影响,你也可以用其它的方法获得。然后在每一个橘红色的区域里面进行max pooling或者average pooling操作,获得最终2x2的输出结果。我们的整个过程中没有用到量化操作,没有引入误差,即原图中的像素和feature map中的像素是完全对齐的,没有偏差,这不仅会提高检测的精度,同时也会有利于实例分割。这么细心,做科研就应该关注细节,细节决定成败。 29 | 30 | ![img](https://i.loli.net/2020/07/25/ENsRKcdnHLhgpzo.jpg) 31 | 32 | ## 参考资料 33 | 34 | [Mask R-CNN详解](https://blog.csdn.net/WZZ18191171661/article/details/79453780) -------------------------------------------------------------------------------- /计算机视觉/41_CascadeRCNN本质解决了什么问题.md: -------------------------------------------------------------------------------- 1 | ## 问题 2 | 3 | 这个问题拖到现在才开始整理属实不应该,实习面华为的时候被问过,我对 Cascade RCNN 的理解貌似面试官不太认同,于是便把这个问题放入了列表中,然后前天面科大讯飞的时候又被问到 Cascade RCNN 的 motivation 是什么,我这时候的回答不知道面试官满不满意,但是更加强烈地觉得这个网络可能还有更多的精髓,因此这篇详细来整理一下。 4 | 5 | ### 先介绍一下 Faster RCNN中相关的一些点: 6 | 7 | 在 Faster RCNN 中,训练阶段,经过 RPN 之后,会提出 2000 左右个 proposals ,这些 proposals 会被送入到 Fast R-CNN 结构中,在 Fast R-CNN 的结构中,首先会计算每个 proposals 和 gt 之间的 IOU ,然后通过人为设定的 IOU 阈值(通常为 0.5),把这些 proposals 分成正样本和负样本(之后正样本才会参与到后面的 bbox 回归学习,从损失函数的表达中也可以看出来,只有正样本才被算入损失函数中),并对这些样本进行采样,使得他们之间的比例尽量满足(1:3,二者总数为 128 ),之后这 128 个proposals 被送入到 Roi Pooling 层,最后进行类别分类和 box 回归。 8 | 9 | 在 inference 阶段,RPN 网络提出了 300 左右的 proposals,这些 proposals 被送入到 Fast RCNN 结构中,与 training 阶段不同的是,inference 阶段没有办法对这些 proposals 重采样,因为 inference 阶段不知道 gt,所以只能把他们都作为正样本,直接进入 Roi Pooling ,之后进行分类和box 回归。 10 | 11 | ## Cascade 中所做的实验以及所揭示的问题 12 | 13 | 要提高Fast R-CNN目标检测的精度,一个非常直觉的做法便是提高判定是正负样本的 IOU 阈值,这样后面的 detector 便接收的是更高质量的 proposals ,自然能产生高精度的 box 。但是这样便会产生两个问题: 14 | 15 | 1. **过拟合问题**。提高了 IOU 阈值,满足这个阈值条件的 proposals 必然比之前少了,容易导致过拟合。 16 | 2. **更加严重的 mismatch 问题**。(这样的话训练阶段模型看到的都是质量比较高的的 proposals,会导致过拟合,而 inference 阶段由于没有经过阈值的采样,因此会存在很多的质量较差的 proposals,而detector 之前都没见过这么差的 proposal ,精度便下降了。) 17 | 18 | > ### mismatch 19 | > 20 | > 从上面我们明显可以看出,training 阶段和 inference 阶段,bbox回归器的输入分布是不一样的,training阶段的输入 proposals 质量更高(因为被采样过,IOU > threshold ),而 inference 阶段的则相对差点,这就是论文中提到的 **mismatch** 问题,这个问题是固有存在的,通常 threshold 取 0.5 时,mismatch 问题还不会很严重。 21 | 22 | 因此作者做了实验来验证这样的说法: 23 | 24 | #### 实验一:统计 RPN 输出的 proposals 在各个 IOU 范围内的数量。 25 | 26 | ![img](https://i.loli.net/2020/08/14/v6SjEZsV7mhWwfd.jpg) 27 | 28 | 可以看出,IOU 在 0.6,0.7 以上的proposals 数量很少,直接提高阈值的话,确实有可能出现上述两个问题。 29 | 30 | #### 实验二:将IOU 阈值设为 0.5,0.6,0.7时,proposals的分布以及检测精度 31 | 32 | ![img](https://i.loli.net/2020/08/14/fb49hSgoLnIVZdU.jpg) 33 | 34 | (c) 图中横轴表示RPN的输出 proposal 的IoU,纵轴表示proposal经过 box reg 的新的IoU。这个图可以这么理解:当横坐标为 0.55 也就是RPN的输出 proposal 的IoU在 0.5 左右时,将 IOU 的阈值设为 0.5 的表现比其他两个更高阈值的更好。横坐标为 0.65或者 0.75时的情况同样。 35 | 36 | 因此可以得出以下结论: 37 | 38 | 1. 只有proposal自身的阈值和训练器训练用的阈值较为接近的时候,训练器的性能才最好。 39 | 2. 单一阈值训练出的检测器效果非常有限,单一阈值不能对所有的Proposals都有很好的优化作用。 40 | 41 | (d) 图中横轴表示inference阶段,判定box为tp的IoU阈值,纵轴为mAP。可以看到IoU阈值从0.5提到0.7时,AP下降很多。进一步说明了 mismatch 的问题。 42 | 43 | ## Cascade 的结构设计 44 | 45 | 做了上面两个实验之后,那么改进的思路便很明显了。既然单一一个阈值训练出的检测器效果有限,作者就提出了muti-stage的结构,每个stage都有一个不同的IoU阈值,而且阈值是逐步提高。 46 | 47 | ![preview](https://i.loli.net/2020/08/14/jaHcAIuvi3dfoMy.jpg) 48 | 49 | (b) 中的 detector 的是共享的,而且三个分支的IOU阈值都取 0.5 。而经过上面的分析,我们已经知道单一阈值0.5,是无法对所有proposal取得良好效果的。此外,detector会改变样本的分布,这时候再使用同一个共享的H对检测肯定是有影响的。 50 | 51 | (c) 第一个stage的输入IoU的分布很不均匀,高阈值proposals数量很少,导致负责高阈值的detector很容易过拟合。此外在inference时,3个detector的结果要进行ensemble,但是它们的输入的IoU大部分都比较低,这时高阈值的detector也需要处理低IoU的proposals,它就存在较严重的mismatch问题,它的detector效果就很差了。 52 | 53 | ## 总结 54 | 55 | RPN提出的proposals大部分质量不高,导致没办法直接使用高阈值的detector,Cascade R-CNN使用cascade回归作为一种重采样的机制,逐stage提高proposal的IoU值,从而使得前一个stage重新采样过的proposals能够适应下一个有更高阈值的stage。 56 | 57 | - 每一个stage的detector都不会过拟合,都有足够满足阈值条件的样本。 58 | - 更深层的detector也就可以优化更大阈值的proposals。 59 | - 每个stage的H不相同,意味着可以适应多级的分布。 60 | - 在inference时,虽然最开始RPN提出的proposals质量依然不高,但在每经过一个stage后质量都会提高,从而和有更高IoU阈值的detector之间不会有很严重的mismatch。 61 | 62 | ## 补充 63 | 64 | 第一个 stage 的输入是 RPN 提出的 2000个 rois,筛选了128个,然后后面 2 个 stage 都是继续沿用这 128 个。其实从图中也可以看出,但是不知道为啥很多人在这里有疑问。 65 | 66 | ## 参考资料 67 | 68 | [Cascade R-CNN 详细解读](https://zhuanlan.zhihu.com/p/42553957) -------------------------------------------------------------------------------- /计算机视觉/44_特征融合concat和add的区别.md: -------------------------------------------------------------------------------- 1 | ## 问题 2 | 3 | 在网络模型当中,经常要进行不同通道特征图的信息融合相加操作,以整合不同通道的信息,在具体实现方面特征的融合方式一共有两种,一种是 ResNet 和 FPN 等当中采用的 element-wise add ,另一种是 DenseNet 等中采用的 concat 。他们之间有什么区别呢? 4 | 5 | ## add 6 | 7 | 以下是 keras 中对 add 的实现源码: 8 | 9 | ```python 10 | def _merge_function(self, inputs): 11 | output = inputs[0] 12 | for i in range(1, len(inputs)): 13 | output += inputs[i] 14 | return output 15 | ``` 16 | 17 | 其中 inputs 为待融合的特征图,inputs[0]、inputs[1]……等的通道数一样,且特征图宽与高也一样。 18 | 19 | 从代码中可以很容易地看出,add 方式有以下特点: 20 | 21 | 1. 做的是对应通道对应位置的值的相加,通道数不变 22 | 2. 描述图像的特征个数不变,但是每个特征下的信息却增加了。 23 | 24 | ## concat 25 | 26 | 阅读下面代码实例帮助理解 concat 的工作原理: 27 | 28 | ```python 29 | t1 = [[1, 2, 3], [4, 5, 6]] 30 | t2 = [[7, 8, 9], [10, 11, 12]] 31 | tf.concat([t1, t2], 0) ==> [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]] 32 | tf.concat([t1, t2], 1) ==> [[1, 2, 3, 7, 8, 9], [4, 5, 6, 10, 11, 12]] 33 | 34 | # tensor t3 with shape [2, 3] 35 | # tensor t4 with shape [2, 3] 36 | tf.shape(tf.concat([t3, t4], 0)) ==> [4, 3] 37 | tf.shape(tf.concat([t3, t4], 1)) ==> [2, 6] 38 | --------------------- 39 | 作者:KUNLI7 40 | 来源:CSDN 41 | 原文:https://blog.csdn.net/u012193416/article/details/79479935 42 | 版权声明:本文为博主原创文章,转载请附上博文链接! 43 | ``` 44 | 45 | 在模型网路当中,数据通常为 4 个维度,即 num×channels×height×width ,因此默认值 1 表示的是 channels 通道进行拼接。如: 46 | 47 | ```python 48 | combine = torch.cat([d1, add1, add2, add3, add4], 1) 49 | ``` 50 | 51 | 从代码中可以很容易地看出,concat 方式有以下特点: 52 | 53 | 1. 做的是通道的合并,通道数变多了 54 | 2. 描述图像的特征个数变多,但是每个特征下的信息却不变。 55 | 56 | ## 多一点理解 57 | 58 | >add相当于加了一种prior,当两路输入可以具有“对应通道的特征图语义类似”的性质的时候,可以用add来替代concat,这样更节省参数和计算量(concat是add的2倍) 59 | 60 | ## 参考资料 61 | 62 | [理解concat和add的不同作用](https://blog.csdn.net/qq_32256033/article/details/89516738) 63 | [卷积神经网络中的add和concatnate区别](https://blog.csdn.net/weixin_39610043/article/details/87103358) 64 | 65 | -------------------------------------------------------------------------------- /计算机视觉/46_LSTM介绍理解.md: -------------------------------------------------------------------------------- 1 | ## 问题 2 | 3 | LSTM是深度学习语音领域必须掌握的一个概念,久仰大名,现在终于要来学习它了,真是世事无常,之前以为永远不会接触到呢,因此每次碰到这个就跳过了。 4 | 5 | ## 前言 6 | 7 | LSTM (Long short-term memory,长短期记忆) 是一种特殊的RNN,主要是为了解决长序列训练过程中梯度消失与梯度爆炸的问题,因此要学习LSTM,必须先了解RNN是一个什么东东。 8 | 9 | ### RNN 10 | 11 | RNN (Recurrent Neural Network,循环卷积网络) 是一种用于处理序列数据的神经网络,比如文本分析中,某个单词的意思会因为上文提到的内容不同而有不同的含义,RNN就能够很好地解决这种问题。 12 | 13 | ![img](https://i.loli.net/2020/06/04/4YjqgUAhcBvtenX.jpg) 14 | 15 | 我们以上图的第一个基本单元进行分析。 16 | 17 | $x^1$ 为当前状态下数据的输入,$h^0$ 表示接收到的上一个节点的输入。 18 | $y^1$ 为当前节点状态下的输出,而 $h^1$ 为传递到下一个节点的输出。 19 | $f$ 为一种映射函数,具体函数形式要看怎么设计。 20 | 21 | $h^1$ 和 $y^1$ 的计算方式为: 22 | $$ 23 | h^1 = \sigma(W^h h^0+W^ix^1) \\ 24 | y^1 = \sigma(W^oh^1) 25 | $$ 26 | 其中 $W^h$ 、$W^i$ 、$W^o$ 为函数 $f$ 的参数。 27 | 28 | 从图例和公式都可以看出,当前结点的输出不仅与当前结点相关,还与之前的所有结点相关。得到 $y^1, y^2,...,y^n$ 之后,使用 softmax 函数便可得到所需的信息进行分类。(这里的输入 $x^1,x^2,...,x^n$ 应该指的是每个字词单元的特征吧) 29 | 30 | ## LSTM 31 | 32 | 介绍完RNN,下面开始介绍正主。如上所说,LSTM是在RNN的基础上改进而来的,解决的是长序列数据训练过程中的梯度消失和梯度爆炸问题。 33 | 34 | LSTM 结构和普通 RNN 的主要输入输出区别如下图所示: 35 | 36 | ![img](https://i.loli.net/2020/06/04/zfJE7smedUuZ2tH.jpg) 37 | 38 | 即 LSTM 结构相比于普通的 RNN 多了一个传输状态 $c^t$ (cell state), $h^t$ 为 hidden state。参考资料中说RNN中的 $h^t$ 对应于 LSTM 中的 $c^t$,传递过程中改变较慢。 39 | 40 | > $c^t$ 也就是cell state中的内容,可以理解为主线,主要是用来保存节点传递下来的数据的,每次传递会对某些维度进行“忘记”并且会加入当前节点所包含的内容,总的来说还是用来保存节点的信息,改变相对较小。而$h^t$ 则主要是为了和当前输入组合来获得门控信号,对于不同的当前输入,传递给下一个状态的h^t区别也会较大。 41 | > 42 | > 上面说到LSTM中的 $c^t$主要是用来保存先前节点的数据的,那么RNN只有 $h^t$,那么这个 $h^t$ 肯定主要是保存了先前节点的信息的,所以我们说RNN中的 $h^t$ 实际上对应的是LSTM中的 $c^t$。 43 | 44 | ### 深度解读 LSTM 结构 45 | 46 | 首先使用LSTM的当前输入 $x^t$ 和上一个状态传递下来的 $h^{t-1}$ 拼接得到四个状态。 47 | 48 | img 49 | 50 | preview 51 | 52 | 其中,$z^i$、$z^f$ 、$z^o$ 是由拼接向量乘以权重矩阵之后,再通过一个 sigmoid 激活函数转换成 0 到 1 之间的数值,来作为一种门控状态。而 $z$ 则是将结果通过一个 tanh 激活函数将其转换为 -1 到 1 之间的值。(这里使用 tanh 是因为这里是将其做为输入数据,而不是门控信号) 53 | 54 | 这四个状态在 LSTM 内部是怎么使用的呢?(把这张图印在脑子里吧) 55 | 56 | img 57 | 58 | 其中 $\bigodot$ 运算符表示矩阵中对应的元素相乘,而 $\bigoplus$ 表示进行矩阵加法。因此要求两个矩阵是同型的。 59 | 60 | 上图表达了 LSTM 的三个阶段: 61 | 62 | 1. **忘记阶段**:这个阶段主要是对上一个结点传进来的输入进行**选择性**忘记,忘记不重要的,记住重要的。具体来说是通过计算得到的 $z^f$ (f 代表 forget) 来作为忘记门控,来控制上一个状态的 $c^{t-1}$ 哪些需要留哪些需要忘记。 63 | 64 | 2. **选择记忆阶段**:这个阶段将这个阶段的输入有选择性地进行“记忆”。主要是会对输入 $x^t$ 进行选择记忆。哪些重要则着重记录下来,哪些不重要,则少记一些。当前的输入内容由前面计算得到的 $z$ 表示。而选择的门控信号则是由 $z^i$ ( i 代表 information ) 来进行控制。 65 | 66 | 将上面两步得到的结果相加,即可得到传输给下一个状态的 $c^t$ 。 67 | 68 | 3. **输出阶段**:这个阶段将决定哪些将会被当成当前状态的输出。主要是通过 $z^o$ 来进行控制的。并且还对上一阶段得到的 $c^t$ 进行了放缩(通过一个 tanh 激活函数进行变化)。 69 | 70 | 与普通 RNN 类似,输出的 $y^t$ 最终也是通过 $h^t$ 变化得到。 71 | 72 | 这里再摘抄一下评论中对 $c^t$ 和 $h^t$ 的理解: 73 | 74 | >无论是 RNN 还是 LSTM ,$h^t$ 感觉表示的都是短期记忆,RNN 相当于LSTM 中的最后一个“输出门”的操作,是 LSTM 的一个特例,也就是 LSTM 中的短期记忆知识,而 LSTM 包含了长短期的记忆,其中 $c^t$ 就是对前期记忆的不断加工,锤炼和理解,沉淀下来的,而 $h^t$ 只是对前期知识点的短暂记忆,是会不断消失的。 75 | > 76 | >$c^t$ 之所以变化慢,主要是对前期记忆和当前输入的线性变换,对前期记忆的更新和变化(可以视为理解或者领悟出来的内容),是线性变换,所以变动不大,而 $h^t$ 是做的非线性变化,根据输入节点内容和非线性变化函数的不同,变动固然很大 77 | 78 | LSTM 解决了需要长期记忆的任务的问题,比较好用,但也因为引入了很多内容,导致参数变多,也使得训练难度加大了很多。因此很多时候我们往往会使用效果和LSTM相当但参数更少的GRU来构建大训练量的模型。 79 | 80 | ## 参考资料 81 | 82 | [人人都能看懂的LSTM](https://zhuanlan.zhihu.com/p/32085405) -------------------------------------------------------------------------------- /计算机视觉/49_Faster_R_CNN和Mask_R_CNN的损失函数详解.md: -------------------------------------------------------------------------------- 1 | ## 问题 2 | 3 | 我的项目中涉及到这两个网络,而网络的最重要的部分除了巧妙的结构设计外,当属损失函数的设计了,所以很多时候面试官都会问我,损失函数使用的是啥,我想他们期待我给的答案是自己设计的损失函数吧,但是做项目的时候我那么菜,根本没有能力去设计好吧(说得好像现在有似的……),所以都是使用人家原有的损失函数,并没做修改。 4 | 5 | 时间久了,这些东西当再次被面试官问起的时候,希望我至少能把人家论文中的损失函数形式讲清楚吧。 6 | 7 | ## 简单介绍下这两个网络 8 | 9 | 目标检测的框架中包含4个关键模块,包括region proposal(生成ROI)、feature extraction(特征提取网络)、classification(ROI分类)、regression(ROI回归)。而faster-rcnn利用一个神经网络将这4个模块结合起来,训练了一个端到端的网络。 10 | 11 | 1. **特征提取网络**:它用来从大量的图片中提取出一些不同目标的重要特征,通常由conv+relu+pool层构成,常用一些预训练好的网络(VGG、Inception、Resnet等),获得的结果叫做特征图; 12 | 13 | 2. **生成ROI**:在获得的特征图的每一个点上做多个候选ROI(这里是9),然后利用分类器将这些ROI区分为背景和前景,同时利用回归器对这些ROI的位置进行初步的调整; 14 | 15 | 3. **ROI分类**:在RPN阶段,用来区分前景(于真实目标重叠并且其重叠区域大于0.5)和背景(不与任何目标重叠或者其重叠区域小于0.1);在Fast-rcnn阶段,用于区分不同种类的目标(猫、狗、人等); 16 | 17 | 4. **ROI回归**:在RPN阶段,进行初步调整;在Fast-rcnn阶段进行精确调整; 18 | 19 | 总之,其整体流程如下所示: 20 | 21 | 首先对输入的图片进行裁剪操作,并将裁剪后的图片送入预训练好的分类网络中获取该图像对应的特征图; 22 | 然后在特征图上的每一个锚点上取9个候选的ROI(3个不同尺度,3个不同长宽比),并根据相应的比例将其映射到原始图像中(因为特征提取网络一般有conv和pool组成,但是只有pool会改变特征图的大小,因此最终的特征图大小和pool的个数相关); 23 | 接着将这些候选的ROI输入到RPN网络中,RPN网络对这些ROI进行分类(即确定这些ROI是前景还是背景)同时对其进行初步回归(即计算这些前景ROI与真实目标之间的BB的偏差值,包括Δx、Δy、Δw、Δh),然后做NMS(非极大值抑制,即根据分类的得分对这些ROI进行排序,然后选择其中的前N个ROI); 24 | 接着对这些不同大小的ROI进行ROI Pooling操作(即将其映射为特定大小的feature_map,文中是7x7),输出固定大小的feature_map; 25 | 最后将其输入简单的检测网络中,然后利用1x1的卷积进行分类(区分不同的类别,N+1类,多余的一类是背景,用于删除不准确的ROI),同时进行BB回归(精确的调整预测的ROI和GT的ROI之间的偏差值),从而输出一个BB集合。 26 | 整个**Mask R-CNN**算法的思路很简单,就是在原始Faster-rcnn算法的基础上面增加了FCN来产生对应的MASK分支。即Faster-rcnn + FCN,更细致的是 RPN + ROIAlign + Fast-rcnn + FCN。 27 | 28 | **FCN**算法是一个经典的语义分割算法,可以对图片中的目标进行准确的分割。它是一个端到端的网络,主要的模快包括卷积和反卷积,即先对图像进行卷积和池化,使其feature map的大小不断减小;然后进行反卷积操作,即进行插值操作,不断的增大其feature map,最后对每一个像素值进行分类。从而实现对输入图像的准确分割。 29 | 30 | ## 损失函数 31 | 32 | Faster R-CNN 是目标检测网络,所以其损失函数由两部分组成:$L = L_{cls} + L_{box}$ 33 | 34 | Mask R-CNN 是实例分割网络,所以其损失函数由三部分组成:$L = L_{cls} + L_{box}+L_{mask}$ 35 | 36 | 其中 Mask R-CNN 损失函数中的前两项与 Faster R-CNN 中的是一样的。 37 | 38 | ### mask损失函数$L_{mask}$ 39 | 40 | 对于每一个ROI,mask分支有 $K*m*m$ 维度的输出,其对K个大小为m*m的mask进行编码,每一个ROI有K个类别。我们使用了per-pixel sigmoid,并且将Lmask定义为the average binary cross-entropy loss 。对应一个属于GT中的第k类的ROI,Lmask仅仅在第k个mask上面有定义(其它的k-1个mask输出对整个Loss没有贡献)。我们定义的Lmask允许网络为每一类生成一个mask,而不用和其它类进行竞争;我们依赖于分类分支所预测的类别标签来选择输出的mask。这样将分类和mask生成分解开来。这与利用FCN进行语义分割的有所不同,它通常使用一个per-pixel sigmoid和一个multinomial cross-entropy loss ,在这种情况下mask之间存在竞争关系;而由于我们使用了一个per-pixel sigmoid 和一个binary loss ,不同的mask之间不存在竞争关系。经验表明,这可以提高实例分割的效果。 41 | 42 | 具体公式如下: 43 | $$ 44 | L_{mask} = \frac{1}{m^2}\sum_{i}^{K}(1^k)\sum_{1}^{m^2}[-y*log(sigmoid(x)) - (1-y)*log(1-sigmoid(x))] 45 | $$ 46 | 其中: 47 | 48 | - $1^k$表示当第 k 个通道对应目标的真实类别时为1,否则为0 ; 49 | 50 | * y 表示当前位置的mask的label值,为0或1; 51 | 52 | * x 当前位置的输出值,sigmoid(x) 表示输出x经过sigmoid函数变换后的结果; 53 | 54 | ### Faster R-CNN 的损失函数 55 | 56 | Faster RCNN的的损失主要分为RPN的损失和Fast RCNN的损失,计算公式如下,并且两部分损失都包括**分类损失**(cls loss)和**回归损失**(bbox regression loss)。 57 | $$ 58 | L(\{p_i\}\{t_i\}) = \frac{1}{N_{cls}}\sum_iL_{cls}(p_i,p_i^*)+\lambda \frac{1}{N_{reg}}\sum_{i}p_i^*L_{reg}(t_i,t_i^*) 59 | $$ 60 | 61 | #### RPN 的分类损失 62 | 63 | RPN网络的产生的anchor只分为前景和背景,前景的标签为1,背景的标签为0。在训练RPN的过程中,会选择256个anchor,256就是公式中的$N_{cls}$。这里的损失是经典的二分类交叉熵损失。 64 | 65 | ![](https://i.loli.net/2020/06/21/FQKfa5hwI3Y4zDg.png) 66 | 67 | 假设我们RPN网络的特征图大小为38×50,那么就会产生38×50×9=17100个anchor,然后在RPN的训练阶段会从17100个anchor中挑选Ncls个anchor用来训练RPN的参数,其中挑选为前景的标签为1,背景的标签为0。 68 | 69 | #### Fast RCNN分类损失 70 | 71 | RPN的分类损失时二分类的交叉熵损失,而Fast RCNN是**多分类的交叉熵损失**(当你训练的类别数>2时,这里假定类别数为5)。在Fast RCNN的训练过程中会选出128个rois,即**Ncls = 128**,标签的值就是0到4。 72 | 73 | #### 回归损失 74 | 75 | 回归损失这块就RPN和Fast RCNN一起讲,公式为:$\lambda \frac{1}{N_{reg}}\sum_{i}p_i^*L_{reg}(t_i,t_i^*)$ 76 | 77 | 其中 $t_i=\{t_x,t_y,t_w,t_h\}$ 是一个向量,表示anchor,RPN训练阶段(rois,FastRCNN阶段)**预测的偏移量**,x, y, w, h分别表示锚盒 anchor 的中心坐标、宽和高。 78 | 79 | $t_i^*$ 是与 $t_i$ 维度相同的向量,表示anchor,RPN训练阶段(rois,FastRCNN阶段)相对于gt**实际的偏移量** 80 | 81 | ![img](https://i.loli.net/2020/06/21/ULwRnE6qFXCgZ2j.png) 82 | 83 | R是 smoothL1 函数,不同之处是这里**σ = 3,RPN训练(σ = 1,Fast RCNN训练)** 84 | 85 | ![img](https://img-blog.csdnimg.cn/20181212134143118.png) 86 | 87 | 其中 $x=t_i - t_i^*$ 88 | 89 | 对于每一个 anchor 计算完 $L_{reg}(t_i,t_i^*)$ 部分后还要乘以P*,如前所述,**P\*有物体时(positive)为1,没有物体(negative)时为0,意味着只有前景才计算损失,背景不计算损失。** 90 | 91 | 参数 $\lambda$ 理解为为平衡分类损失和回归损失而引入的权重参数。 92 | 93 | ## 参考资料 94 | 95 | [Mask R-CNN 论文笔记](https://blog.csdn.net/JerryZhang__/article/details/88385290) 96 | [Mask R-CNN详解](https://blog.csdn.net/WZZ18191171661/article/details/79453780) 97 | [Faster-rcnn详解](https://blog.csdn.net/WZZ18191171661/article/details/79439212) 98 | [【Faster RCNN】损失函数理解](https://blog.csdn.net/Mr_health/article/details/84970776) 99 | [Mask-RCNN 算法及其实现详解](https://blog.csdn.net/remanented/article/details/79564045)(讲得非常好) 100 | 101 | --------------------------------------------------------------------------------