├── .github └── workflows │ └── update.yml ├── Kaggle-Competition ├── PS-S3-Ep23 │ ├── 12th-place-solution.ipynb │ ├── 2th-place-solution.ipynb │ ├── Advanced-model-stacking-trick.ipynb │ ├── Easy-to-maintain-code.ipynb │ ├── README.md │ ├── Software Defect Prediction │ │ ├── about JM1 Dataset.txt │ │ ├── jm1.arff │ │ └── jm1.csv │ └── playground-series-s3e23 │ │ ├── sample_submission.csv │ │ ├── test.csv │ │ └── train.csv └── PS-S3-Ep25 │ ├── 1th-place-solution.ipynb │ ├── README.md │ └── playground-series-s3e25 │ ├── sample_submission.csv │ ├── test.csv │ └── train.csv ├── PDF ├── 交叉检验.pdf ├── 准备环境.pdf ├── 可重复代码和模型方法.pdf ├── 图像分类和分割方法.pdf ├── 处理分类变量.pdf ├── 文本分类或回归方法.pdf ├── 无监督和有监督学习.pdf ├── 特征工程.pdf ├── 特征选择.pdf ├── 组合和堆叠方法.pdf ├── 组织机器学习项目.pdf ├── 评估指标.pdf └── 超参数优化.pdf ├── README.md ├── docs ├── figures │ ├── AAAMLP_page101_image.png │ ├── AAAMLP_page102_image.png │ ├── AAAMLP_page119_image.png │ ├── AAAMLP_page11_image.png │ ├── AAAMLP_page12_image.png │ ├── AAAMLP_page136_image.png │ ├── AAAMLP_page142_image.png │ ├── AAAMLP_page144_image.png │ ├── AAAMLP_page146_image.png │ ├── AAAMLP_page147_image.png │ ├── AAAMLP_page148_image.png │ ├── AAAMLP_page148_image_1.png │ ├── AAAMLP_page149_image_2.png │ ├── AAAMLP_page14_image.png │ ├── AAAMLP_page150_image.png │ ├── AAAMLP_page151_image.png │ ├── AAAMLP_page152_image.png │ ├── AAAMLP_page155_image.png │ ├── AAAMLP_page161_image.png │ ├── AAAMLP_page163_image.png │ ├── AAAMLP_page168_image.png │ ├── AAAMLP_page179_image.png │ ├── AAAMLP_page186_image.png │ ├── AAAMLP_page186_image_1.png │ ├── AAAMLP_page189_image.png │ ├── AAAMLP_page189_image_1.png │ ├── AAAMLP_page190_image.png │ ├── AAAMLP_page191_image.png │ ├── AAAMLP_page19_image.png │ ├── AAAMLP_page205_image.png │ ├── AAAMLP_page206_image.png │ ├── AAAMLP_page20_image.png │ ├── AAAMLP_page219_image.png │ ├── AAAMLP_page21_image.png │ ├── AAAMLP_page21_image_1.png │ ├── AAAMLP_page225_image.png │ ├── AAAMLP_page244_image.png │ ├── AAAMLP_page249_image.png │ ├── AAAMLP_page24_image.png │ ├── AAAMLP_page25_image.png │ ├── AAAMLP_page26_image.png │ ├── AAAMLP_page273_image.png │ ├── AAAMLP_page280_image.png │ ├── AAAMLP_page30_image.png │ ├── AAAMLP_page39_image.png │ ├── AAAMLP_page43_image.png │ ├── AAAMLP_page44_image.png │ ├── AAAMLP_page47_image.png │ ├── AAAMLP_page48_image.png │ ├── AAAMLP_page57_image.png │ ├── AAAMLP_page58_image.png │ ├── AAAMLP_page59_image.png │ ├── AAAMLP_page6_image.png │ ├── AAAMLP_page74_image.png │ ├── AAAMLP_page74_image_1.png │ ├── AAAMLP_page85_image.png │ ├── AAAMLP_page86_image.png │ ├── AAAMLP_page8_image.png │ ├── AAAMLP_page9_image.png │ └── AAAMLP_page9_image_1.png ├── index.md ├── javascripts │ └── katex.js ├── 交叉检验.md ├── 准备环境.md ├── 可重复代码和模型方法.md ├── 图像分类和分割方法.md ├── 处理分类变量.md ├── 文本分类或回归方法.md ├── 无监督和有监督学习.md ├── 特征工程.md ├── 特征选择.md ├── 组合和堆叠方法.md ├── 组织机器学习项目.md ├── 评估指标.md └── 超参数优化.md ├── mkdocs.yml └── requirements.txt /.github/workflows/update.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: update 4 | 5 | # Controls when the action will run. Triggers the workflow on push or pull request 6 | on: 7 | push: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | deploy: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | with: 17 | fetch-depth: 0 18 | 19 | - uses: actions/setup-node@v2 20 | with: 21 | node-version: '16' 22 | 23 | - uses: actions/setup-python@v2 24 | with: 25 | python-version: 3.x 26 | 27 | - run: pip3 install -U -r requirements.txt 28 | - run: mkdocs gh-deploy --force 29 | -------------------------------------------------------------------------------- /Kaggle-Competition/PS-S3-Ep23/README.md: -------------------------------------------------------------------------------- 1 | # Playground-Series-S3E23 2 | 3 | ## 数据来源 4 | 5 | - 数据来自Kaggle,[数据说明](https://www.kaggle.com/competitions/playground-series-s3e23/data) 6 | - 文件夹中的notebook相较于原始文件做了可能有利的改动,所以可能与原文件不同 7 | - 对**2th-place-solution**、**12th-place-solution**、**Easy-to-maintain-code**进行了全文逐行中文注释 8 | 9 | ## 解决方案 10 | 11 | ### 2th-place-solution 12 | 13 | - 第2名解决方案来自**Oscar Aguilar**,[个人主页](https://www.kaggle.com/oscarm524) 14 | 15 | - 解决方案[说明](https://www.kaggle.com/competitions/playground-series-s3e23/discussion/450315) 16 | - 解决方案原[jupyter notebook](https://www.kaggle.com/code/oscarm524/ps-s3-ep23-eda-modeling-submission) 17 | 18 | ### 12th-place-solution 19 | 20 | - 第12名解决方案来自**Vasya E**,[个人主页](https://www.kaggle.com/vasiliie) 21 | - 解决方案原[jupyter notebook](https://www.kaggle.com/code/vasiliie/ps3e23-top-1-well-structured-stacking-ensemble) 22 | 23 | ### Advanced-model-stacking-trick 24 | 25 | - 第32名解决方案来自**Master Jiraiya**,[个人主页](https://www.kaggle.com/arunklenin) 26 | - 该notebook中有大量机器学习小trick,包括如何进行特征工程,筛选特征,变换特征 27 | - 关于模型堆叠和集合部分也十分复杂,需要时间慢慢理解,但所展现出来的经验可以被用在其他项目上 28 | 29 | - 解决方案原[jupyter notebook](https://www.kaggle.com/code/arunklenin/ps3e23-eda-feature-engineering-ensemble) 30 | 31 | ### Easy-to-maintain-code 32 | 33 | - 推荐这个notebook的原因是代码非常整洁,可视化做的也非常棒,同时分数也不算太低 34 | - 希望大家在做自己的项目时,也能保持这样的结构,方便后期复用和随时调整 35 | - 解决方案原[jupyter notebook](https://www.kaggle.com/code/ravi20076/playgrounds3e23-eda-baseline) 36 | -------------------------------------------------------------------------------- /Kaggle-Competition/PS-S3-Ep23/Software Defect Prediction/about JM1 Dataset.txt: -------------------------------------------------------------------------------- 1 | %%-*- text -*- 2 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 3 | % This is a PROMISE data set made publicly available in order to encourage 4 | % repeatable, verifiable, refutable, and/or improvable predictive models 5 | % of software engineering. 6 | % 7 | % If you publish material based on PROMISE data sets then, please 8 | % follow the acknowledgment guidelines posted on the PROMISE repository 9 | % web page http://promise.site.uottawa.ca/SERepository . 10 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 11 | 12 | % 1. Title/Topic: JM1/software defect prediction 13 | 14 | % 2. Sources: 15 | % 16 | % -- Creators: NASA, then the NASA Metrics Data Program, 17 | % -- http://mdp.ivv.nasa.gov. Contacts: Mike Chapman, 18 | % Galaxy Global Corporation (Robert.Chapman@ivv.nasa.gov) 19 | % +1-304-367-8341; Pat Callis, NASA, NASA project manager 20 | % for MDP (Patrick.E.Callis@ivv.nasa.gov) +1-304-367-8309 21 | % 22 | % -- Donor: Tim Menzies (tim@barmag.net) 23 | % 24 | % -- Date: December 2 2004 25 | 26 | % 3. Past usage: 27 | % 28 | % 1. How Good is Your Blind Spot Sampling Policy?; 2003; Tim Menzies 29 | % and Justin S. Di Stefano; 2004 IEEE Conference on High Assurance 30 | % Software Engineering (http://menzies.us/pdf/03blind.pdf). 31 | % 32 | % -- Results: 33 | % 34 | % -- Very simple learners (ROCKY) perform as well in this domain 35 | % as more sophisticated methods (e.g. J48, model trees, model 36 | % trees) for predicting detects 37 | % 38 | % -- Many learners have very low false alarm rates. 39 | % 40 | % -- Probability of detection (PD) rises with effort and rarely 41 | % rises above it. 42 | % 43 | % -- High PDs are associated with high PFs (probability of 44 | % failure) 45 | % 46 | % -- PD, PF, effort can change significantly while accuracy 47 | % remains essentially stable 48 | % 49 | % -- With two notable exceptions, detectors learned from one 50 | % data set (e.g. KC2) have nearly they same properties when 51 | % applied to another (e.g. PC2, KC2). Exceptions: 52 | % -- LinesOfCode measures generate wider inter-data-set variances; 53 | % -- Precision's inter-data-set variances vary wildly 54 | % 55 | % 2. "Assessing Predictors of Software Defects", T. Menzies and 56 | % J. DiStefano and A. Orrego and R. Chapman, 2004, 57 | % Proceedings, workshop on Predictive Software Models, Chicago, 58 | % Available from http://menzies.us/pdf/04psm.pdf. 59 | % -- Results: 60 | % 61 | % -- From JM1, Naive Bayes generated PDs of 25% with PF of 20% 62 | % 63 | % -- Naive Bayes out-performs J48 for defect detection 64 | % 65 | % -- When learning on more and more data, little improvement is 66 | % seen after processing 300 examples. 67 | % 68 | % -- PDs are much higher from data collected below the sub-sub- 69 | % system level. 70 | % 71 | % -- Accuracy is a surprisingly uninformative measure of success 72 | % for a defect detector. Two detectors with the same accuracy 73 | % can have widely varying PDs and PFs. 74 | 75 | % 4. Relevant information: 76 | % 77 | % -- JM1 is written in "C" and is a real-time predictive ground system: 78 | % Uses simulations to generate predictions 79 | % 80 | % -- Data comes from McCabe and Halstead features extractors of 81 | % source code. These features were defined in the 70s in an attempt 82 | % to objectively characterize code features that are associated with 83 | % software quality. The nature of association is under dispute. 84 | % Notes on McCabe and Halstead follow. 85 | % 86 | % -- The McCabe and Halstead measures are "module"-based where a 87 | % "module" is the smallest unit of functionality. In C or Smalltalk, 88 | % "modules" would be called "function" or "method" respectively. 89 | % 90 | % -- Defect detectors can be assessed according to the following measures: 91 | % 92 | % module actually has defects 93 | % +-------------+------------+ 94 | % | no | yes | 95 | % +-----+-------------+------------+ 96 | % classifier predicts no defects | no | a | b | 97 | % +-----+-------------+------------+ 98 | % classifier predicts some defects | yes | c | d | 99 | % +-----+-------------+------------+ 100 | % 101 | % accuracy = acc = (a+d)/(a+b+c+d 102 | % probability of detection = pd = recall = d/(b+d) 103 | % probability of false alarm = pf = c/(a+c) 104 | % precision = prec = d/(c+d) 105 | % effort = amount of code selected by detector 106 | % = (c.LOC + d.LOC)/(Total LOC) 107 | % 108 | % Ideally, detectors have high PDs, low PFs, and low 109 | % effort. This ideal state rarely happens: 110 | % 111 | % -- PD and effort are linked. The more modules that trigger 112 | % the detector, the higher the PD. However, effort also gets 113 | % increases 114 | % 115 | % -- High PD or low PF comes at the cost of high PF or low PD 116 | % (respectively). This linkage can be seen in a standard 117 | % receiver operator curve (ROC). Suppose, for example, LOC> x 118 | % is used as the detector (i.e. we assume large modules have 119 | % more errors). LOC > x represents a family of detectors. At 120 | % x=0, EVERY module is predicted to have errors. This detector 121 | % has a high PD but also a high false alarm rate. At x=0, NO 122 | % module is predicted to have errors. This detector has a low 123 | % false alarm rate but won't detect anything at all. At 0 but does not reach it. 149 | % 150 | % -- The line pf=pd on the above graph represents the "no information" 151 | % line. If pf=pd then the detector is pretty useless. The better 152 | % the detector, the more it rises above PF=PD towards the "sweet spot". 153 | % 154 | % NOTES ON MCCABE/HALSTEAD 155 | % ======================== 156 | % McCabe argued that code with complicated pathways are more 157 | % error-prone. His metrics therefore reflect the pathways within a 158 | % code module. 159 | % @Article{mccabe76, 160 | % title = "A Complexity Measure", 161 | % author = "T.J. McCabe", 162 | % pages = "308--320", 163 | % journal = "IEEE Transactions on Software Engineering", 164 | % year = "1976", 165 | % volume = "2", 166 | % month = "December", 167 | % number = "4"} 168 | % 169 | % Halstead argued that code that is hard to read is more likely to be 170 | % fault prone. Halstead estimates reading complexity by counting the 171 | % number of concepts in a module; e.g. number of unique operators. 172 | % @Book{halstead77, 173 | % Author = "M.H. Halstead", 174 | % Title = "Elements of Software Science", 175 | % Publisher = "Elsevier ", 176 | % Year = 1977} 177 | % 178 | % We study these static code measures since they are useful, easy to 179 | % use, and widely used: 180 | % 181 | % -- USEFUL: E.g. this data set can generate highly accurate 182 | % predictors for defects 183 | % 184 | % -- EASY TO USE: Static code measures (e.g. lines of code, the 185 | % McCabe/Halstead measures) can be automatically and cheaply 186 | % collected. 187 | % 188 | % -- WIDELY USED: Many researchers use static measures to guide 189 | % software quality predictions (see the reference list in the above 190 | % "blind spot" paper. Verification and validation (V\&V) textbooks 191 | % advise using static code complexity measures to decide which 192 | % modules are worthy of manual inspections. Further, we know of 193 | % several large government software contractors that won't review 194 | % software modules _unless_ tools like McCabe predict that they are 195 | % fault prone. Hence, defect detectors have a major economic impact 196 | % when they may force programmers to rewrite code. 197 | % 198 | % Nevertheless, the merits of these metrics has been widely 199 | % criticized. Static code measures are hardly a complete 200 | % characterization of the internals of a function. Fenton offers an 201 | % insightful example where the same functionality is achieved using 202 | % different programming language constructs resulting in different 203 | % static measurements for that module. Fenton uses this example to 204 | % argue the uselessness of static code measures. 205 | % @book{fenton97, 206 | % author = "N.E. Fenton and S.L. Pfleeger", 207 | % title = {Software metrics: a Rigorous \& Practical Approach}, 208 | % publisher = {International Thompson Press}, 209 | % year = {1997}} 210 | % 211 | % An alternative interpretation of Fenton's example is that static 212 | % measures can never be a definite and certain indicator of the 213 | % presence of a fault. Rather, defect detectors based on static 214 | % measures are best viewed as probabilistic statements that the 215 | % frequency of faults tends to increase in code modules that trigger 216 | % the detector. By definition, such probabilistic statements will 217 | % are not categorical claims with some a non-zero false alarm 218 | % rate. The research challenge for data miners is to ensure that 219 | % these false alarms do not cripple their learned theories. 220 | % 221 | % The McCabe metrics are a collection of four software metrics: 222 | % essential complexity, cyclomatic complexity, design complexity and 223 | % LOC, Lines of Code. 224 | % 225 | % -- Cyclomatic Complexity, or "v(G)", measures the number of 226 | % "linearly independent paths". A set of paths is said to be 227 | % linearly independent if no path in the set is a linear combination 228 | % of any other paths in the set through a program's "flowgraph". A 229 | % flowgraph is a directed graph where each node corresponds to a 230 | % program statement, and each arc indicates the flow of control from 231 | % one statement to another. "v(G)" is calculated by "v(G) = e - n + 2" 232 | % where "G" is a program's flowgraph, "e" is the number of arcs in 233 | % the flowgraph, and "n" is the number of nodes in the 234 | % flowgraph. The standard McCabes rules ("v(G)">10), are used to 235 | % identify fault-prone module. 236 | % 237 | % -- Essential Complexity, or "ev(G)$" is the extent to which a 238 | % flowgraph can be "reduced" by decomposing all the subflowgraphs 239 | % of $G$ that are "D-structured primes". Such "D-structured 240 | % primes" are also sometimes referred to as "proper one-entry 241 | % one-exit subflowgraphs" (for a more thorough discussion of 242 | % D-primes, see the Fenton text referenced above). "ev(G)" is 243 | % calculated using "ev(G)= v(G) - m" where $m$ is the number of 244 | % subflowgraphs of "G" that are D-structured primes. 245 | % 246 | % -- Design Complexity, or "iv(G)", is the cyclomatic complexity of a 247 | % module's reduced flowgraph. The flowgraph, "G", of a module is 248 | % reduced to eliminate any complexity which does not influence the 249 | % interrelationship between design modules. According to McCabe, 250 | % this complexity measurement reflects the modules calling patterns 251 | % to its immediate subordinate modules. 252 | % 253 | % -- Lines of code is measured according to McCabe's line counting 254 | % conventions. 255 | % 256 | % The Halstead falls into three groups: the base measures, the 257 | % derived measures, and lines of code measures. 258 | % 259 | % -- Base measures: 260 | % -- mu1 = number of unique operators 261 | % -- mu2 = number of unique operands 262 | % -- N1 = total occurrences of operators 263 | % -- N2 = total occurrences of operands 264 | % -- length = N = N1 + N2 265 | % -- vocabulary = mu = mu1 + mu2 266 | % -- Constants set for each function: 267 | % -- mu1' = 2 = potential operator count (just the function 268 | % name and the "return" operator) 269 | % -- mu2' = potential operand count. (the number 270 | % of arguments to the module) 271 | % 272 | % For example, the expression "return max(w+x,x+y)" has "N1=4" 273 | % operators "return, max, +,+)", "N2=4" operands (w,x,x,y), 274 | % "mu1=3" unique operators (return, max,+), and "mu2=3" unique 275 | % operands (w,x,y). 276 | % 277 | % -- Derived measures: 278 | % -- P = volume = V = N * log2(mu) (the number of mental 279 | % comparisons needed to write 280 | % a program of length N) 281 | % -- V* = volume on minimal implementation 282 | % = (2 + mu2')*log2(2 + mu2') 283 | % -- L = program length = V*/N 284 | % -- D = difficulty = 1/L 285 | % -- L' = 1/D 286 | % -- I = intelligence = L'*V' 287 | % -- E = effort to write program = V/L 288 | % -- T = time to write program = E/18 seconds 289 | 290 | % 5. Number of instances: 10885 291 | 292 | % 6. Number of attributes: 22 (5 different lines of code measure, 293 | % 3 McCabe metrics, 4 base Halstead measures, 8 derived 294 | % Halstead measures, a branch-count, and 1 goal field) 295 | 296 | % 7. Attribute Information: 297 | % 298 | % 1. loc : numeric % McCabe's line count of code 299 | % 2. v(g) : numeric % McCabe "cyclomatic complexity" 300 | % 3. ev(g) : numeric % McCabe "essential complexity" 301 | % 4. iv(g) : numeric % McCabe "design complexity" 302 | % 5. n : numeric % Halstead total operators + operands 303 | % 6. v : numeric % Halstead "volume" 304 | % 7. l : numeric % Halstead "program length" 305 | % 8. d : numeric % Halstead "difficulty" 306 | % 9. i : numeric % Halstead "intelligence" 307 | % 10. e : numeric % Halstead "effort" 308 | % 11. b : numeric % Halstead 309 | % 12. t : numeric % Halstead's time estimator 310 | % 13. lOCode : numeric % Halstead's line count 311 | % 14. lOComment : numeric % Halstead's count of lines of comments 312 | % 15. lOBlank : numeric % Halstead's count of blank lines 313 | % 16. lOCodeAndComment: numeric 314 | % 17. uniq_Op : numeric % unique operators 315 | % 18. uniq_Opnd : numeric % unique operands 316 | % 19. total_Op : numeric % total operators 317 | % 20. total_Opnd : numeric % total operands 318 | % 21: branchCount : numeric % of the flow graph 319 | % 22. defects : {false,true} % module has/has not one or more 320 | % % reported defects 321 | 322 | % 8. Missing attributes: none 323 | 324 | % 9. Class Distribution: the class value (defects) is discrete 325 | % false: 2106 = 19.35% 326 | % true: 8779 = 80.65% 327 | 328 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 329 | -------------------------------------------------------------------------------- /Kaggle-Competition/PS-S3-Ep25/README.md: -------------------------------------------------------------------------------- 1 | # Playground-Series-S3E25 2 | 3 | ## 数据来源 4 | 5 | - 数据来自Kaggle,[数据说明](https://www.kaggle.com/competitions/playground-series-s3e25/data) 6 | - 此次比赛是回归问题,但最后大家发现转换成分类得到的结果居然会更好 7 | - 在Private排行榜上前189名得分完全一致,均为0.25,是一场非常有意思的比赛 8 | 9 | ## 解决方案 10 | 11 | ### 1th-place-solution 12 | 13 | - 第1名解决方案来自**Oscar Aguilar**,[个人主页](https://www.kaggle.com/oscarm524) 14 | - 解决方案原[jupyter notebook](https://www.kaggle.com/code/oscarm524/ps-s3-ep25-eda-modeling-submission) -------------------------------------------------------------------------------- /PDF/交叉检验.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/PDF/交叉检验.pdf -------------------------------------------------------------------------------- /PDF/准备环境.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/PDF/准备环境.pdf -------------------------------------------------------------------------------- /PDF/可重复代码和模型方法.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/PDF/可重复代码和模型方法.pdf -------------------------------------------------------------------------------- /PDF/图像分类和分割方法.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/PDF/图像分类和分割方法.pdf -------------------------------------------------------------------------------- /PDF/处理分类变量.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/PDF/处理分类变量.pdf -------------------------------------------------------------------------------- /PDF/文本分类或回归方法.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/PDF/文本分类或回归方法.pdf -------------------------------------------------------------------------------- /PDF/无监督和有监督学习.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/PDF/无监督和有监督学习.pdf -------------------------------------------------------------------------------- /PDF/特征工程.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/PDF/特征工程.pdf -------------------------------------------------------------------------------- /PDF/特征选择.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/PDF/特征选择.pdf -------------------------------------------------------------------------------- /PDF/组合和堆叠方法.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/PDF/组合和堆叠方法.pdf -------------------------------------------------------------------------------- /PDF/组织机器学习项目.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/PDF/组织机器学习项目.pdf -------------------------------------------------------------------------------- /PDF/评估指标.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/PDF/评估指标.pdf -------------------------------------------------------------------------------- /PDF/超参数优化.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/PDF/超参数优化.pdf -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AAAMLP-CN 2 | ## 新特性 3 | ### 2023.10.25 4 | - 😎完成全部翻译 5 | - 📝计划对kaggle游乐园系列优秀解决方案代码进行解析 6 | ### 2023.09.07 7 | - ⚡修正部分已知文字错误和代码错误 8 | - 🤗添加[在线文档](https://ytzfhqs.github.io/AAAMLP-CN/) 9 | 10 | ## 简介 11 | Abhishek Thakur,很多 kaggler 对他都非常熟悉,2017 年,他在 Linkedin 发表了一篇名为**Approaching (Almost) Any Machine Learning Problem**的文章,介绍他建立的一个自动的机器学习框架,几乎可以解决任何机器学习问题,这篇文章曾火遍 Kaggle。 12 | 13 | Abhishek 在 Kaggle 上的成就: 14 | 15 | - Competitions Grandmaster(17 枚金牌,世界排名第 3) 16 | - Kernels Expert (Kagglers 排名前 1%) 17 | - Discussion Grandmaster(65 枚金牌,世界排名第 2) 18 | 19 | 目前,Abhishek 在挪威 boost 公司担任首席数据科学家的职位,这是一家专门从事会话人工智能的软件公司。 20 | 21 | 本文对**Approaching (Almost) Any Machine Learning Problem**进行了**中文翻译**,由于本人水平有限,且未使用机器翻译,可能有部分言语不通顺或本土化程度不足,也请大家在阅读过程中多提供宝贵意见。另附上书籍原[项目地址](https://github.com/abhishekkrthakur/approachingalmost),**转载请一定标明出处!** 22 | 23 | 本项目**支持在线阅读**,方便您随时随地进行查阅。 24 | 25 | 因为有几章内容太过基础,所以未进行翻译,详细情况请参照书籍目录: 26 | 27 | - **准备环境(已翻译)** 28 | - **无监督和有监督学习(已翻译)** 29 | - **交叉检验(已翻译)** 30 | - **评估指标(已翻译)** - 31 | - **组织机器学习(已翻译)** 32 | - **处理分类变量(已翻译)** 33 | - **特征工程(已翻译)** 34 | - **特征选择(已翻译)** 35 | - **超参数优化(已翻译)** 36 | - **图像分类和分割方法(已翻译)** 37 | - **文本分类或回归方法(已翻译)** 38 | - **组合和堆叠方法(已翻译)** 39 | - **可重复代码和模型方法(已翻译)** 40 | 41 | 我将会把完整的翻译版 `Markdown` 文件上传到 GitHub,以供大家免费下载和阅读。为了最佳的阅读体验,推荐使用 PDF 格式或是在线阅读进行查看 42 | 43 | 若您在阅读过程中发现任何错误或不准确之处,非常欢迎通过提交 Issue 或 Pull Request 来协助我进行修正。 44 | 45 | 如果您觉得这个项目对您有帮助,请不吝给予 Star 或者进行关注。 46 | -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page101_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page101_image.png -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page102_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page102_image.png -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page119_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page119_image.png -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page11_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page11_image.png -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page12_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page12_image.png -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page136_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page136_image.png -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page142_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page142_image.png -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page144_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page144_image.png -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page146_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page146_image.png -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page147_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page147_image.png -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page148_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page148_image.png -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page148_image_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page148_image_1.png -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page149_image_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page149_image_2.png -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page14_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page14_image.png -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page150_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page150_image.png -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page151_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page151_image.png -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page152_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page152_image.png -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page155_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page155_image.png -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page161_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page161_image.png -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page163_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page163_image.png -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page168_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page168_image.png -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page179_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page179_image.png -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page186_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page186_image.png -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page186_image_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page186_image_1.png -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page189_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page189_image.png -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page189_image_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page189_image_1.png -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page190_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page190_image.png -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page191_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page191_image.png -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page19_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page19_image.png -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page205_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page205_image.png -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page206_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page206_image.png -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page20_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page20_image.png -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page219_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page219_image.png -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page21_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page21_image.png -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page21_image_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page21_image_1.png -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page225_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page225_image.png -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page244_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page244_image.png -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page249_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page249_image.png -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page24_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page24_image.png -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page25_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page25_image.png -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page26_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page26_image.png -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page273_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page273_image.png -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page280_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page280_image.png -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page30_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page30_image.png -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page39_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page39_image.png -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page43_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page43_image.png -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page44_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page44_image.png -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page47_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page47_image.png -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page48_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page48_image.png -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page57_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page57_image.png -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page58_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page58_image.png -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page59_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page59_image.png -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page6_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page6_image.png -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page74_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page74_image.png -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page74_image_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page74_image_1.png -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page85_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page85_image.png -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page86_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page86_image.png -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page8_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page8_image.png -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page9_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page9_image.png -------------------------------------------------------------------------------- /docs/figures/AAAMLP_page9_image_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ytzfhqs/AAAMLP-CN/b25a4386893b4148487581e883580c6d85f8dafa/docs/figures/AAAMLP_page9_image_1.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # AAAMLP-CN 2 | ## 新特性 3 | ### 2023.10.25 4 | - 😎完成全部翻译 5 | - 📝计划对kaggle游乐园系列优秀解决方案代码进行解析 6 | ### 2023.09.07 7 | - ⚡修正部分已知文字错误和代码错误 8 | - 🤗添加[在线文档](https://ytzfhqs.github.io/AAAMLP-CN/) 9 | 10 | ## 翻译进程 11 | 12 | - 2023.09.12 添加章节:**组合和堆叠方法**、**可重复代码和模型方法** 13 | 14 | ## 简介 15 | 16 | Abhishek Thakur,很多 kaggler 对他都非常熟悉,2017 年,他在 Linkedin 发表了一篇名为**Approaching (Almost) Any Machine Learning Problem**的文章,介绍他建立的一个自动的机器学习框架,几乎可以解决任何机器学习问题,这篇文章曾火遍 Kaggle。 17 | 18 | Abhishek 在 Kaggle 上的成就: 19 | 20 | - Competitions Grandmaster(17 枚金牌,世界排名第 3) 21 | - Kernels Expert (Kagglers 排名前 1%) 22 | - Discussion Grandmaster(65 枚金牌,世界排名第 2) 23 | 24 | 目前,Abhishek 在挪威 boost 公司担任首席数据科学家的职位,这是一家专门从事会话人工智能的软件公司。 25 | 26 | 本文对**Approaching (Almost) Any Machine Learning Problem**进行了**中文翻译**,由于本人水平有限,且未使用机器翻译,可能有部分言语不通顺或本土化程度不足,也请大家在阅读过程中多提供宝贵意见。另附上书籍原[项目地址](https://github.com/abhishekkrthakur/approachingalmost),**转载请一定标明出处!** 27 | 28 | 本项目**支持在线阅读**,方便您随时随地进行查阅。 29 | 30 | 因为有几章内容太过基础,所以未进行翻译,详细情况请参照书籍目录: 31 | 32 | - **准备环境(已翻译)** 33 | - **无监督和有监督学习(已翻译)** 34 | - **交叉检验(已翻译)** 35 | - **评估指标(已翻译)** - 36 | - **组织机器学习(已翻译)** 37 | - **处理分类变量(已翻译)** 38 | - **特征工程(已翻译)** 39 | - **特征选择(已翻译)** 40 | - **超参数优化(已翻译)** 41 | - **图像分类和分割方法(已翻译)** 42 | - **文本分类或回归方法(已翻译)** 43 | - **组合和堆叠方法(已翻译)** 44 | - **可重复代码和模型方法(已翻译)** 45 | 46 | 我将会把完整的翻译版 `Markdown` 文件上传到 GitHub,以供大家免费下载和阅读。为了最佳的阅读体验,推荐使用 PDF 格式或是在线阅读进行查看 47 | 48 | 若您在阅读过程中发现任何错误或不准确之处,非常欢迎通过提交 Issue 或 Pull Request 来协助我进行修正。 49 | 50 | 随着时间推移,我可能会**继续翻译尚未完成的章节**。如果您觉得这个项目对您有帮助,请不吝给予 Star 或者进行关注。 51 | -------------------------------------------------------------------------------- /docs/javascripts/katex.js: -------------------------------------------------------------------------------- 1 | document$.subscribe(({ body }) => { 2 | renderMathInElement(body, { 3 | delimiters: [ 4 | { left: '$$', right: '$$', display: true }, 5 | { left: '$', right: '$', display: false }, 6 | { left: '\\(', right: '\\)', display: false }, 7 | { left: '\\[', right: '\\]', display: true }, 8 | ], 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /docs/交叉检验.md: -------------------------------------------------------------------------------- 1 | # 交叉检验 2 | 3 | 在上一章中,我们没有建立任何模型。原因很简单,在创建任何一种机器学习模型之前,我们必须知道什么是交叉检验,以及如何根据数据集选择最佳交叉检验数据集。 4 | 5 | 那么,什么是**交叉检验**,我们为什么要关注它? 6 | 7 | 关于什么是交叉检验,我们可以找到多种定义。我的定义只有一句话:交叉检验是构建机器学习模型过程中的一个步骤,它可以帮助我们确保模型准确拟合数据,同时确保我们不会过拟合。但这又引出了另一个词:**过拟合**。 8 | 9 | 要解释过拟合,我认为最好先看一个数据集。有一个相当有名的红酒质量数据集(**red wine quality dataset**)。这个数据集有 11 个不同的特征,这些特征决定了红酒的质量。 10 | 11 | 这些属性包括: 12 | 13 | - 固定酸度(fixed acidity) 14 | - 挥发性酸度(volatile acidity) 15 | - 柠檬酸(citric acid) 16 | - 残留糖(residual sugar) 17 | - 氯化物(chlorides) 18 | - 游离二氧化硫(free sulfur dioxide) 19 | - 二氧化硫总量(total sulfur dioxide) 20 | - 密度(density) 21 | - PH 值(pH) 22 | - 硫酸盐(sulphates) 23 | - 酒精(alcohol) 24 | 25 | 根据这些不同特征,我们需要预测红葡萄酒的质量,质量值介于 0 到 10 之间。 26 | 27 | 让我们看看这些数据是怎样的。 28 | 29 | ```python 30 | import pandas as pd 31 | df = pd.read_csv("winequality-red.csv") 32 | ``` 33 | 34 | ![](figures/AAAMLP_page14_image.png) 35 | 36 |

图 1:红葡萄酒质量数据集简单展示

37 | 38 | 我们可以将这个问题视为分类问题,也可以视为回归问题。为了简单起见,我们选择分类。然而,这个数据集值包含 6 种质量值。因此,我们将所有质量值映射到 0 到 5 之间。 39 | 40 | ```python 41 | # 一个映射字典,用于将质量值从 0 到 5 进行映射 42 | quality_mapping = { 43 | 3: 0, 44 | 4: 1, 45 | 5: 2, 46 | 6: 3, 47 | 7: 4, 48 | 8: 5 49 | } 50 | 51 | # 你可以使用 pandas 的 map 函数以及任何字典, 52 | # 来转换给定列中的值为字典中的值 53 | df.loc[:, "quality"] = df.quality.map(quality_mapping) 54 | 55 | ``` 56 | 57 | 当我们看大这些数据并将其视为一个分类问题时,我们脑海中会浮现出很多可以应用的算法,也许,我们可以使用神经网络。但是,如果我们从一开始就深入研究神经网络,那就有点牵强了。所以,让我们从简单的、我们也能可视化的东西开始:决策树。 58 | 59 | 在开始了解什么是过拟合之前,我们先将数据分为两部分。这个数据集有 1599 个样本。我们保留 1000 个样本用于训练,599 个样本作为一个单独的集合。 60 | 61 | 以下代码可以轻松完成划分: 62 | 63 | ```python 64 | # 使用 frac=1 的 sample 方法来打乱 dataframe 65 | # 由于打乱后索引会改变,所以我们重置索引 66 | df = df.sample(frac=1).reset_index(drop=True) 67 | 68 | # 选取前 1000 行作为训练数据 69 | df_train = df.head(1000) 70 | 71 | # 选取最后的 599 行作为测试/验证数据 72 | df_test = df.tail(599) 73 | 74 | ``` 75 | 76 | 现在,我们将在训练集上使用 scikit-learn 训练一个决策树模型。 77 | 78 | ```python 79 | # 从 scikit-learn 导入需要的模块 80 | from sklearn import tree 81 | from sklearn import metrics 82 | 83 | # 初始化一个决策树分类器,设置最大深度为 3 84 | clf = tree.DecisionTreeClassifier(max_depth=3) 85 | 86 | # 选择你想要训练模型的列 87 | # 这些列作为模型的特征 88 | cols = ['fixed acidity', 89 | 'volatile acidity', 90 | 'citric acid', 91 | 'residual sugar', 92 | 'chlorides', 93 | 'free sulfur dioxide', 94 | 'total sulfur dioxide', 95 | 'density', 96 | 'pH', 97 | 'sulphates', 98 | 'alcohol'] 99 | 100 | # 使用之前映射的质量以及提供的特征来训练模型 101 | clf.fit(df_train[cols], df_train.quality) 102 | 103 | ``` 104 | 105 | 请注意,我将决策树分类器的最大深度(max_depth)设为 3。该模型的所有其他参数均保持默认值。现在,我们在训练集和测试集上测试该模型的准确性: 106 | 107 | ```python 108 | # 在训练集上生成预测 109 | train_predictions = clf.predict(df_train[cols]) 110 | 111 | # 在测试集上生成预测 112 | test_predictions = clf.predict(df_test[cols]) 113 | 114 | # 计算训练数据集上预测的准确度 115 | train_accuracy = metrics.accuracy_score( 116 | df_train.quality, train_predictions 117 | ) 118 | 119 | # 计算测试数据集上预测的准确度 120 | test_accuracy = metrics.accuracy_score( 121 | df_test.quality, test_predictions 122 | ) 123 | ``` 124 | 125 | 训练和测试的准确率分别为 58.9%和 54.25%。现在,我们将最大深度(max_depth)增加到 7,并重复上述过程。这样,训练准确率为 76.6%,测试准确率为 57.3%。在这里,我们使用准确率,主要是因为它是最直接的指标。对于这个问题来说,它可能不是最好的指标。我们可以根据最大深度(max_depth)的不同值来计算这些准确率,并绘制曲线图。 126 | 127 | ```python 128 | # 注意:这段代码在 Jupyter 笔记本中编写 129 | # 导入 scikit-learn 的 tree 和 metrics 130 | from sklearn import tree 131 | from sklearn import metrics 132 | # 导入 matplotlib 和 seaborn 133 | # 用于绘图 134 | import matplotlib 135 | import matplotlib.pyplot as plt 136 | import seaborn as sns 137 | 138 | # 设置全局标签文本的大小 139 | matplotlib.rc('xtick', labelsize=20) 140 | matplotlib.rc('ytick', labelsize=20) 141 | 142 | # 确保图表直接在笔记本内显示 143 | %matplotlib inline 144 | 145 | # 初始化用于存储训练和测试准确度的列表 146 | # 我们从 50% 的准确度开始 147 | train_accuracies = [0.5] 148 | test_accuracies = [0.5] 149 | 150 | # 遍历几个不同的树深度值 151 | for depth in range(1, 25): 152 | # 初始化模型 153 | clf = tree.DecisionTreeClassifier(max_depth=depth) 154 | 155 | # 选择用于训练的列/特征 156 | cols = [ 157 | 'fixed acidity', 'volatile acidity', 'citric acid', 158 | 'residual sugar', 'chlorides', 'free sulfur dioxide', 159 | 'total sulfur dioxide', 'density', 'pH', 160 | 'sulphates', 'alcohol' 161 | ] 162 | 163 | # 在给定特征上拟合模型 164 | clf.fit(df_train[cols], df_train.quality) 165 | 166 | # 创建训练和测试预测 167 | train_predictions = clf.predict(df_train[cols]) 168 | test_predictions = clf.predict(df_test[cols]) 169 | 170 | # 计算训练和测试准确度 171 | train_accuracy = metrics.accuracy_score( 172 | df_train.quality, train_predictions 173 | ) 174 | test_accuracy = metrics.accuracy_score( 175 | df_test.quality, test_predictions 176 | ) 177 | 178 | # 添加准确度到列表 179 | train_accuracies.append(train_accuracy) 180 | test_accuracies.append(test_accuracy) 181 | 182 | # 使用 matplotlib 和 seaborn 创建两个图 183 | plt.figure(figsize=(10, 5)) 184 | sns.set_style("whitegrid") 185 | plt.plot(train_accuracies, label="train accuracy") 186 | plt.plot(test_accuracies, label="test accuracy") 187 | plt.legend(loc="upper left", prop={'size': 15}) 188 | plt.xticks(range(0, 26, 5)) 189 | plt.xlabel("max_depth", size=20) 190 | plt.ylabel("accuracy", size=20) 191 | plt.show() 192 | 193 | ``` 194 | 195 | 这将生成如图 2 所示的曲线图。 196 | 197 | ![](figures/AAAMLP_page19_image.png) 198 | 199 |

图 2:不同 max_depth 训练和测试准确率。

200 | 201 | 我们可以看到,当最大深度(max_depth)的值为 14 时,测试数据的得分最高。随着我们不断增加这个参数的值,测试准确率会保持不变或变差,但训练准确率会不断提高。这说明,随着最大深度(max_depth)的增加,决策树模型对训练数据的学习效果越来越好,但测试数据的性能却丝毫没有提高。 202 | 203 | **这就是所谓的过拟合**。 204 | 205 | 模型在训练集上完全拟合,而在测试集上却表现不佳。这意味着模型可以很好地学习训练数据,但无法泛化到未见过的样本上。在上面的数据集中,我们可以建立一个最大深度(max_depth)非常高的模型,它在训练数据上会有出色的结果,但这种模型并不实用,因为它在真实世界的样本或实时数据上不会提供类似的结果。 206 | 207 | 有人可能会说,这种方法并没有过拟合,因为测试集的准确率基本保持不变。过拟合的另一个定义是,当我们不断提高训练损失时,测试损失也在增加。这种情况在神经网络中非常常见。 208 | 209 | 每当我们训练一个神经网络时,都必须在训练期间监控训练集和测试集的损失。如果我们有一个非常大的网络来处理一个非常小的数据集(即样本数非常少),我们就会观察到,随着我们不断训练,训练集和测试集的损失都会减少。但是,在某个时刻,测试损失会达到最小值,之后,即使训练损失进一步减少,测试损失也会开始增加。我们必须在验证损失达到最小值时停止训练。 210 | 211 | **这是对过拟合最常见的解释**。 212 | 213 | 奥卡姆剃刀用简单的话说,就是不要试图把可以用简单得多的方法解决的事情复杂化。换句话说,最简单的解决方案就是最具通用性的解决方案。一般来说,只要你的模型不符合奥卡姆剃刀原则,就很可能是过拟合。 214 | 215 | ![](figures/AAAMLP_page20_image.png) 216 | 217 |

图 3:过拟合的最一般定义

218 | 219 | 现在我们可以回到交叉检验。 220 | 221 | 在解释过拟合时,我决定将数据分为两部分。我在其中一部分上训练模型,然后在另一部分上检查其性能。这也是交叉检验的一种,通常被称为 "暂留集"(**hold-out set**)。当我们拥有大量数据,而模型推理是一个耗时的过程时,我们就会使用这种(交叉)验证。 222 | 223 | 交叉检验有许多不同的方法,它是建立一个良好的机器学习模型的最关键步骤。**选择正确的交叉检验**取决于所处理的数据集,在一个数据集上适用的交叉检验也可能不适用于其他数据集。不过,有几种类型的交叉检验技术最为流行和广泛使用。 224 | 225 | 其中包括: 226 | 227 | - k 折交叉检验 228 | - 分层 k 折交叉检验 229 | - 暂留交叉检验 230 | - 留一交叉检验 231 | - 分组 k 折交叉检验 232 | 233 | 交叉检验是将训练数据分层几个部分,我们在其中一部分上训练模型,然后在其余部分上进行测试。请看图 4。 234 | 235 | ![](figures/AAAMLP_page21_image.png) 236 | 237 |

图 4:将数据集拆分为训练集和验证集

238 | 239 | 图 4 和图 5 说明,当你得到一个数据集来构建机器学习模型时,你会把它们分成**两个不同的集:训练集和验证集**。很多人还会将其分成第三组,称之为测试集。不过,我们将只使用两个集。如你所见,我们将样本和与之相关的目标进行了划分。我们可以将数据分为 k 个互不关联的不同集合。这就是所谓的 **k 折交叉检验**。 240 | 241 | ![](figures/AAAMLP_page21_image_1.png) 242 | 243 |

图 5:K 折交叉检验

244 | 245 | 我们可以使用 scikit-learn 中的 KFold 将任何数据分割成 k 个相等的部分。每个样本分配一个从 0 到 k-1 的值。 246 | 247 | ```python 248 | # 导入 pandas 和 scikit-learn 的 model_selection 模块 249 | import pandas as pd 250 | from sklearn import model_selection 251 | 252 | if __name__ == "__main__": 253 | # 训练数据存储在名为 train.csv 的 CSV 文件中 254 | df = pd.read_csv("train.csv") 255 | 256 | # 我们创建一个名为 kfold 的新列,并用 -1 填充 257 | df["kfold"] = -1 258 | 259 | # 接下来的步骤是随机打乱数据的行 260 | df = df.sample(frac=1).reset_index(drop=True) 261 | 262 | # 从 model_selection 模块初始化 kfold 类 263 | kf = model_selection.KFold(n_splits=5) 264 | 265 | # 填充新的 kfold 列(enumerate的作用是返回一个迭代器) 266 | for fold, (trn_, val_) in enumerate(kf.split(X=df)): 267 | df.loc[val_, 'kfold'] = fold 268 | 269 | # 保存带有 kfold 列的新 CSV 文件 270 | df.to_csv("train_folds.csv", index=False) 271 | ``` 272 | 273 | 几乎所有类型的数据集都可以使用此流程。例如,当数据图像时,您可以创建一个包含图像 ID、图像位置和图像标签的 CSV,然后使用上述流程。 274 | 275 | 另一种重要的交叉检验类型是**分层 k 折交叉检验**。如果你有一个偏斜的二元分类数据集,其中正样本占 90%,负样本只占 10%,那么你就不应该使用随机 k 折交叉。对这样的数据集使用简单的 k 折交叉检验可能会导致折叠样本全部为负样本。在这种情况下,我们更倾向于使用分层 k 折交叉检验。分层 k 折交叉检验可以保持每个折中标签的比例不变。因此,在每个折叠中,都会有相同的 90% 正样本和 10% 负样本。因此,无论您选择什么指标进行评估,都会在所有折叠中得到相似的结果。 276 | 277 | 修改创建 k 折交叉检验的代码以创建分层 k 折交叉检验也很容易。我们只需将 model_selection.KFold 更改为 model_selection.StratifiedKFold ,并在 kf.split(...) 函数中指定要分层的目标列。我们假设 CSV 数据集有一列名为 "target" ,并且是一个分类问题。 278 | 279 | ```python 280 | # 导入 pandas 和 scikit-learn 的 model_selection 模块 281 | import pandas as pd 282 | from sklearn import model_selection 283 | 284 | if __name__ == "__main__": 285 | # 训练数据保存在名为 train.csv 的 CSV 文件中 286 | df = pd.read_csv("train.csv") 287 | 288 | # 添加一个新列 kfold,并用 -1 初始化 289 | df["kfold"] = -1 290 | 291 | # 随机打乱数据行 292 | df = df.sample(frac=1).reset_index(drop=True) 293 | 294 | # 获取目标变量 295 | y = df.target.values 296 | 297 | # 初始化 StratifiedKFold 类,设置折数(folds)为 5 298 | kf = model_selection.StratifiedKFold(n_splits=5) 299 | 300 | # 使用 StratifiedKFold 对象的 split 方法来获取训练和验证索引 301 | for f, (t_, v_) in enumerate(kf.split(X=df, y=y)): 302 | df.loc[v_, 'kfold'] = f 303 | 304 | # 保存包含 kfold 列的新 CSV 文件 305 | df.to_csv("train_folds.csv", index=False) 306 | 307 | ``` 308 | 309 | 对于葡萄酒数据集,我们来看看标签的分布情况。 310 | 311 | ```python 312 | b = sns.countplot(x='quality', data=df) 313 | b.set_xlabel("quality", fontsize=20) 314 | b.set_ylabel("count", fontsize=20) 315 | ``` 316 | 317 | 请注意,我们继续上面的代码。因此,我们已经转换了目标值。从图 6 中我们可以看出,质量偏差很大。有些类别有很多样本,有些则没有那么多。如果我们进行简单的 k 折交叉检验,那么每个折叠中的目标值分布都不会相同。因此,在这种情况下,我们选择分层 k 折交叉检验。 318 | 319 | ![](figures/AAAMLP_page24_image.png) 320 | 321 |

图 6:葡萄酒数据集中 "质量" 分布情况

322 | 323 | 规则很简单,如果是标准分类问题,就盲目选择分层 k 折交叉检验。 324 | 325 | 但如果数据量很大,该怎么办呢?假设我们有 100 万个样本。5 倍交叉检验意味着在 800k 个样本上进行训练,在 200k 个样本上进行验证。根据我们选择的算法,对于这样规模的数据集来说,训练甚至验证都可能非常昂贵。在这种情况下,我们可以选择**暂留交叉检验**。 326 | 327 | 创建保持结果的过程与分层 k 折交叉检验相同。对于拥有 100 万个样本的数据集,我们可以创建 10 个折叠而不是 5 个,并保留其中一个折叠作为保留样本。这意味着,我们将有 10 万个样本被保留下来,我们将始终在这个样本集上计算损失、准确率和其他指标,并在 90 万个样本上进行训练。 328 | 329 | 在处理时间序列数据时,暂留交叉检验也非常常用。假设我们要解决的问题是预测一家商店 2020 年的销售额,而我们得到的是 2015-2019 年的所有数据。在这种情况下,你可以选择 2019 年的所有数据作为保留数据,然后在 2015 年至 2018 年的所有数据上训练你的模型。 330 | 331 | ![](figures/AAAMLP_page25_image.png) 332 | 333 |

图 7:时间序列数据示例

334 | 335 | 在图 7 所示的示例中,假设我们的任务是预测从时间步骤 31 到 40 的销售额。我们可以保留 21 至 30 步的数据,然后从 0 步到 20 步训练模型。需要注意的是,在预测 31 步至 40 步时,应将 21 步至 30 步的数据纳入模型,否则,模型的性能将大打折扣。 336 | 337 | 在很多情况下,我们必须处理小型数据集,而创建大型验证集意味着模型学习会丢失大量数据。在这种情况下,我们可以选择留一交叉检验,相当于特殊的 k 则交叉检验其中 k=N ,N 是数据集中的样本数。这意味着在所有的训练折叠中,我们将对除 1 之外的所有数据样本进行训练。这种类型的交叉检验的折叠数与数据集中的样本数相同。 338 | 339 | 需要注意的是,如果模型的速度不够快,这种类型的交叉检验可能会耗费大量时间,但由于这种交叉检验只适用于小型数据集,因此并不重要。 340 | 341 | 现在我们可以转向回归问题了。回归问题的好处在于,除了分层 k 折交叉检验之外,我们可以在回归问题上使用上述所有交叉检验技术。也就是说,我们不能直接使用分层 k 折交叉检验,但有一些方法可以稍稍改变问题,从而在回归问题中使用分层 k 折交叉检验。大多数情况下,简单的 k 折交叉检验适用于任何回归问题。但是,如果发现目标分布不一致,就可以使用分层 k 折交叉检验。 342 | 343 | 要在回归问题中使用分层 k 折交叉检验,我们必须先将目标划分为若干个分层,然后再以处理分类问题的相同方式使用分层 k 折交叉检验。选择合适的分层数有几种选择。如果样本量很大(> 10k,> 100k),那么就不需要考虑分层的数量。只需将数据分为 10 或 20 层即可。如果样本数不多,则可以使用 Sturge's Rule 这样的简单规则来计算适当的分层数。 344 | 345 | Sturge's Rule: 346 | 347 | $$ 348 | Number of Bins = 1 + log_2(N) 349 | $$ 350 | 351 | 其中 $N$ 是数据集中的样本数。该函数如图 8 所示。 352 | 353 | ![](figures/AAAMLP_page26_image.png) 354 | 355 |

图 8:利用斯特格法则绘制样本与箱数对比图

356 | 357 | 让我们制作一个回归数据集样本,并尝试应用分层 k 折交叉检验,如下面的 python 代码段所示。 358 | 359 | ```python 360 | # stratified-kfold for regression 361 | # 为回归问题进行分层K-折交叉验证 362 | 363 | # 导入需要的库 364 | import numpy as np 365 | import pandas as pd 366 | from sklearn import datasets 367 | from sklearn import model_selection 368 | 369 | # 创建分折(folds)的函数 370 | def create_folds(data): 371 | # 创建一个新列叫做kfold,并用-1来填充 372 | data["kfold"] = -1 373 | 374 | # 随机打乱数据的行 375 | data = data.sample(frac=1).reset_index(drop=True) 376 | 377 | # 使用Sturge规则计算bin的数量 378 | num_bins = int(np.floor(1 + np.log2(len(data)))) 379 | 380 | # 使用pandas的cut函数进行目标变量(target)的分箱 381 | data.loc[:, "bins"] = pd.cut( 382 | data["target"], bins=num_bins, labels=False 383 | ) 384 | 385 | # 初始化StratifiedKFold类 386 | kf = model_selection.StratifiedKFold(n_splits=5) 387 | 388 | # 填充新的kfold列 389 | # 注意:我们使用的是bins而不是实际的目标变量(target)! 390 | for f, (t_, v_) in enumerate(kf.split(X=data, y=data.bins.values)): 391 | data.loc[v_, 'kfold'] = f 392 | 393 | # 删除bins列 394 | data = data.drop("bins", axis=1) 395 | 396 | # 返回包含folds的数据 397 | return data 398 | 399 | # 主程序开始 400 | if __name__ == "__main__": 401 | # 创建一个带有15000个样本、100个特征和1个目标变量的样本数据集 402 | X, y = datasets.make_regression( 403 | n_samples=15000, n_features=100, n_targets=1 404 | ) 405 | # 使用numpy数组创建一个数据框 406 | df = pd.DataFrame( 407 | X, 408 | columns=[f"f_{i}" for i in range(X.shape[1])] 409 | ) 410 | df.loc[:, "target"] = y 411 | 412 | # 创建folds 413 | df = create_folds(df) 414 | ``` 415 | 416 | 交叉检验是构建机器学习模型的第一步,也是最基本的一步。如果要做特征工程,首先要拆分数据。如果要建立模型,首先要拆分数据。如果你有一个好的交叉检验方案,其中验证数据能够代表训练数据和真实世界的数据,那么你就能建立一个具有高度通用性的好的机器学习模型。 417 | 418 | 本章介绍的交叉检验类型几乎适用于所有机器学习问题。不过,你必须记住,交叉检验也在很大程度上取决于数据,你可能需要根据你的问题和数据采用新的交叉检验形式。 419 | 420 | 例如,假设我们有一个问题,希望建立一个模型,从患者的皮肤图像中检测出皮肤癌。我们的任务是建立一个二元分类器,该分类器接收输入图像并预测其良性或恶性的概率。 421 | 422 | 在这类数据集中,训练数据集中可能有同一患者的多张图像。因此,要在这里建立一个良好的交叉检验系统,必须有分层的 k 折交叉检验,但也必须确保训练数据中的患者不会出现在验证数据中。幸运的是,scikit-learn 提供了一种称为 GroupKFold 的交叉检验类型。 在这里,患者可以被视为组。 但遗憾的是,scikit-learn 无法将 GroupKFold 与 StratifiedKFold 结合起来。所以你需要自己动手。我把它作为一个练习留给读者的练习。 423 | -------------------------------------------------------------------------------- /docs/准备环境.md: -------------------------------------------------------------------------------- 1 | # 准备环境 2 | 3 | 在我们开始编程之前,在你的机器上设置好一切是非常重要的。在本书中,我们将使用 Ubuntu 18.04 和 Python 3.7.6。如果你是 Windows 用户,可以通过多种方式安装 Ubuntu。例如,在虚拟机上安装由Oracle公司提供的免费软件 Virtual Box。与Windows一起作为双启动系统。我更喜欢双启动,因为它是原生的。如果你不是Ubuntu用户,在使用本书中的某些bash脚本时可能会遇到问题。为了避免这种情况,你可以在虚拟机中安装Ubuntu,或者在Windows上安装Linux shell。 4 | 5 | 用 Anaconda 在任何机器上安装 Python 都很简单。我特别喜欢 **Miniconda**,它是 conda 的最小安装程序。它适用于 Linux、OSX 和 Windows。由于 Python 2 支持已于 2019 年底结束,我们将使用 Python 3 发行版。需要注意的是,miniconda 并不像普通 Anaconda 附带所有软件包。因此,我们随时安装新软件包。安装 miniconda 非常简单。 6 | 7 | 首先要做的是将 **Miniconda3** 下载到系统中。 8 | 9 | ```shell 10 | cd ~/Downloads 11 | wget https://repo.anaconda.com/miniconda/... 12 | ``` 13 | 14 | 其中 wget 命令后的 URL 是 miniconda3 网页的 URL。对于 64 位 Linux 系统,编写本书时的 URL 是 15 | 16 | ```shell 17 | https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh 18 | ``` 19 | 20 | 下载 miniconda3 后,可以运行以下命令: 21 | 22 | ```shell 23 | sh Miniconda3-latest-Linux-x86_64.sh 24 | ``` 25 | 26 | 接下来,请阅读并按照屏幕上的说明操作。如果安装正确,你应该可以通过在终端输入 conda init 来启动 conda 环境。我们将创建一个在本书中一直使用的 conda 环境。要创建 conda 环境,可以输入: 27 | 28 | ```shell 29 | conda create -n environment_name python=3.7.6 30 | ``` 31 | 32 | 此命令将创建名为 environment_name 的 conda 环境,可以使用: 33 | 34 | ```shell 35 | conda activate environment_name 36 | ``` 37 | 38 | 现在我们的环境已经搭建完毕。是时候安装一些我们会用到的软件包了。在 conda 环境中,安装软件包有两种不同的方式。 你可以从 conda 仓库或 PyPi 官方仓库安装软件包。 39 | 40 | ```shell 41 | conda/pip install package_name 42 | ``` 43 | 44 | 注意:某些软件包可能无法在 conda 软件仓库中找到。因此,在本书中,使用 pip 安装是最可取的方法。我已经创建了一个编写本书时使用的软件包列表,保存在 environment.yml 中。 你可以在我的 GitHub 仓库中的额外资料中找到它。你可以使用以下命令创建环境: 45 | 46 | ```shell 47 | conda env create -f environment.yml 48 | ``` 49 | 50 | 该命令将创建一个名为 ml 的环境。要激活该环境并开始使用,应运行: 51 | 52 | ```shell 53 | conda activate ml 54 | ``` 55 | 56 | 现在我们已经准备就绪,可以进行一些应用机器学习的工作了!在使用本书进行编码时,请始终记住要在 "ml "环境下进行。现在,让我们开始学习真正的第一章。 57 | -------------------------------------------------------------------------------- /docs/可重复代码和模型方法.md: -------------------------------------------------------------------------------- 1 | # 可重复代码和模型方法 2 | 3 | 我们现在已经到了可以将模型/训练代码分发给他人使用的阶段。您可以用软盘分发或与他人共享代码,但这并不理想。是这样吗?也许很多年前,这是理想的做法,但现在不是了。 与他人共享代码和协作的首选方式是使用源代码管理系统。Git 是最流行的源代码管理系统之一。那么,假设你已经学会了 Git,并正确地格式化了代码,编写了适当的文档,还开源了你的项目。这就够了吗?不,还不够。因为你在自己的电脑上写的代码,在别人的电脑上可能会因为各种原因而无法运行。因此,如果您在发布代码时能复制自己的电脑,而其他人在安装您的软件或运行您的代码时也能复制您的电脑,那就再好不过了。为此,如今最流行的方法是使用 Docker 容器(Docker Containers)。要使用 Docker 容器,你需要安装 Docker。 4 | 5 | 让我们用下面的命令来安装 Docker。 6 | 7 | ```shell 8 | sudo apt install docker.io 9 | sudo systemctl start docker 10 | sudo systemctl enable docker 11 | sudo groupadd docker 12 | sudo usermod -aG docker $USER 13 | ``` 14 | 15 | 这些命令可以在 Ubuntu 18.04 上运行。Docker 最棒的地方在于它可以安装在任何机器上: Linux、Windows、OSX。因此,如果你一直在 Docker 容器中工作,哪台机器都没关系! 16 | 17 | Docker 容器可以被视为小型虚拟机。你可以为你的代码创建一个容器,然后每个人都可以使用和访问它。让我们看看如何创建可用于训练模型的容器。我们将使用在自然语言处理一章中训练的 BERT 模型,并尝试将训练代码容器化。 18 | 19 | 首先,你需要一个包含 python 项目需求的文件。需求包含在名为 requirements.txt 的文件中。文件名是 thestandard。该文件包含项目中使用的所有 python 库。也就是可以通过 PyPI (pip) 下载的 python 库。用于 训练 BERT 模型以检测正/负情感,我们使用了 torch、transformers、tqdm、scikit-learn、pandas 和 numpy。 让我们把它们写入 requirements.txt 中。你可以只写名称,也可以包括版本。包含版本总是最好的,这也是你应该做的。包含版本后,可以确保其他人使用的版本与你的版本相同,而不是最新版本,因为最新版本可能会更改某些内容,如果是这样的话,模型的训练方式就不会与你的相同了。 20 | 21 | 下面的代码段显示了 requirements.txt。 22 | 23 | ```python 24 | # requirements.txt 25 | pandas==1.0.4 26 | scikit-learn==0.22.1 27 | torch==1.5.0 28 | transformers==2.11.0 29 | ``` 30 | 31 | 现在,我们将创建一个名为 Dockerfile 的 Docker 文件。没有扩展名。Dockerfile 有几个元素。让我们来看看。 32 | 33 | ```dockerfile 34 | # Dockerfile 35 | # First of all, we include where we are getting the image 36 | # from. Image can be thought of as an operating system. 37 | # You can do "FROM ubuntu:18.04" 38 | # this will start from a clean ubuntu 18.04 image. 39 | # All images are downloaded from dockerhub 40 | # Here are we grabbing image from nvidia's repo 41 | # they created a docker image using ubuntu 18.04 42 | # and installed cuda 10.1 and cudnn7 in it. Thus, we don't have to 43 | # install it. Makes our life easy. 44 | FROM nvidia/cuda:10.1-cudnn7-runtime-ubuntu18.04 45 | # this is the same apt-get command that you are used to 46 | # except the fact that, we have -y argument. Its because 47 | # when we build this container, we cannot press Y when asked for 48 | RUN apt-get update && apt-get install -y \ 49 | git \ 50 | curl \ 51 | ca-certificates \ 52 | python3 \ 53 | python3-pip \ 54 | sudo \ 55 | && rm -rf /var/lib/apt/lists/* 56 | # We add a new user called "abhishek" 57 | # this can be anything. Anything you want it 58 | # to be. Usually, we don't use our own name, 59 | # you can use "user" or "ubuntu" 60 | RUN useradd -m abhishek 61 | # make our user own its own home directory 62 | RUN chown -R abhishek:abhishek /home/abhishek/ 63 | # copy all files from this direrctory to a 64 | # directory called app inside the home of abhishek 65 | # and abhishek owns it. 66 | COPY --chown=abhishek *.* /home/abhishek/app/ 67 | # change to user abhishek 68 | USER abhishek 69 | RUN mkdir /home/abhishek/data/ 70 | # Now we install all the requirements 71 | # after moving to the app directory 72 | # PLEASE NOTE that ubuntu 18.04 image 73 | # has python 3.6.9 and not python 3.7.6 74 | # you can also install conda python here and use that 75 | # however, to simplify it, I will be using python 3.6.9 76 | # inside the docker container!!!! 77 | RUN cd /home/abhishek/app/ && pip3 install -r requirements.txt 78 | # install mkl. its needed for transformers 79 | RUN pip3 install mkl 80 | # when we log into the docker container, 81 | # we will go inside this directory automatically 82 | WORKDIR /home/abhishek/app 83 | ``` 84 | 85 | 创建好 Docker 文件后,我们就需要构建它。构建 Docker 容器是一个非常简单的命令。 86 | 87 | ```dockerfile 88 | docker build -f Dockerfile -t bert:train . 89 | ``` 90 | 91 | 该命令根据提供的 Dockerfile 构建一个容器。Docker 容器的名称是 bert:train。输出结果如下: 92 | 93 | ``` 94 | ❯ docker build -f Dockerfile -t bert:train . 95 | Sending build context to Docker daemon 19.97kB 96 | Step 1/7 : FROM nvidia/cuda:10.1-cudnn7-ubuntu18.04 97 | ---> 3b55548ae91f 98 | Step 2/7 : RUN apt-get update && apt-get install -y git curl ca- 99 | certificates python3 python3-pip sudo && rm -rf 100 | /var/lib/apt/lists/* 101 | . 102 | . 103 | . 104 | . 105 | Removing intermediate container 8f6975dd08ba 106 | ---> d1802ac9f1b4 107 | Step 7/7 : WORKDIR /home/abhishek/app 108 | ---> Running in 257ff09502ed 109 | Removing intermediate container 257ff09502ed 110 | ---> e5f6eb4cddd7 111 | Successfully built e5f6eb4cddd7 112 | Successfully tagged bert:train 113 | ``` 114 | 115 | 请注意,我删除了输出中的许多行。现在,您可以使用以下命令登录容器。 116 | 117 | ```shell 118 | docker run -ti bert:train /bin/bash 119 | ``` 120 | 121 | 你需要记住,一旦退出 shell,你在 shell 中所做的一切都将丢失。你还可以在 Docker 容器中使用。 122 | 123 | ```shell 124 | docker run -ti bert:train python3 train.py 125 | ``` 126 | 127 | 输出情况: 128 | 129 | ``` 130 | Traceback (most recent call last): 131 | File "train.py", line 2, in 132 | import config 133 | File "/home/abhishek/app/config.py", line 28, in 134 | do_lower_case=True 135 | File "/usr/local/lib/python3.6/dist- 136 | packages/transformers/tokenization_utils.py", line 393, in 137 | from_pretrained 138 | return cls._from_pretrained(*inputs, **kwargs) 139 | File "/usr/local/lib/python3.6/dist- 140 | packages/transformers/tokenization_utils.py", line 496, in 141 | _from_pretrained 142 | list(cls.vocab_files_names.values()), 143 | OSError: Model name '../input/bert_base_uncased/' was not found in 144 | tokenizers model name list (bert-base-uncased, bert-large-uncased, bert- 145 | base-cased, bert-large-cased, bert-base-multilingual-uncased, bert-base- 146 | multilingual-cased, bert-base-chinese, bert-base-german-cased, bert- 147 | large-uncased-whole-word-masking, bert-large-cased-whole-word-masking, 148 | bert-large-uncased-whole-word-masking-finetuned-squad, bert-large-cased- 149 | whole-word-masking-finetuned-squad, bert-base-cased-finetuned-mrpc, bert- 150 | base-german-dbmdz-cased, bert-base-german-dbmdz-uncased, bert-base- 151 | finnish-cased-v1, bert-base-finnish-uncased-v1, bert-base-dutch-cased). 152 | We assumed '../input/bert_base_uncased/' was a path, a model identifier, 153 | or url to a directory containing vocabulary files named ['vocab.txt'] but 154 | couldn't find such vocabulary files at this path or url. 155 | ``` 156 | 157 | 哎呀,出错了! 158 | 159 | 我为什么要把错误印在书上呢? 160 | 161 | 因为理解这个错误非常重要。这个错误说明代码无法找到目录".../input/bert_base_cased"。为什么会出现这种情况呢?我们可以在没有 Docker 的情况下进行训练,我们可以看到目录和所有文件都存在。出现这种情况是因为 Docker 就像一个虚拟机!它有自己的文件系统,本地机器上的文件不会共享给 Docker 容器。如果你想使用本地机器上的路径并对其进行修改,你需要在运行 Docker 时将其挂载到 Docker 容器上。当我们查看这个文件夹的路径时,我们知道它位于名为 input 的文件夹的上一级。让我们稍微修改一下 config.py 文件! 162 | 163 | ```python 164 | # config.py 165 | import os 166 | import transformers 167 | # fetch home directory 168 | # in our docker container, it is 169 | # /home/abhishek 170 | HOME_DIR = os.path.expanduser("~") 171 | # this is the maximum number of tokens in the sentence 172 | MAX_LEN = 512 173 | # batch sizes is low because model is huge! 174 | TRAIN_BATCH_SIZE = 8 175 | VALID_BATCH_SIZE = 4 176 | # let's train for a maximum of 10 epochs 177 | EPOCHS = 10 178 | # define path to BERT model files 179 | # Now we assume that all the data is stored inside 180 | # /home/abhishek/data 181 | BERT_PATH = os.path.join(HOME_DIR, "data", "bert_base_uncased") 182 | # this is where you want to save the model 183 | MODEL_PATH = os.path.join(HOME_DIR, "data", "model.bin") 184 | # training file 185 | TRAINING_FILE = os.path.join(HOME_DIR, "data", "imdb.csv") 186 | TOKENIZER = transformers.BertTokenizer.from_pretrained(BERT_PATH,do_lower_case=True ) 187 | ``` 188 | 189 | 现在,代码假定所有内容都在主目录下名为 data 的文件夹中。 190 | 191 | 请注意,如果 Python 脚本有任何改动,都意味着需要重建 Docker 容器!因此,我们重建容器,然后重新运行 Docker 命令,但这次要有所改变。不过,如果我们没有英伟达™(NVIDIA®)Docker 运行时,这也是行不通的。别担心,这只是一个 Docker 容器。你只需要做一次。要安装英伟达™(NVIDIA®)Docker 运行时,可以在 Ubuntu 18.04 中运行以下命令。 192 | 193 | ```shell 194 | distribution=$(. /etc/os-release;echo $ID$VERSION_ID) 195 | curl -s -L https://nvidia.github.io/nvidia-docker/gpgkey | sudo apt-key 196 | add - 197 | curl -s -L https://nvidia.github.io/nvidia-docker/$distribution/nvidia- 198 | docker.list | sudo tee /etc/apt/sources.list.d/nvidia-docker.list 199 | sudo apt-get update && sudo apt-get install -y nvidia-container-toolkit 200 | sudo systemctl restart docker 201 | ``` 202 | 203 | 现在,我们可以再次构建我们的容器,并开始训练过程: 204 | 205 | ```shell 206 | docker run --gpus 1 -v 207 | /home/abhishek/workspace/approaching_almost/input/:/home/abhishek/data/ - 208 | ti bert:train python3 train.py 209 | ``` 210 | 211 | 其中,-gpus 1 表示我们在 docker 容器中使用 1 个 GPU,-v 表示挂载卷。 因此,我们要将本地目录 /home/abhishek/workspace/approaching_almost/input/ 挂载到 docker 容器中的 /home/abhishek/data/。这一步要花点时间,但完成后,本地文件夹中就会有 model.bin。 212 | 213 | 这样,只需做一些简单的改动,你的训练代码就已经 "dockerized "了。现在,你可以在(几乎)任何你想要的系统上使用这些代码进行训练。 214 | 215 | 下一部分是将我们训练好的模型 "提供 "给最终用户。假设您想从接收到的推文流中提取情感信息。要完成这项任务,您必须创建一个 API,用于输入句子,然后返回带有情感概率的输出。使用 Python 构建 API 的最常见方法是使用 **Flask**,它是一个微型网络服务框架。 216 | 217 | ```python 218 | # api.py 219 | import config 220 | import flask 221 | 222 | import time 223 | import torch 224 | import torch.nn as nn 225 | from flask import Flask 226 | from flask import request 227 | from model import BERTBaseUncased 228 | 229 | app = Flask(__name__) 230 | 231 | MODEL = None 232 | 233 | DEVICE = "cuda" 234 | def sentence_prediction(sentence): 235 | tokenizer = config.TOKENIZER 236 | max_len = config.MAX_LEN 237 | 238 | review = str(sentence) 239 | review = " ".join(review.split()) 240 | 241 | inputs = tokenizer.encode_plus( 242 | review, 243 | None, 244 | add_special_tokens=True, 245 | max_length=max_len 246 | ) 247 | 248 | ids = inputs["input_ids"] 249 | mask = inputs["attention_mask"] 250 | token_type_ids = inputs["token_type_ids"] 251 | 252 | padding_length = max_len - len(ids) 253 | ids = ids + ([0] * padding_length) 254 | mask = mask + ([0] * padding_length) 255 | token_type_ids = token_type_ids + ([0] * padding_length) 256 | 257 | ids = torch.tensor(ids, dtype=torch.long).unsqueeze(0) 258 | mask = torch.tensor(mask, dtype=torch.long).unsqueeze(0) 259 | token_type_ids = torch.tensor(token_type_ids, 260 | dtype=torch.long).unsqueeze(0) 261 | 262 | ids = ids.to(DEVICE, dtype=torch.long) 263 | token_type_ids = token_type_ids.to(DEVICE, dtype=torch.long) 264 | mask = mask.to(DEVICE, dtype=torch.long) 265 | 266 | outputs = MODEL(ids=ids, mask=mask, token_type_ids=token_type_ids) 267 | outputs = torch.sigmoid(outputs).cpu().detach().numpy() 268 | return outputs[0][0] 269 | 270 | @app.route("/predict", methods=["GET"]) 271 | def predict(): 272 | sentence = request.args.get("sentence") 273 | start_time = time.time() 274 | positive_prediction = sentence_prediction(sentence) 275 | negative_prediction = 1 - positive_prediction 276 | response = {} 277 | response["response"] = { 278 | "positive": str(positive_prediction), 279 | "negative": str(negative_prediction), 280 | "sentence": str(sentence), 281 | "time_taken": str(time.time() - start_time), 282 | } 283 | return flask.jsonify(response) 284 | 285 | if __name__ == "__main__": 286 | MODEL = BERTBaseUncased() 287 | MODEL.load_state_dict(torch.load( 288 | config.MODEL_PATH, map_location=torch.device(DEVICE) 289 | )) 290 | MODEL.to(DEVICE) 291 | MODEL.eval() 292 | app.run(host="0.0.0.0") 293 | ``` 294 | 295 | 然后运行 "python api.py "命令启动 API。API 将在端口 5000 的 localhost 上启动。cURL 请求及其响应示例如下。 296 | 297 | ``` 298 | ❯ curl 299 | $'http://192.168.86.48:5000/predict?sentence=this%20is%20the%20best%20boo 300 | k%20ever' 301 | {"response":{"negative":"0.0032927393913269043","positive":"0.99670726"," 302 | sentence":"this is the best book 303 | ever","time_taken":"0.029126882553100586"}} 304 | ``` 305 | 306 | 可以看到,我们得到的输入句子的正面情感概率很高。输入句子的正面情感概率很高。 您还可以访问 http://127.0.0.1:5000/predict?sentence=this%20book%20is%20too%20complicated%20for%20me。这将再次返回一个 JSON 文件。 307 | 308 | ```python 309 | { 310 | response: { 311 | negative: "0.8646619468927383", 312 | positive: "0.13533805", 313 | sentence: "this book is too complicated for me", 314 | time_taken: "0.03852701187133789" 315 | } 316 | } 317 | ``` 318 | 319 | 现在,我们创建了一个简单的应用程序接口,可以用来为少量用户提供服务。为什么是少量?因为这个 API 一次只服务一个请求。gunicorn 是 UNIX 上的 Python WSGI HTTP 服务器,让我们使用它的 CPU 来处理多个并行请求。Gunicorn 可以为 API 创建多个进程,因此我们可以同时为多个客户提供服务。您可以使用 "pip install gunicorn "安装 gunicorn。 320 | 321 | 为了将代码转换为与 gunicorn 兼容,我们需要移除 init main,并将其中的所有内容移至全局范围。此外,我们现在使用的是 CPU 而不是 GPU。修改后的代码如下。 322 | 323 | ```python 324 | # api.py 325 | import config 326 | import flask 327 | import time 328 | import torch 329 | import torch.nn as nn 330 | from flask import Flask 331 | from flask import request 332 | from model import BERTBaseUncased 333 | app = Flask(__name__) 334 | DEVICE = "cpu" 335 | MODEL = BERTBaseUncased() 336 | MODEL.load_state_dict(torch.load(config.MODEL_PATH, map_location=torch.device(DEVICE))) 337 | MODEL.to(DEVICE) 338 | MODEL.eval() 339 | def sentence_prediction(sentence): 340 | 341 | return outputs[0][0] 342 | 343 | 344 | @app.route("/predict", methods=["GET"]) 345 | def predict(): 346 | 347 | return flask.jsonify(response) 348 | ``` 349 | 350 | 我们使用以下命令运行这个应用程序接口。 351 | 352 | ```shell 353 | gunicorn api:app --bind 0.0.0.0:5000 --workers 4 354 | ``` 355 | 356 | 这意味着我们在提供的 IP 地址和端口上使用 4 个 Worker 运行我们的 flask 应用程序。由于有 4 个 Worker,我们现在可以同时处理 4 个请求。请注意,现在我们的终端使用的是 CPU,因此不需要 GPU 机器,可以在任何标准服务器/虚拟机上运行。不过,我们还有一个问题:我们已经在本地机器上完成了所有工作,因此必须将其坞化。看看下面这个未注释的 Dockerfile,它可以用来部署这个应用程序接口。请注意用于培训的旧 Dockerfile 和这个 Dockerfile 之间的区别。区别不大。 357 | 358 | ```dockerfile 359 | # CPU Dockerfile 360 | FROM ubuntu:18.04 361 | RUN apt-get update && apt-get install -y \ 362 | git \ 363 | curl \ 364 | ca-certificates \ 365 | python3 \ 366 | python3-pip \ 367 | sudo \ 368 | && rm -rf /var/lib/apt/lists/* 369 | RUN useradd -m abhishek 370 | RUN chown -R abhishek:abhishek /home/abhishek/ 371 | COPY --chown=abhishek *.* /home/abhishek/app/ 372 | USER abhishek 373 | RUN mkdir /home/abhishek/data/ 374 | RUN cd /home/abhishek/app/ && pip3 install -r requirements.txt 375 | RUN pip3 install mkl 376 | WORKDIR /home/abhishek/app 377 | ``` 378 | 379 | 让我们构建一个新的 Docker 容器。 380 | 381 | ```shell 382 | docker build -f Dockerfile -t bert:api 383 | ``` 384 | 385 | 当 Docker 容器构建完成后,我们就可以使用以下命令直接运行 API 了。 386 | 387 | ```shell 388 | docker run -p 5000:5000 -v 389 | /home/abhishek/workspace/approaching_almost/input/:/home/abhishek/data/ - 390 | ti bert:api /home/abhishek/.local/bin/gunicorn api:app --bind 391 | 0.0.0.0:5000 --workers 4 392 | ``` 393 | 394 | 请注意,我们将容器内的 5000 端口暴露给容器外的 5000 端口。如果使用 docker-compose,也可以很好地做到这一点。Dockercompose 是一个可以让你同时在不同或相同容器中运行不同服务的工具。你可以使用 "pip install docker-compose "安装 docker-compose,然后在构建容器后运行 "docker-compose up"。要使用 docker-compose,你需要一个 docker-compose.yml 文件。 395 | 396 | ```yaml 397 | # docker-compose.yml 398 | # specify a version of the compose 399 | version: '3.7' 400 | # you can add multiple services 401 | services: 402 | # specify service name. we call our service: api 403 | api: 404 | # specify image name 405 | image: bert:api 406 | # the command that you would like to run inside the container 407 | command: /home/abhishek/.local/bin/gunicorn api:app --bind 408 | 0.0.0.0:5000 --workers 4 409 | # mount the volume 410 | volumes: 411 | - 412 | /home/abhishek/workspace/approaching_almost/input/:/home/abhishek/data/ 413 | # this ensures that our ports from container will be 414 | # exposed as it is 415 | network_mode: host 416 | ``` 417 | 418 | 现在,您只需使用上述命令即可重新运行 API,其运行方式与之前相同。恭喜你,现在,你也已经成功地将预测 API 进行了 Docker 化,可以随时随地部署了。在本章中,我们学习了 Docker、使用 flask 构建 API、使用 gunicorn 和 Docker 服务 API 以及 docker-compose。关于 docker 的知识远不止这些,但这应该是一个开始。其他内容可以在学习过程中逐渐掌握。 我们还跳过了许多工具,如 kubernetes、bean-stalk、sagemaker、heroku 和许多其他工具,这些工具如今被人们用来在生产中部署模型。"我要写什么?点击修改图 X 中的 docker 容器"?在书中描述这些是不可行的,也是不可取的,所以我将使用不同的媒介来赞美本书的这一部分。请记住,一旦你对应用程序进行了 Docker 化,使用这些技术/平台进行部署就变得易如反掌了。请务必记住,要让你的代码和模型对他人可用,并做好文档记录,这样任何人都可以使用你开发的东西,而无需多次询问你。这不仅能节省您的时间,还能节省他人的时间。好的、开源的、可重复使用的代码在您的作品集中也非常重要。 -------------------------------------------------------------------------------- /docs/文本分类或回归方法.md: -------------------------------------------------------------------------------- 1 | # 文本分类或回归方法 2 | 3 | 文本问题是我的最爱。一般来说,这些问题也被称为**自然语言处理(NLP)问题**。NLP 问题与图像问题也有很大不同。你需要创建以前从未为表格问题创建过的数据管道。你需要了解商业案例,才能建立一个好的模型。顺便说一句,机器学习中的任何事情都是如此。建立模型会让你达到一定的水平,但要想改善和促进你所建立模型的业务,你必须了解它对业务的影响。 4 | 5 | NLP 问题有很多种,其中最常见的是字符串分类。很多时候,我们会看到人们在处理表格数据或图像时表现出色,但在处理文本时,他们甚至不知道从何入手。文本数据与其他类型的数据集没有什么不同。对于计算机来说,一切都是数字。 6 | 7 | 假设我们从情感分类这一基本任务开始。我们将尝试对电影评论进行情感分类。因此,您有一个文本,并有与之相关的情感。你将如何处理这类问题?是应用深度神经网络? 不,绝对错了。你要从最基本的开始。让我们先看看这些数据是什么样子的。 8 | 9 | 我们从**IMDB 电影评论数据集**开始,该数据集包含 25000 篇正面情感评论和 25000 篇负面情感评论。 10 | 11 | 我将在此讨论的概念几乎适用于任何文本分类数据集。 12 | 13 | 这个数据集非常容易理解。一篇评论对应一个目标变量。请注意,我写的是评论而不是句子。评论就是一堆句子。所以,到目前为止,你一定只看到了对单句的分类,但在这个问题中,我们将对多个句子进行分类。简单地说,这意味着不仅一个句子会对情感产生影响,而且情感得分是多个句子得分的组合。数据简介如图 1 所示。 14 | 15 | ![](figures/AAAMLP_page225_image.png) 16 | 17 | 如何着手解决这样的问题?一个简单的方法就是手工制作两份单词表。一个列表包含你能想象到的所有正面词汇,例如好、棒、好等;另一个列表包含所有负面词汇,例如坏、恶等。我们先不要举例说明坏词,否则这本书就只能供 18 岁以上的人阅读了。一旦你有了这些列表,你甚至不需要一个模型来进行预测。这些列表也被称为情感词典。你可以用一个简单的计数器来计算句子中正面和负面词语的数量。如果正面词语的数量较多,则表示该句子具有正面情感;如果负面词语的数量较多,则表示该句子具有负面情感。如果句子中没有这些词,则可以说该句子具有中性情感。这是最古老的方法之一,现在仍有人在使用。它也不需要太多代码。 18 | 19 | ```python 20 | def find_sentiment(sentence, pos, neg): 21 | sentence = sentence.split() 22 | sentence = set(sentence) 23 | num_common_pos = len(sentence.intersection(pos)) 24 | num_common_neg = len(sentence.intersection(neg)) 25 | if num_common_pos > num_common_neg: 26 | return "positive" 27 | if num_common_pos < num_common_neg: 28 | return "negative" 29 | return "neutral" 30 | ``` 31 | 32 | 不过,这种方法考虑的因素并不多。正如你所看到的,我们的 split() 也并不完美。如果使用 split(),就会出现这样的句子: 33 | 34 | "hi, how are you?" 35 | 36 | 经过分割后变为: 37 | 38 | ["hi,", "how","are","you?"] 39 | 40 | 这种方法并不理想,因为单词中包含了逗号和问号,它们并没有被分割。因此,如果没有在分割前对这些特殊字符进行预处理,不建议使用这种方法。将字符串拆分为单词列表称为标记化。最流行的标记化方法之一来自 **NLTK(自然语言工具包)**。 41 | 42 | ```python 43 | In [X]: from nltk.tokenize import word_tokenize 44 | In [X]: sentence = "hi, how are you?" 45 | In [X]: sentence.split() 46 | Out[X]: ['hi,', 'how', 'are', 'you?'] 47 | In [X]: word_tokenize(sentence) 48 | Out[X]: ['hi', ',', 'how', 'are', 'you', '?'] 49 | ``` 50 | 51 | 正如您所看到的,使用 NLTK 的单词标记化功能,同一个句子的拆分效果要好得多。使用单词列表进行对比的效果也会更好!这就是我们将应用于第一个情感检测模型的方法。 52 | 53 | 在处理 NLP 分类问题时,您应该经常尝试的基本模型之一是**词袋模型(bag of words)**。在词袋模型中,我们创建一个巨大的稀疏矩阵,存储语料库(语料库=所有文档=所有句子)中所有单词的计数。为此,我们将使用 scikit-learn 中的 CountVectorizer。让我们看看它是如何工作的。 54 | 55 | ```python 56 | from sklearn.feature_extraction.text import CountVectorizer 57 | 58 | corpus = [ 59 | "hello, how are you?", 60 | "im getting bored at home. And you? What do you think?", 61 | "did you know about counts", 62 | "let's see if this works!", 63 | "YES!!!!" 64 | ] 65 | ctv = CountVectorizer() 66 | ctv.fit(corpus) 67 | corpus_transformed = ctv.transform(corpus) 68 | ``` 69 | 70 | 如果我们打印 corpus_transformed,就会得到类似下面的结果: 71 | 72 | ```python 73 | (0, 2) 1 74 | (0, 9) 1 75 | (0, 11) 1 76 | (0, 22) 1 77 | (1, 1) 1 78 | (1, 3) 1 79 | (1, 4) 1 80 | (1, 7) 1 81 | (1, 8) 1 82 | (1, 10) 1 83 | (1, 13) 1 84 | (1, 17) 1 85 | (1, 19) 1 86 | (1, 22) 2 87 | (2, 0) 1 88 | (2, 5) 1 89 | (2, 6) 1 90 | (2, 14) 1 91 | (2, 22) 1 92 | (3, 12) 1 93 | (3, 15) 1 94 | (3, 16) 1 95 | (3, 18) 1 96 | (3, 20) 1 97 | (4, 21) 1 98 | ``` 99 | 100 | 在前面的章节中,我们已经见识过这种表示法。即稀疏表示法。因此,语料库现在是一个稀疏矩阵,其中第一个样本有 4 个元素,第二个样本有 10 个元素,以此类推,第三个样本有 5 个元素,以此类推。我们还可以看到,这些元素都有相关的计数。有些元素会出现两次,有些则只有一次。例如,在样本 2(第 1 行)中,我们看到第 22 列的数值是 2。这是为什么呢?第 22 列是什么? 101 | 102 | CountVectorizer 的工作方式是首先对句子进行标记化处理,然后为每个标记赋值。因此,每个标记都由一个唯一索引表示。这些唯一索引就是我们看到的列。CountVectorizer 会存储这些信息。 103 | 104 | ```python 105 | print(ctv.vocabulary_) 106 | {'hello': 9, 'how': 11, 'are': 2, 'you': 22, 'im': 13, 'getting': 8, 107 | 'bored': 4, 'at': 3, 'home': 10, 'and': 1, 'what': 19, 'do': 7, 'think': 108 | 17, 'did': 6, 'know': 14, 'about': 0, 'counts': 5, 'let': 15, 'see': 16, 109 | 'if': 12, 'this': 18, 'works': 20, 'yes': 21} 110 | ``` 111 | 112 | 我们看到,索引 22 属于 "you",而在第二句中,我们使用了两次 "you"。我希望大家现在已经清楚什么是词袋了。但是我们还缺少一些特殊字符。有时,这些特殊字符也很有用。例如,"? "在大多数句子中表示疑问句。让我们把 scikit-learn 的 word_tokenize 整合到 CountVectorizer 中,看看会发生什么。 113 | 114 | ```python 115 | from sklearn.feature_extraction.text import CountVectorizer 116 | from nltk.tokenize import word_tokenize 117 | 118 | corpus = [ 119 | "hello, how are you?", 120 | "im getting bored at home. And you? What do you think?", 121 | "did you know about counts", 122 | "let's see if this works!", 123 | "YES!!!!" 124 | ] 125 | ctv = CountVectorizer(tokenizer=word_tokenize, token_pattern=None) 126 | ctv.fit(corpus) 127 | corpus_transformed = ctv.transform(corpus) 128 | print(ctv.vocabulary_) 129 | ``` 130 | 131 | 这样,我们的词袋就变成了: 132 | 133 | ```python 134 | {'hello': 14, ',': 2, 'how': 16, 'are': 7, 'you': 27, '?': 4, 'im': 18, 135 | 'getting': 13, 'bored': 9, 'at': 8, 'home': 15, '.': 3, 'and': 6, 'what': 136 | 24, 'do': 12, 'think': 22, 'did': 11, 'know': 19, 'about': 5, 'counts': 137 | 10, 'let': 20, "'s": 1, 'see': 21, 'if': 17, 'this': 23, 'works': 25, 138 | '!': 0, 'yes': 26} 139 | ``` 140 | 141 | 我们现在可以利用 IMDB 数据集中的所有句子创建一个稀疏矩阵,并建立一个模型。该数据集中正负样本的比例为 1:1,因此我们可以使用准确率作为衡量标准。我们将使用 StratifiedKFold 并创建一个脚本来训练5个折叠。你会问使用哪个模型?对于高维稀疏数据,哪个模型最快?逻辑回归。我们将首先使用逻辑回归来处理这个数据集,并创建第一个基准模型。 142 | 143 | 让我们看看如何做到这一点。 144 | 145 | ```python 146 | import pandas as pd 147 | from nltk.tokenize import word_tokenize 148 | from sklearn import linear_model 149 | from sklearn import metrics 150 | from sklearn import model_selection 151 | from sklearn.feature_extraction.text import CountVectorizer 152 | if __name__ == "__main__": 153 | df = pd.read_csv("../input/imdb.csv") 154 | df.sentiment = df.sentiment.apply( 155 | lambda x: 1 if x == "positive" else 0 156 | ) 157 | df["kfold"] = -1 158 | df = df.sample(frac=1).reset_index(drop=True) 159 | y = df.sentiment.values 160 | kf = model_selection.StratifiedKFold(n_splits=5) 161 | for f, (t_, v_) in enumerate(kf.split(X=df, y=y)): 162 | df.loc[v_, 'kfold'] = f 163 | 164 | for fold_ in range(5): 165 | train_df = df[df.kfold != fold_].reset_index(drop=True) 166 | test_df = df[df.kfold == fold_].reset_index(drop=True) 167 | count_vec = CountVectorizer( 168 | tokenizer=word_tokenize, 169 | token_pattern=None 170 | ) 171 | count_vec.fit(train_df.review) 172 | xtrain = count_vec.transform(train_df.review) 173 | xtest = count_vec.transform(test_df.review) 174 | model = linear_model.LogisticRegression() 175 | model.fit(xtrain, train_df.sentiment) 176 | preds = model.predict(xtest) 177 | accuracy = metrics.accuracy_score(test_df.sentiment, preds) 178 | print(f"Fold: {fold_}") 179 | print(f"Accuracy = {accuracy}") 180 | print("") 181 | ``` 182 | 183 | 这段代码的运行需要一定的时间,但可以得到以下输出结果: 184 | 185 | ```python 186 | Fold: 0 187 | Accuracy = 0.8903 188 | 189 | Fold: 1 190 | Accuracy = 0.897 191 | 192 | Fold: 2 193 | Accuracy = 0.891 194 | 195 | Fold: 3 196 | Accuracy = 0.8914 197 | 198 | Fold: 4 199 | Accuracy = 0.8931 200 | ``` 201 | 202 | 哇,准确率已经达到 89%,而我们所做的只是使用词袋和逻辑回归!这真是太棒了!不过,这个模型的训练花费了很多时间,让我们看看能否通过使用朴素贝叶斯分类器来缩短训练时间。朴素贝叶斯分类器在 NLP 任务中相当流行,因为稀疏矩阵非常庞大,而朴素贝叶斯是一个简单的模型。要使用这个模型,需要更改一个导入和模型的行。让我们看看这个模型的性能如何。我们将使用 scikit-learn 中的 MultinomialNB。 203 | 204 | ```python 205 | import pandas as pd 206 | from nltk.tokenize import word_tokenize 207 | from sklearn import naive_bayes 208 | from sklearn import metrics 209 | from sklearn import model_selection 210 | from sklearn.feature_extraction.text import CountVectorizer 211 | 212 | 213 | model = naive_bayes.MultinomialNB() 214 | model.fit(xtrain, train_df.sentiment) 215 | ``` 216 | 217 | 得到如下结果: 218 | 219 | ```python 220 | Fold: 0 221 | Accuracy = 0.8444 222 | 223 | Fold: 1 224 | Accuracy = 0.8499 225 | 226 | Fold: 2 227 | Accuracy = 0.8422 228 | 229 | Fold: 3 230 | Accuracy = 0.8443 231 | 232 | Fold: 4 233 | Accuracy = 0.8455 234 | ``` 235 | 236 | 我们看到这个分数很低。但朴素贝叶斯模型的速度非常快。 237 | 238 | NLP 中的另一种方法是 TF-IDF,如今大多数人都倾向于忽略或不屑于了解这种方法。TF 是术语频率,IDF 是反向文档频率。从这些术语来看,这似乎有些困难,但通过 TF 和 IDF 的计算公式,事情就会变得很明显。 239 | $$ 240 | TF(t) = \frac{Number\ of\ times\ a\ term\ t\ appears\ in\ a\ document}{Total\ number\ of\ terms\ in \ the\ document} 241 | $$ 242 | 243 | $$ 244 | IDF(t) = LOG\left(\frac{Total\ number\ of\ documents}{Number\ of\ documents with\ term\ t\ in\ it}\right) 245 | $$ 246 | 247 | 术语 t 的 TF-IDF 定义为: 248 | $$ 249 | TF-IDF(t) = TF(t) \times IDF(t) 250 | $$ 251 | 与 scikit-learn 中的 CountVectorizer 类似,我们也有 TfidfVectorizer。让我们试着像使用 CountVectorizer 一样使用它。 252 | 253 | ```python 254 | from sklearn.feature_extraction.text import TfidfVectorizer 255 | from nltk.tokenize import word_tokenize 256 | 257 | corpus = [ 258 | "hello, how are you?", 259 | "im getting bored at home. And you? What do you think?", 260 | "did you know about counts", 261 | "let's see if this works!", 262 | "YES!!!!" 263 | ] 264 | tfv = TfidfVectorizer(tokenizer=word_tokenize, token_pattern=None) 265 | tfv.fit(corpus) 266 | corpus_transformed = tfv.transform(corpus) 267 | print(corpus_transformed) 268 | ``` 269 | 270 | 输出结果如下: 271 | 272 | ```python 273 | (0, 27) 0.2965698850220162 274 | (0, 16) 0.4428321995085722 275 | (0, 14) 0.4428321995085722 276 | (0, 7) 0.4428321995085722 277 | (0, 4) 0.35727423026525224 278 | (0, 2) 0.4428321995085722 279 | (1, 27) 0.35299699146792735 280 | (1, 24) 0.2635440111190765 281 | (1, 22) 0.2635440111190765 282 | (1, 18) 0.2635440111190765 283 | (1, 15) 0.2635440111190765 284 | (1, 13) 0.2635440111190765 285 | (1, 12) 0.2635440111190765 286 | (1, 9) 0.2635440111190765 287 | (1, 8) 0.2635440111190765 288 | (1, 6) 0.2635440111190765 289 | (1, 4) 0.42525129752567803 290 | (1, 3) 0.2635440111190765 291 | (2, 27) 0.31752680284846835 292 | (2, 19) 0.4741246485558491 293 | (2, 11) 0.4741246485558491 294 | (2, 10) 0.4741246485558491 295 | (2, 5) 0.4741246485558491 296 | (3, 25) 0.38775666010579296 297 | (3, 23) 0.38775666010579296 298 | (3, 21) 0.38775666010579296 299 | (3, 20) 0.38775666010579296 300 | (3, 17) 0.38775666010579296 301 | (3, 1) 0.38775666010579296 302 | (3, 0) 0.3128396318588854 303 | (4, 26) 0.2959842226518677 304 | (4, 0) 0.9551928286692534 305 | ``` 306 | 307 | 可以看到,这次我们得到的不是整数值,而是浮点数。 用 TfidfVectorizer 代替 CountVectorizer 也是小菜一碟。Scikit-learn 还提供了 TfidfTransformer。如果你使用的是计数值,可以使用 TfidfTransformer 并获得与 TfidfVectorizer 相同的效果。 308 | 309 | ```python 310 | import pandas as pd 311 | from nltk.tokenize import word_tokenize 312 | from sklearn import linear_model 313 | from sklearn import metrics 314 | from sklearn import model_selection 315 | from sklearn.feature_extraction.text import TfidfVectorizer 316 | 317 | for fold_ in range(5): 318 | train_df = df[df.kfold != fold_].reset_index(drop=True) 319 | test_df = df[df.kfold == fold_].reset_index(drop=True) 320 | tfidf_vec = TfidfVectorizer( 321 | tokenizer=word_tokenize, 322 | token_pattern=None 323 | ) 324 | tfidf_vec.fit(train_df.review) 325 | xtrain = tfidf_vec.transform(train_df.review) 326 | xtest = tfidf_vec.transform(test_df.review) 327 | model = linear_model.LogisticRegression() 328 | model.fit(xtrain, train_df.sentiment) 329 | preds = model.predict(xtest) 330 | accuracy = metrics.accuracy_score(test_df.sentiment, preds) 331 | print(f"Fold: {fold_}") 332 | print(f"Accuracy = {accuracy}") 333 | print("") 334 | ``` 335 | 336 | 我们可以看看 TF-IDF 在逻辑回归模型上的表现如何。 337 | 338 | ```python 339 | Fold: 0 340 | Accuracy = 0.8976 341 | 342 | Fold: 1 343 | Accuracy = 0.8998 344 | 345 | Fold: 2 346 | Accuracy = 0.8948 347 | 348 | Fold: 3 349 | Accuracy = 0.8912 350 | 351 | Fold: 4 352 | Accuracy = 0.8995 353 | ``` 354 | 355 | 我们看到,这些分数都比 CountVectorizer 高一些,因此它成为了我们想要击败的新基准。 356 | 357 | NLP 中另一个有趣的概念是 N-gram。N-grams 是按顺序排列的单词组合。N-grams 很容易创建。您只需注意顺序即可。为了让事情变得更简单,我们可以使用 NLTK 的 N-gram 实现。 358 | 359 | ```python 360 | from nltk import ngrams 361 | from nltk.tokenize import word_tokenize 362 | 363 | N = 3 364 | sentence = "hi, how are you?" 365 | tokenized_sentence = word_tokenize(sentence) 366 | n_grams = list(ngrams(tokenized_sentence, N)) 367 | print(n_grams) 368 | ``` 369 | 370 | 由此得到: 371 | 372 | ```python 373 | [('hi', ',', 'how'), 374 | (',', 'how', 'are'), 375 | ('how', 'are', 'you'), 376 | ('are', 'you', '?')] 377 | ``` 378 | 379 | 同样,我们还可以创建 2-gram 或 4-gram 等。现在,这些 n-gram 将成为我们词汇表的一部分,当我们计算计数或 tf-idf 时,我们会将一个 n-gram 视为一个全新的标记。因此,在某种程度上,我们是在结合上下文。scikit-learn 的 CountVectorizer 和 TfidfVectorizer 实现都通过 ngram_range 参数提供 n-gram,该参数有最小和最大限制。默认情况下,该参数为(1, 1)。当我们将其改为 (1, 3) 时,我们将看到单字元、双字元和三字元。代码改动很小。 380 | 381 | 由于到目前为止我们使用 tf-idf 得到了最好的结果,让我们来看看包含 n-grams 直至 trigrams 是否能改进模型。唯一需要修改的是 TfidfVectorizer 的初始化。 382 | 383 | ```python 384 | tfidf_vec = TfidfVectorizer( 385 | tokenizer=word_tokenize, 386 | token_pattern=None, 387 | ngram_range=(1, 3) 388 | ) 389 | ``` 390 | 391 | 让我们看看是否会有改进。 392 | 393 | ```python 394 | Fold: 0 395 | Accuracy = 0.8931 396 | 397 | Fold: 1 398 | Accuracy = 0.8941 399 | 400 | Fold: 2 401 | Accuracy = 0.897 402 | 403 | Fold: 3 404 | Accuracy = 0.8922 405 | 406 | Fold: 4 407 | Accuracy = 0.8847 408 | ``` 409 | 410 | 看起来还行,但我们看不到任何改进。 也许我们可以通过多使用 bigrams 来获得改进。 我不会在这里展示这一部分。也许你可以自己试着做。 411 | 412 | NLP 的基础知识还有很多。你必须知道的一个术语是词干提取(strmming)。另一个是词形还原(lemmatization)。**词干提取和词形还原**可以将一个词减少到最小形式。在词干提取的情况下,处理后的单词称为词干单词,而在词形还原情况下,处理后的单词称为词形。必须指出的是,词形还原比词干提取更激进,而词干提取更流行和广泛。词干和词形都来自语言学。如果你打算为某种语言制作词干或词型,需要对该语言有深入的了解。如果要过多地介绍这些知识,就意味着要在本书中增加一章。使用 NLTK 软件包可以轻松完成词干提取和词形还原。让我们来看看这两种方法的一些示例。有许多不同类型的词干提取和词形还原器。我将用最常见的 Snowball Stemmer 和 WordNet Lemmatizer 来举例说明。 413 | 414 | ```python 415 | from nltk.stem import WordNetLemmatizer 416 | from nltk.stem.snowball import SnowballStemmer 417 | 418 | lemmatizer = WordNetLemmatizer() 419 | stemmer = SnowballStemmer("english") 420 | words = ["fishing", "fishes", "fished"] 421 | for word in words: 422 | print(f"word={word}") 423 | print(f"stemmed_word={stemmer.stem(word)}") 424 | print(f"lemma={lemmatizer.lemmatize(word)}") 425 | print("") 426 | ``` 427 | 428 | 这将打印: 429 | 430 | ```python 431 | word=fishing 432 | stemmed_word=fish 433 | lemma=fishing 434 | word=fishes 435 | stemmed_word=fish 436 | lemma=fish 437 | word=fished 438 | stemmed_word=fish 439 | lemma=fished 440 | ``` 441 | 442 | 正如您所看到的,词干提取和词形还原是截然不同的。当我们进行词干提取时,我们得到的是一个词的最小形式,它可能是也可能不是该词所属语言词典中的一个词。但是,在词形还原情况下,这将是一个词。现在,您可以自己尝试添加词干和词素化,看看是否能改善结果。 443 | 444 | 您还应该了解的一个主题是主题提取。**主题提取**可以使用非负矩阵因式分解(NMF)或潜在语义分析(LSA)来完成,后者也被称为奇异值分解或 SVD。这些分解技术可将数据简化为给定数量的成分。 您可以在从 CountVectorizer 或 TfidfVectorizer 中获得的稀疏矩阵上应用其中任何一种技术。 445 | 446 | 让我们把它应用到之前使用过的 TfidfVetorizer 上。 447 | 448 | ```python 449 | import pandas as pd 450 | from nltk.tokenize import word_tokenize 451 | from sklearn import decomposition 452 | from sklearn.feature_extraction.text import TfidfVectorizer 453 | corpus = pd.read_csv("../input/imdb.csv", nrows=10000) 454 | corpus = corpus.review.values 455 | tfv = TfidfVectorizer(tokenizer=word_tokenize, token_pattern=None) 456 | tfv.fit(corpus) 457 | corpus_transformed = tfv.transform(corpus) 458 | svd = decomposition.TruncatedSVD(n_components=10) 459 | corpus_svd = svd.fit(corpus_transformed) 460 | sample_index = 0 461 | feature_scores = dict( 462 | zip( 463 | tfv.get_feature_names(), 464 | corpus_svd.components_[sample_index] 465 | ) 466 | ) 467 | N = 5 468 | print(sorted(feature_scores, key=feature_scores.get, reverse=True)[:N]) 469 | 470 | ``` 471 | 472 | 您可以使用循环来运行多个样本。 473 | 474 | ```python 475 | N = 5 476 | for sample_index in range(5): 477 | feature_scores = dict( 478 | zip( 479 | tfv.get_feature_names(), 480 | corpus_svd.components_[sample_index] 481 | ) 482 | ) 483 | print( 484 | sorted( 485 | feature_scores, 486 | key=feature_scores.get, 487 | reverse=True 488 | )[:N] 489 | ) 490 | ``` 491 | 492 | 输出结果如下: 493 | 494 | ```python 495 | ['the', ',', '.', 'a', 'and'] 496 | ['br', '<', '>', '/', '-'] 497 | ['i', 'movie', '!', 'it', 'was'] 498 | [',', '!', "''", '``', 'you'] 499 | ['!', 'the', '...', "''", '``'] 500 | ``` 501 | 502 | 你可以看到,这根本说不通。怎么办呢?让我们试着清理一下,看看是否有意义。要清理任何文本数据,尤其是 pandas 数据帧中的文本数据,可以创建一个函数。 503 | 504 | ```python 505 | import re 506 | import string 507 | 508 | def clean_text(s): 509 | s = s.split() 510 | s = " ".join(s) 511 | s = re.sub(f'[{re.escape(string.punctuation)}]', '', s) 512 | return s 513 | ``` 514 | 515 | 该函数会将 "hi, how are you????" 这样的字符串转换为 "hi how are you"。让我们把这个函数应用到旧的 SVD 代码中,看看它是否能给提取的主题带来提升。使用 pandas,你可以使用 apply 函数将清理代码 "应用 "到任意给定的列中。 516 | 517 | ```python 518 | import pandas as pd 519 | corpus = pd.read_csv("../input/imdb.csv", nrows=10000) 520 | corpus.loc[:, "review"] = corpus.review.apply(clean_text) 521 | ``` 522 | 523 | 请注意,我们只在主 SVD 脚本中添加了一行代码,这就是使用函数和 pandas 应用的好处。这次生成的主题如下。 524 | 525 | ```python 526 | ['the', 'a', 'and', 'of', 'to'] 527 | ['i', 'movie', 'it', 'was', 'this'] 528 | ['the', 'was', 'i', 'were', 'of'] 529 | ['her', 'was', 'she', 'i', 'he'] 530 | ['br', 'to', 'they', 'he', 'show'] 531 | ``` 532 | 533 | 呼!至少这比我们之前好多了。但你知道吗?你可以通过在清理功能中删除停止词(stopwords)来使它变得更好。什么是stopwords?它们是存在于每种语言中的高频词。例如,在英语中,这些词包括 "a"、"an"、"the"、"for "等。删除停止词并非总是明智的选择,这在很大程度上取决于业务问题。像 "I need a new dog"这样的句子,去掉停止词后会变成 "need new dog",此时我们不知道谁需要new dog。 534 | 535 | 如果我们总是删除停止词,就会丢失很多上下文信息。你可以在 NLTK 中找到许多语言的停止词,如果没有,你也可以在自己喜欢的搜索引擎上快速搜索一下。 536 | 537 | 现在,让我们转到大多数人都喜欢使用的方法:深度学习。但首先,我们必须知道什么是词嵌入(embedings for words)。你已经看到,到目前为止,我们已经将标记转换成了数字。因此,如果某个语料库中有 N 个唯一的词块,它们可以用 0 到 N-1 之间的整数来表示。现在,我们将用向量来表示这些整数词块。这种将单词表示成向量的方法被称为单词嵌入或单词向量。谷歌的 Word2Vec 是将单词转换为向量的最古老方法之一。此外,还有 Facebook 的 FastText 和斯坦福大学的 GloVe(用于单词表示的全局向量)。这些方法彼此大相径庭。 538 | 539 | 其基本思想是建立一个浅层网络,通过重构输入句子来学习单词的嵌入。因此,您可以通过使用周围的所有单词来训练网络预测一个缺失的单词,在此过程中,网络将学习并更新所有相关单词的嵌入。这种方法也被称为连续词袋或 CBoW 模型。您也可以尝试使用一个单词来预测上下文中的单词。这就是所谓的跳格模型。Word2Vec 可以使用这两种方法学习嵌入。 540 | 541 | FastText 可以学习字符 n-gram 的嵌入。和单词 n-gram 一样,如果我们使用的是字符,则称为字符 n-gram,最后,GloVe 通过共现矩阵来学习这些嵌入。因此,我们可以说,所有这些不同类型的嵌入最终都会返回一个字典,其中键是语料库(例如英语维基百科)中的单词,值是大小为 N(通常为 300)的向量。 542 | 543 | ![](figures/AAAMLP_page244_image.png) 544 | 545 |

图 1:可视化二维单词嵌入。

546 | 547 | 图 1 显示了二维单词嵌入的可视化效果。假设我们以某种方式完成了词语的二维表示。图 1 显示,如果从Berlin(德国首都)的向量中减去德国(Germany)的向量,再加上法国(France)的向量,就会得到一个接近Paris(法国首都)的向量。由此可见,嵌入式也能进行类比。 这并不总是正确的,但这样的例子有助于理解单词嵌入的作用。像 "hi, how are you?" 这样的句子可以用下面的一堆向量来表示。 548 | 549 | hi ─> [vector (v1) of size 300] 550 | 551 | , ─> [vector (v2) of size 300] 552 | 553 | how ─> [vector (v3) of size 300] 554 | 555 | are ─> [vector (v4) of size 300] 556 | 557 | you ─> [vector (v5) of size 300] 558 | 559 | ? ─> [vector (v6) of size 300] 560 | 561 | 使用这些信息有多种方法。最简单的方法之一就是使用嵌入向量。如上例所示,每个单词都有一个 1x300 的嵌入向量。利用这些信息,我们可以计算出整个句子的嵌入。计算方法有多种。其中一种方法如下所示。在这个函数中,我们将给定句子中的所有单词向量提取出来,然后从所有标记词的单词向量中创建一个归一化的单词向量。这样就得到了一个句子向量。 562 | 563 | ```python 564 | import numpy as np 565 | def sentence_to_vec(s, embedding_dict, stop_words, tokenizer): 566 | words = str(s).lower() 567 | words = tokenizer(words) 568 | words = [w for w in words if not w in stop_words] 569 | words = [w for w in words if w.isalpha()] 570 | M = [] 571 | for w in words: 572 | if w in embedding_dict: 573 | M.append(embedding_dict[w]) 574 | if len(M) == 0: 575 | return np.zeros(300) 576 | M = np.array(M) 577 | v = M.sum(axis=0) 578 | return v / np.sqrt((v ** 2).sum()) 579 | ``` 580 | 581 | 我们可以用这种方法将所有示例转换成一个向量。我们能否使用 fastText 向量来改进之前的结果?每篇评论都有 300 个特征。 582 | 583 | ```python 584 | import io 585 | import numpy as np 586 | import pandas as pd 587 | from nltk.tokenize import word_tokenize 588 | from sklearn import linear_model 589 | from sklearn import metrics 590 | from sklearn import model_selection 591 | from sklearn.feature_extraction.text import TfidfVectorizer 592 | def load_vectors(fname): 593 | fin = io.open( 594 | fname, 595 | 'r', 596 | encoding='utf-8', 597 | newline='\n', 598 | errors='ignore' 599 | ) 600 | n, d = map(int, fin.readline().split()) 601 | data = {} 602 | for line in fin: 603 | tokens = line.rstrip().split(' ') 604 | data[tokens[0]] = list(map(float, tokens[1:])) 605 | return data 606 | 607 | def sentence_to_vec(s, embedding_dict, stop_words, tokenizer): 608 | 609 | if __name__ == "__main__": 610 | df = pd.read_csv("../input/imdb.csv") 611 | df.sentiment = df.sentiment.apply( 612 | lambda x: 1 if x == "positive" else 0 613 | ) 614 | df = df.sample(frac=1).reset_index(drop=True) 615 | print("Loading embeddings") 616 | embeddings = load_vectors("../input/crawl-300d-2M.vec") 617 | print("Creating sentence vectors") 618 | vectors = [] 619 | for review in df.review.values: 620 | vectors.append( 621 | sentence_to_vec( 622 | s = review, 623 | embedding_dict = embeddings, 624 | stop_words = [], 625 | tokenizer = word_tokenize 626 | ) 627 | ) 628 | vectors = np.array(vectors) 629 | y = df.sentiment.values 630 | kf = model_selection.StratifiedKFold(n_splits=5) 631 | for fold_, (t_, v_) in enumerate(kf.split(X=vectors, y=y)): 632 | print(f"Training fold: {fold_}") 633 | xtrain = vectors[t_, :] 634 | ytrain = y[t_] 635 | xtest = vectors[v_, :] 636 | ytest = y[v_] 637 | model = linear_model.LogisticRegression() 638 | model.fit(xtrain, ytrain) 639 | preds = model.predict(xtest) 640 | accuracy = metrics.accuracy_score(ytest, preds) 641 | print(f"Accuracy = {accuracy}") 642 | print("") 643 | ``` 644 | 645 | 这将得到如下结果: 646 | 647 | ```python 648 | Loading embeddings 649 | Creating sentence vectors 650 | 651 | Training fold: 0 652 | Accuracy = 0.8619 653 | 654 | Training fold: 1 655 | Accuracy = 0.8661 656 | 657 | Training fold: 2 658 | Accuracy = 0.8544 659 | 660 | Training fold: 3 661 | Accuracy = 0.8624 662 | 663 | Training fold: 4 664 | Accuracy = 0.8595 665 | ``` 666 | 667 | Wow!真是出乎意料。我们所做的一切都是为了使用 FastText 嵌入。试着把嵌入式换成 GloVe,看看会发生什么。我把它作为一个练习留给大家。 668 | 当我们谈论文本数据时,我们必须牢记一件事。文本数据与时间序列数据非常相似。如图 2 所示,我们评论中的任何样本都是在不同时间戳上按递增顺序排列的标记序列,每个标记都可以表示为一个向量/嵌入。 669 | 670 | ![](figures/AAAMLP_page249_image.png) 671 | 672 |

图 2:将标记表示为嵌入,并将其视为时间序列

673 | 674 | 这意味着我们可以使用广泛用于时间序列数据的模型,例如长短期记忆(LSTM)或门控递归单元(GRU),甚至卷积神经网络(CNN)。让我们看看如何在该数据集上训练一个简单的双向 LSTM 模型。 675 | 676 | 首先,我们将创建一个项目。你可以随意给它命名。然后,我们的第一步将是分割数据进行交叉验证。 677 | 678 | ```python 679 | import pandas as pd 680 | from sklearn import model_selection 681 | if __name__ == "__main__": 682 | df = pd.read_csv("../input/imdb.csv") 683 | df.sentiment = df.sentiment.apply( 684 | lambda x: 1 if x == "positive" else 0 685 | ) 686 | df["kfold"] = -1 687 | df = df.sample(frac=1).reset_index(drop=True) 688 | y = df.sentiment.values 689 | kf = model_selection.StratifiedKFold(n_splits=5) 690 | for f, (t_, v_) in enumerate(kf.split(X=df, y=y)): 691 | df.loc[v_, 'kfold'] = f 692 | df.to_csv("../input/imdb_folds.csv", index=False) 693 | ``` 694 | 695 | 将数据集划分为多个折叠后,我们就可以在 dataset.py 中创建一个简单的数据集类。数据集类会返回一个训练或验证数据样本。 696 | 697 | ```python 698 | import torch 699 | class IMDBDataset: 700 | def __init__(self, reviews, targets): 701 | self.reviews = reviews 702 | self.target = targets 703 | def __len__(self): 704 | return len(self.reviews) 705 | def __getitem__(self, item): 706 | review = self.reviews[item, :] 707 | target = self.target[item] 708 | return { 709 | "review": torch.tensor(review, dtype=torch.long), 710 | "target": torch.tensor(target, dtype=torch.float) 711 | } 712 | ``` 713 | 714 | 完成数据集分类后,我们就可以创建 lstm.py,其中包含我们的 LSTM 模型 715 | 716 | ```python 717 | import torch 718 | import torch.nn as nn 719 | class LSTM(nn.Module): 720 | def __init__(self, embedding_matrix): 721 | super(LSTM, self).__init__() 722 | num_words = embedding_matrix.shape[0] 723 | embed_dim = embedding_matrix.shape[1] 724 | self.embedding = nn.Embedding( 725 | num_embeddings=num_words, 726 | embedding_dim=embed_dim) 727 | self.embedding.weight = nn.Parameter( 728 | torch.tensor( 729 | embedding_matrix, 730 | dtype=torch.float32 731 | ) 732 | ) 733 | self.embedding.weight.requires_grad = False 734 | self.lstm = nn.LSTM( 735 | embed_dim, 736 | 128, 737 | bidirectional=True, 738 | batch_first=True, 739 | ) 740 | self.out = nn.Linear(512, 1) 741 | 742 | def forward(self, x): 743 | x = self.embedding(x) 744 | x, _ = self.lstm(x) 745 | avg_pool = torch.mean(x, 1) 746 | max_pool, _ = torch.max(x, 1) 747 | out = torch.cat((avg_pool, max_pool), 1) 748 | out = self.out(out) 749 | return out 750 | ``` 751 | 752 | 现在,我们创建 engine.py,其中包含训练和评估函数。 753 | 754 | ```python 755 | import torch 756 | import torch.nn as nn 757 | def train(data_loader, model, optimizer, device): 758 | model.train() 759 | for data in data_loader: 760 | reviews = data["review"] 761 | targets = data["target"] 762 | reviews = reviews.to(device, dtype=torch.long) 763 | targets = targets.to(device, dtype=torch.float) 764 | optimizer.zero_grad() 765 | predictions = model(reviews) 766 | loss = nn.BCEWithLogitsLoss()( 767 | predictions, 768 | targets.view(-1, 1) 769 | ) 770 | loss.backward() 771 | optimizer.step() 772 | 773 | def evaluate(data_loader, model, device): 774 | final_predictions = [] 775 | final_targets = [] 776 | model.eval() 777 | with torch.no_grad(): 778 | for data in data_loader: 779 | reviews = data["review"] 780 | targets = data["target"] 781 | reviews = reviews.to(device, dtype=torch.long) 782 | targets = targets.to(device, dtype=torch.float) 783 | predictions = model(reviews) 784 | predictions = predictions.cpu().numpy().tolist() 785 | targets = data["target"].cpu().numpy().tolist() 786 | final_predictions.extend(predictions) 787 | final_targets.extend(targets) 788 | return final_predictions, final_targets 789 | ``` 790 | 791 | 这些函数将在 train.py 中为我们提供帮助,该函数用于训练多个折叠。 792 | 793 | ```python 794 | import io 795 | import torch 796 | import numpy as np 797 | import pandas as pd 798 | import tensorflow as tf 799 | from sklearn import metrics 800 | import config 801 | import dataset 802 | import engine 803 | import lstm 804 | 805 | def load_vectors(fname): 806 | fin = io.open( 807 | fname, 808 | 'r', 809 | encoding='utf-8', 810 | newline='\n', 811 | errors='ignore' 812 | ) 813 | n, d = map(int, fin.readline().split()) 814 | data = {} 815 | for line in fin: 816 | tokens = line.rstrip().split(' ') 817 | data[tokens[0]] = list(map(float, tokens[1:])) 818 | return data 819 | 820 | def create_embedding_matrix(word_index, embedding_dict): 821 | embedding_matrix = np.zeros((len(word_index) + 1, 300)) 822 | for word, i in word_index.items(): 823 | if word in embedding_dict: 824 | embedding_matrix[i] = embedding_dict[word] 825 | return embedding_matrix 826 | 827 | def run(df, fold): 828 | train_df = df[df.kfold != fold].reset_index(drop=True) 829 | valid_df = df[df.kfold == fold].reset_index(drop=True) 830 | print("Fitting tokenizer") 831 | tokenizer = tf.keras.preprocessing.text.Tokenizer() 832 | tokenizer.fit_on_texts(df.review.values.tolist()) 833 | xtrain = tokenizer.texts_to_sequences(train_df.review.values) 834 | xtest = tokenizer.texts_to_sequences(valid_df.review.values) 835 | xtrain = tf.keras.preprocessing.sequence.pad_sequences( 836 | xtrain, maxlen=config.MAX_LEN 837 | ) 838 | xtest = tf.keras.preprocessing.sequence.pad_sequences( 839 | xtest, maxlen=config.MAX_LEN 840 | ) 841 | train_dataset = dataset.IMDBDataset( 842 | reviews=xtrain, 843 | targets=train_df.sentiment.values 844 | ) 845 | train_data_loader = torch.utils.data.DataLoader( 846 | train_dataset, 847 | batch_size=config.TRAIN_BATCH_SIZE, 848 | num_workers=2 849 | ) 850 | valid_dataset = dataset.IMDBDataset( 851 | reviews=xtest, 852 | targets=valid_df.sentiment.values 853 | ) 854 | valid_data_loader = torch.utils.data.DataLoader( 855 | valid_dataset, 856 | batch_size=config.VALID_BATCH_SIZE, 857 | num_workers=1 858 | ) 859 | print("Loading embeddings") 860 | embedding_dict = load_vectors("../input/crawl-300d-2M.vec") 861 | embedding_matrix = create_embedding_matrix( 862 | tokenizer.word_index, embedding_dict 863 | ) 864 | device = torch.device("cuda") 865 | model = lstm.LSTM(embedding_matrix) 866 | model.to(device) 867 | optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) 868 | print("Training Model") 869 | best_accuracy = 0 870 | early_stopping_counter = 0 871 | for epoch in range(config.EPOCHS): 872 | engine.train(train_data_loader, model, optimizer, device) 873 | outputs, targets = engine.evaluate( 874 | valid_data_loader, model, device 875 | ) 876 | outputs = np.array(outputs) >= 0.5 877 | accuracy = metrics.accuracy_score(targets, outputs) 878 | print(f"FOLD:{fold}, Epoch: {epoch}, Accuracy Score = {accuracy}") 879 | if accuracy > best_accuracy: 880 | best_accuracy = accuracy 881 | else: 882 | early_stopping_counter += 1 883 | if early_stopping_counter > 2: 884 | break 885 | if __name__ == "__main__": 886 | df = pd.read_csv("../input/imdb_folds.csv") 887 | run(df, fold=0) 888 | run(df, fold=1) 889 | run(df, fold=2) 890 | run(df, fold=3) 891 | run(df, fold=4) 892 | ``` 893 | 894 | 最后是 config.py。 895 | 896 | ```python 897 | MAX_LEN = 128 898 | TRAIN_BATCH_SIZE = 16 899 | VALID_BATCH_SIZE = 8 900 | EPOCHS = 10 901 | ``` 902 | 903 | 让我们看看输出: 904 | 905 | ```python 906 | FOLD:0, Epoch: 3, Accuracy Score = 0.9015 907 | FOLD:1, Epoch: 4, Accuracy Score = 0.9007 908 | FOLD:2, Epoch: 3, Accuracy Score = 0.8924 909 | FOLD:3, Epoch: 2, Accuracy Score = 0.9 910 | FOLD:4, Epoch: 1, Accuracy Score = 0.878 911 | ``` 912 | 913 | 这是迄今为止我们获得的最好成绩。 请注意,我只显示了每个折叠中精度最高的Epoch。 914 | 915 | 你一定已经注意到,我们使用了预先训练的嵌入和简单的双向 LSTM。 如果你想改变模型,你可以只改变 lstm.py 中的模型并保持一切不变。 这种代码只需要很少的实验改动,并且很容易理解。 例如,您可以自己学习嵌入而不是使用预训练的嵌入,您可以使用其他一些预训练的嵌入,您可以组合多个预训练的嵌入,您可以使用GRU,您可以在嵌入后使用空间dropout,您可以添加GRU LSTM 层之后,您可以添加两个 LSTM 层,您可以进行 LSTM-GRU-LSTM 配置,您可以用卷积层替换 LSTM 等,而无需对代码进行太多更改。 我提到的大部分内容只需要更改模型类。 916 | 917 | 当您使用预训练的嵌入时,尝试查看有多少单词无法找到嵌入以及原因。 预训练嵌入的单词越多,结果就越好。 我向您展示以下未注释的 (!) 函数,您可以使用它为任何类型的预训练嵌入创建嵌入矩阵,其格式与 glove 或 fastText 相同(可能需要进行一些更改)。 918 | 919 | ```python 920 | def load_embeddings(word_index, embedding_file, vector_length=300): 921 | max_features = len(word_index) + 1 922 | words_to_find = list(word_index.keys()) 923 | more_words_to_find = [] 924 | for wtf in words_to_find: 925 | more_words_to_find.append(wtf) 926 | more_words_to_find.append(str(wtf).capitalize()) 927 | more_words_to_find = set(more_words_to_find) 928 | def get_coefs(word, *arr): 929 | return word, np.asarray(arr, dtype='float32') 930 | 931 | embeddings_index = dict( 932 | get_coefs(*o.strip().split(" ")) 933 | for o in open(embedding_file) 934 | if o.split(" ")[0] 935 | in more_words_to_find 936 | and len(o) > 100 937 | ) 938 | 939 | embedding_matrix = np.zeros((max_features, vector_length)) 940 | for word, i in word_index.items(): 941 | if i >= max_features: 942 | continue 943 | embedding_vector = embeddings_index.get(word) 944 | if embedding_vector is None: 945 | embedding_vector = embeddings_index.get( 946 | str(word).capitalize() 947 | ) 948 | if embedding_vector is None: 949 | embedding_vector = embeddings_index.get( 950 | str(word).upper() 951 | ) 952 | if (embedding_vector is not None 953 | and len(embedding_vector) == vector_length): 954 | embedding_matrix[i] = embedding_vector 955 | return embedding_matrix 956 | ``` 957 | 958 | 阅读并运行上面的函数,看看发生了什么。 该函数还可以修改为使用词干词或词形还原词。 最后,您希望训练语料库中的未知单词数量最少。 另一个技巧是学习嵌入层,即使其可训练,然后训练网络。 959 | 960 | 到目前为止,我们已经为分类问题构建了很多模型。 然而,现在是布偶时代,越来越多的人转向基于 Transformer 的模型。 基于 Transformer 的网络能够处理本质上长期的依赖关系。 LSTM 仅当它看到前一个单词时才查看下一个单词。 Transformer 的情况并非如此。 它可以同时查看整个句子中的所有单词。 因此,另一个优点是它可以轻松并行化并更有效地使用 GPU。 961 | 962 | Transformers 是一个非常广泛的话题,有太多的模型:**BERT、RoBERTa、XLNet、XLM-RoBERTa、T5** 等。我将向您展示一种可用于所有这些模型(T5 除外)进行分类的通用方法 我们一直在讨论的问题。 请注意,这些 Transformer 需要训练它们所需的计算能力。 因此,如果您没有高端系统,与基于 LSTM 或 TF-IDF 的模型相比,训练模型可能需要更长的时间。 963 | 964 | 我们要做的第一件事是创建一个配置文件。 965 | 966 | ```python 967 | import transformers 968 | MAX_LEN = 512 969 | TRAIN_BATCH_SIZE = 8 970 | VALID_BATCH_SIZE = 4 971 | EPOCHS = 10 972 | 973 | BERT_PATH = "../input/bert_base_uncased/" 974 | MODEL_PATH = "model.bin" 975 | TRAINING_FILE = "../input/imdb.csv" 976 | TOKENIZER = transformers.BertTokenizer.from_pretrained( 977 | BERT_PATH, 978 | do_lower_case=True 979 | ) 980 | ``` 981 | 982 | 这里的配置文件是我们定义分词器和其他我们想要经常更改的参数的唯一地方 —— 这样我们就可以做很多实验而不需要进行大量更改。 983 | 984 | 下一步是构建数据集类。 985 | 986 | ```python 987 | import config 988 | import torch 989 | class BERTDataset: 990 | def __init__(self, review, target): 991 | self.review = review 992 | self.target = target 993 | self.tokenizer = config.TOKENIZER 994 | self.max_len = config.MAX_LEN 995 | def __len__(self): 996 | return len(self.review) 997 | def __getitem__(self, item): 998 | review = str(self.review[item]) 999 | review = " ".join(review.split()) 1000 | inputs = self.tokenizer.encode_plus( 1001 | review, 1002 | None, 1003 | add_special_tokens=True, 1004 | max_length=self.max_len, 1005 | pad_to_max_length=True, 1006 | ) 1007 | ids = inputs["input_ids"] 1008 | mask = inputs["attention_mask"] 1009 | token_type_ids = inputs["token_type_ids"] 1010 | return { 1011 | "ids": torch.tensor( 1012 | ids, dtype=torch.long 1013 | ), 1014 | "mask": torch.tensor( 1015 | mask, dtype=torch.long 1016 | ), 1017 | "token_type_ids": torch.tensor( 1018 | token_type_ids, dtype=torch.long 1019 | ), 1020 | "targets": torch.tensor( 1021 | self.target[item], dtype=torch.float 1022 | ) 1023 | } 1024 | ``` 1025 | 1026 | 现在我们来到了该项目的核心,即模型。 1027 | 1028 | ```python 1029 | import config 1030 | import transformers 1031 | import torch.nn as nn 1032 | class BERTBaseUncased(nn.Module): 1033 | def __init__(self): 1034 | super(BERTBaseUncased, self).__init__() 1035 | self.bert = transformers.BertModel.from_pretrained( 1036 | config.BERT_PATH 1037 | ) 1038 | self.bert_drop = nn.Dropout(0.3) 1039 | self.out = nn.Linear(768, 1) 1040 | def forward(self, ids, mask, token_type_ids): 1041 | hidden state 1042 | _, o2 = self.bert( 1043 | ids, 1044 | attention_mask=mask, 1045 | token_type_ids=token_type_ids 1046 | ) 1047 | bo = self.bert_drop(o2) 1048 | output = self.out(bo) 1049 | return output 1050 | ``` 1051 | 1052 | 该模型返回单个输出。 我们可以使用带有 logits 的二元交叉熵损失,它首先应用 sigmoid,然后计算损失。 这是在engine.py 中完成的。 1053 | 1054 | ```python 1055 | import torch 1056 | import torch.nn as nn 1057 | def loss_fn(outputs, targets): 1058 | return nn.BCEWithLogitsLoss()(outputs, targets.view(-1, 1)) 1059 | def train_fn(data_loader, model, optimizer, device, scheduler): 1060 | model.train() 1061 | for d in data_loader: 1062 | ids = d["ids"] 1063 | token_type_ids = d["token_type_ids"] 1064 | mask = d["mask"] 1065 | targets = d["targets"] 1066 | ids = ids.to(device, dtype=torch.long) 1067 | token_type_ids = token_type_ids.to(device, dtype=torch.long) 1068 | mask = mask.to(device, dtype=torch.long) 1069 | targets = targets.to(device, dtype=torch.float) 1070 | optimizer.zero_grad() 1071 | outputs = model( 1072 | ids=ids, 1073 | mask=mask, 1074 | token_type_ids=token_type_ids 1075 | ) 1076 | loss = loss_fn(outputs, targets) 1077 | loss.backward() 1078 | optimizer.step() 1079 | scheduler.step() 1080 | 1081 | def eval_fn(data_loader, model, device): 1082 | model.eval() 1083 | fin_targets = [] 1084 | fin_outputs = [] 1085 | with torch.no_grad(): 1086 | for d in data_loader: 1087 | ids = d["ids"] 1088 | token_type_ids = d["token_type_ids"] 1089 | mask = d["mask"] 1090 | targets = d["targets"] 1091 | ids = ids.to(device, dtype=torch.long) 1092 | token_type_ids = token_type_ids.to(device, dtype=torch.long) 1093 | mask = mask.to(device, dtype=torch.long) 1094 | targets = targets.to(device, dtype=torch.float) 1095 | outputs = model( 1096 | ids=ids, 1097 | mask=mask, 1098 | token_type_ids=token_type_ids 1099 | ) 1100 | targets = targets.cpu().detach() 1101 | fin_targets.extend(targets.numpy().tolist()) 1102 | outputs = torch.sigmoid(outputs).cpu().detach() 1103 | fin_outputs.extend(outputs.numpy().tolist()) 1104 | return fin_outputs, fin_targets 1105 | ``` 1106 | 1107 | 最后,我们准备好训练了。 我们来看看训练脚本吧! 1108 | 1109 | ```python 1110 | import config 1111 | import dataset 1112 | import engine 1113 | import torch 1114 | import pandas as pd 1115 | import torch.nn as nn 1116 | import numpy as np 1117 | from model import BERTBaseUncased 1118 | from sklearn import model_selection 1119 | from sklearn import metrics 1120 | from transformers import AdamW 1121 | from transformers import get_linear_schedule_with_warmup 1122 | def train(): 1123 | dfx = pd.read_csv(config.TRAINING_FILE).fillna("none") 1124 | dfx.sentiment = dfx.sentiment.apply( 1125 | lambda x: 1 if x == "positive" else 0 1126 | ) 1127 | df_train, df_valid = model_selection.train_test_split( 1128 | dfx, 1129 | test_size=0.1, 1130 | random_state=42, 1131 | stratify=dfx.sentiment.values 1132 | ) 1133 | df_train = df_train.reset_index(drop=True) 1134 | df_valid = df_valid.reset_index(drop=True) 1135 | train_dataset = dataset.BERTDataset( 1136 | review=df_train.review.values, 1137 | target=df_train.sentiment.values 1138 | ) 1139 | train_data_loader = torch.utils.data.DataLoader( 1140 | train_dataset, 1141 | batch_size=config.TRAIN_BATCH_SIZE, 1142 | num_workers=4 1143 | ) 1144 | valid_dataset = dataset.BERTDataset( 1145 | review=df_valid.review.values, 1146 | target=df_valid.sentiment.values 1147 | ) 1148 | valid_data_loader = torch.utils.data.DataLoader( 1149 | valid_dataset, 1150 | batch_size=config.VALID_BATCH_SIZE, 1151 | num_workers=1 1152 | ) 1153 | device = torch.device("cuda") 1154 | model = BERTBaseUncased() 1155 | model.to(device) 1156 | param_optimizer = list(model.named_parameters()) 1157 | no_decay = ["bias", "LayerNorm.bias", "LayerNorm.weight"] 1158 | optimizer_parameters = [ 1159 | { 1160 | "params": [ 1161 | p for n, p in param_optimizer if 1162 | not any(nd in n for nd in no_decay) 1163 | ], 1164 | "weight_decay": 0.001, 1165 | }, 1166 | { 1167 | "params": [ 1168 | p for n, p in param_optimizer if 1169 | any(nd in n for nd in no_decay) 1170 | ], 1171 | "weight_decay": 0.0, 1172 | }] 1173 | num_train_steps = int( 1174 | len(df_train) / config.TRAIN_BATCH_SIZE * config.EPOCHS 1175 | ) 1176 | optimizer = AdamW(optimizer_parameters, lr=3e-5) 1177 | scheduler = get_linear_schedule_with_warmup( 1178 | optimizer, 1179 | num_warmup_steps=0, 1180 | num_training_steps=num_train_steps 1181 | ) 1182 | model = nn.DataParallel(model) 1183 | best_accuracy = 0 1184 | for epoch in range(config.EPOCHS): 1185 | engine.train_fn( 1186 | train_data_loader, model, optimizer, device, scheduler 1187 | ) 1188 | outputs, targets = engine.eval_fn( 1189 | valid_data_loader, model, device 1190 | ) 1191 | 1192 | outputs = np.array(outputs) >= 0.5 1193 | accuracy = metrics.accuracy_score(targets, outputs) 1194 | print(f"Accuracy Score = {accuracy}") 1195 | if accuracy > best_accuracy: 1196 | torch.save(model.state_dict(), config.MODEL_PATH) 1197 | best_accuracy = accuracy 1198 | 1199 | if __name__ == "__main__": 1200 | train() 1201 | ``` 1202 | 1203 | 乍一看可能看起来很多,但一旦您了解了各个组件,就不再那么简单了。 您只需更改几行代码即可轻松将其更改为您想要使用的任何其他 Transformer 模型。 1204 | 1205 | 该模型的准确率为 93%! 哇! 这比任何其他模型都要好得多。 但是这值得吗? 1206 | 1207 | 我们使用 LSTM 能够实现 90% 的目标,而且它们更简单、更容易训练并且推理速度更快。 通过使用不同的数据处理或调整层、节点、dropout、学习率、更改优化器等参数,我们可以将该模型改进一个百分点。然后我们将从 BERT 中获得约 2% 的收益。 另一方面,BERT 的训练时间要长得多,参数很多,而且推理速度也很慢。 最后,您应该审视自己的业务并做出明智的选择。 不要仅仅因为 BERT“酷”而选择它。 1208 | 1209 | 必须注意的是,我们在这里讨论的唯一任务是分类,但将其更改为回归、多标签或多类只需要更改几行代码。 例如,多类分类设置中的同一问题将有多个输出和交叉熵损失。 其他一切都应该保持不变。 自然语言处理非常庞大,我们只讨论了其中的一小部分。 显然,这是一个很大的比例,因为大多数工业模型都是分类或回归模型。 如果我开始详细写所有内容,我最终可能会写几百页,这就是为什么我决定将所有内容包含在一本单独的书中:接近(几乎)任何 NLP 问题! 1210 | -------------------------------------------------------------------------------- /docs/无监督和有监督学习.md: -------------------------------------------------------------------------------- 1 | # 无监督和有监督学习 2 | 3 | 在处理机器学习问题时,通常有两类数据(和机器学习模型): 4 | 5 | - 监督数据:总是有一个或多个与之相关的目标 6 | - 无监督数据:没有任何目标变量。 7 | 8 | 有监督问题比无监督问题更容易解决。我们需要预测一个值的问题被称为有监督问题。例如,如果问题是根据历史房价预测房价,那么医院、学校或超市的存在,与最近公共交通的距离等特征就是一个有监督的问题。同样,当我们得到猫和狗的图像时,我们事先知道哪些是猫,哪些是狗,如果任务是创建一个模型来预测所提供的图像是猫还是狗,那么这个问题就被认为是有监督的问题。 9 | 10 | ![](figures/AAAMLP_page6_image.png) 11 | 12 |

图 1:有监督学习数据

13 | 14 | 如图 1 所示,数据的每一行都与一个目标或标签相关联。列是不同的特征,行代表不同的数据点,通常称为样本。示例中的十个样本有十个特征和一个目标变量,目标变量可以是数字或类别。如果目标变量是分类变量,问题就变成了分类问题。如果目标变量是实数,问题就被定义为回归问题。因此,有监督问题可分为两个子类: 15 | 16 | - 分类:预测类别,如猫或狗 17 | - 回归:预测值,如房价 18 | 19 | 必须注意的是,有时我们可能会在分类设置中使用回归,这取决于用于评估的指标。不过,我们稍后会讨论这个问题。 20 | 21 | 另一种机器学习问题是无监督类型。**无监督**数据集没有与之相关的目标,一般来说,与有监督问题相比,处理无监督数据集更具挑战性。 22 | 23 | 假设你在一家处理信用卡交易的金融公司工作。每秒钟都有大量数据涌入。唯一的问题是,很难找到一个人来将每笔交易标记为有效交易、真实交易或欺诈交易。当我们没有任何关于交易是欺诈还是真实的信息时,问题就变成了无监督问题。要解决这类问题,我们必须考虑可以将数据分为多少个**聚类**。聚类是解决此类问题的方法之一,但必须注意的是,还有其他几种方法可以应用于无监督问题。对于欺诈检测问题,我们可以说数据可以分为两类(欺诈或真实)。 24 | 25 | 当我们知道聚类的数量后,就可以使用聚类算法来解决无监督问题。在图 2 中,假设数据分为两类,深色代表欺诈,浅色代表真实交易。然而,在使用聚类方法之前,我们并不知道这些类别。应用聚类算法后,我们应该能够区分这两个假定目标。 为了理解无监督问题,我们还可以使用许多分解技术,如**主成分分析(PCA)、t-分布随机邻域嵌入(t-SNE)**等。 26 | 27 | 有监督的问题更容易解决,因为它们很容易评估。我们将在接下来的章节中详细介绍评估技术。然而,对无监督算法的结果进行评估具有挑战性,需要大量的人为干预或启发式方法。在本书中,我们将主要关注有监督数据和模型,但这并不意味着我们会忽略无监督数据问题。 28 | 29 | ![](figures/AAAMLP_page8_image.png) 30 | 31 |

图 2:无监督学习数据集

32 | 33 | 大多数情况下,当人们开始学习数据科学或机器学习时,都会从非常著名的数据集开始,例如泰坦尼克数据集或鸢尾花数据集,这些都是有监督的问题。在泰坦尼克号数据集中,你必须根据船票等级、性别、年龄等因素预测泰坦尼克号上乘客的存活率。同样,在鸢尾花数据集中,您必须根据萼片宽度、花瓣长度、萼片长度和花瓣宽度等因素预测花的种类。 34 | 35 | 无监督数据集可能包括用于客户细分的数据集。 例如,您拥有访问您的电子商务网站的客户数据,或者访问商店或商场的客户数据,而您希望将它们细分或聚类为不同的类别。无监督数据集的另一个例子可能包括信用卡欺诈检测或对几张图片进行聚类等。 36 | 37 | 大多数情况下,还可以将有监督数据集转换为无监督数据集,以查看它们在绘制时的效果。 38 | 39 | 例如,让我们来看看图 3 中的数据集。图 3 显示的是 MNIST 数据集,这是一个非常流行的手写数字数据集,它是一个有监督的问题,在这个问题中,你会得到数字图像和与之相关的正确标签。你必须建立一个模型,在只提供图像的情况下识别出哪个数字是它。 40 | 41 | ![](figures/AAAMLP_page9_image.png) 42 | 43 |

图 3:MNIST数据集

44 | 45 | 如果我们对这个数据集进行 t 分布随机邻域嵌入(t-SNE)分解,我们可以看到,只需在图像像素上降维至 2 个维度,就能在一定程度上分离图像。如图 4 所示。 46 | 47 | ![](figures/AAAMLP_page9_image_1.png) 48 | 49 |

图 4:MNIST 数据集的 t-SNE 可视化。使用了 3000 幅图像。

50 | 51 | 让我们来看看是如何实现的。首先是导入所有需要的库。 52 | 53 | ```python 54 | import matplotlib.pyplot as plt 55 | import numpy as np 56 | import pandas as pd 57 | import seaborn as sns 58 | from sklearn import datasets 59 | from sklearn import manifold 60 | %matplotlib inline 61 | ``` 62 | 63 | 我们使用 matplotlib 和 seaborn 进行绘图,使用 numpy 处理数值数组,使用 pandas 从数值数组创建数据帧,使用 scikit-learn (sklearn) 获取数据并执行 t-SNE。 64 | 65 | 导入后,我们需要下载数据并单独读取,或者使用 sklearn 的内置函数来提供 MNIST 数据集。 66 | 67 | ```python 68 | data = datasets.fetch_openml('mnist_784', version=1, return_X_y=True) 69 | pixel_values, targets = data 70 | targets = targets.astype(int) 71 | ``` 72 | 73 | 在这部分代码中,我们使用 sklearn 数据集获取了数据,并获得了一个像素值数组和另一个目标数组。由于目标是字符串类型,我们将其转换为整数。 74 | 75 | pixel_values 是一个形状为 70000x784 的二维数组。 共有 70000 张不同的图像,每张图像大小为 28x28 像素。平铺 28x28 后得到 784 个数据点。 76 | 77 | 我们可以将该数据集中的样本重塑为原来的形状,然后使用 matplotlib 绘制成图表,从而将其可视化。 78 | 79 | ```python 80 | single_image = pixel_values[1, :].reshape(28, 28) 81 | plt.imshow(single_image, cmap='gray') 82 | ``` 83 | 84 | 这段代码将绘制如下图像: 85 | 86 | ![](figures/AAAMLP_page11_image.png) 87 | 88 |

图 5:绘制MNIST数据集单张图片

89 | 90 | 最重要的一步是在我们获取数据之后。 91 | 92 | ```python 93 | tsne = manifold.TSNE(n_components=2, random_state=42) 94 | transformed_data = tsne.fit_transform(pixel_values[:3000, :]) 95 | ``` 96 | 97 | 这一步创建了数据的 t-SNE 变换。我们只使用 2 个维度,因为在二维环境中可以很好地将它们可视化。在本例中,转换后的数据是一个 3000x2 形状的数组(3000 行 2 列)。在数组上调用 pd.DataFrame 可以将这样的数据转换为 pandas 数据帧。 98 | 99 | ```python 100 | tsne_df = pd.DataFrame(np.column_stack((transformed_data, targets[:3000])), 101 | columns=["x", "y", "targets"]) 102 | tsne_df.loc[:, "targets"] = tsne_df.targets.astype(int) 103 | ``` 104 | 105 | 在这里,我们从一个 numpy 数组创建一个 pandas 数据帧。x 和 y 是 t-SNE 分解的两个维度,target 是实际数字。这样我们就得到了如图 6 所示的数据帧。 106 | 107 | ![](figures/AAAMLP_page12_image.png) 108 | 109 |

图 6:t-SNE后数据前10行

110 | 111 | 最后,我们可以使用 seaborn 和 matplotlib 绘制它。 112 | 113 | ```python 114 | grid = sns.FacetGrid(tsne_df, hue="targets", size=8) 115 | grid.map(plt.scatter, "x", "y").add_legend() 116 | ``` 117 | 118 | 这是无监督数据集可视化的一种方法。我们还可以在同一数据集上进行 k-means 聚类,看看它在无监督环境下的表现如何。一个经常出现的问题是,如何在 k-means 聚类中找到最佳的簇数。这个问题没有正确答案。你必须通过交叉验证来找到最佳簇数。本书稍后将讨论交叉验证。请注意,上述代码是在 jupyter 笔记本中运行的。 119 | 120 | 在本书中,我们将使用 jupyter 做一些简单的事情,比如上面的例子和 绘图。对于本书中的大部分内容,我们将使用 python 脚本。您可以使用其他 IDE 因为结果都是一样的。 121 | 122 | MNIST 是一个有监督的分类问题,我们把它转换成一个无监督的问题,只是为了检查它是否能带来任何好的结果。如果我们使用分类算法,效果会更好。让我们在接下来的章节中一探究竟。 123 | -------------------------------------------------------------------------------- /docs/特征工程.md: -------------------------------------------------------------------------------- 1 | # 特征工程 2 | 3 | 特征工程是构建良好机器学习模型的最关键部分之一。如果我们拥有有用的特征,模型就会表现得更好。在许多情况下,您可以避免使用大型复杂模型,而使用具有关键工程特征的简单模型。我们必须牢记,只有当你对问题的领域有一定的了解,并且在很大程度上取决于相关数据时,才能以最佳方式完成特征工程。不过,您可以尝试使用一些通用技术,从几乎所有类型的数值变量和分类变量中创建特征。特征工程不仅仅是从数据中创建新特征,还包括不同类型的归一化和转换。 4 | 5 | 在有关分类特征的章节中,我们已经了解了结合不同分类变量的方法、如何将分类变量转换为计数、标签编码和使用嵌入。这些几乎都是利用分类变量设计特征的方法。因此,在本章中,我们的重点将仅限于数值变量以及数值变量和分类变量的组合。 6 | 7 | 让我们从最简单但应用最广泛的特征工程技术开始。假设你正在处理日期和时间数据。因此,我们有一个带有日期类型列的 pandas 数据帧。利用这一列,我们可以创建以下特征: 8 | 9 | - 年 10 | - 年中的周 11 | - 月 12 | - 星期 13 | - 周末 14 | - 小时 15 | - 还有更多 16 | 17 | 而使用pandas就可以非常容易地做到这一点。 18 | 19 | ```python 20 | # 添加'year'列,将 'datetime_column' 中的年份提取出来 21 | df.loc[:, 'year'] = df['datetime_column'].dt.year 22 | # 添加'weekofyear'列,将 'datetime_column' 中的周数提取出来 23 | df.loc[:, 'weekofyear'] = df['datetime_column'].dt.weekofyear 24 | # 添加'month'列,将 'datetime_column' 中的月份提取出来 25 | df.loc[:, 'month'] = df['datetime_column'].dt.month 26 | # 添加'dayofweek'列,将 'datetime_column' 中的星期几提取出来 27 | df.loc[:, 'dayofweek'] = df['datetime_column'].dt.dayofweek 28 | # 添加'weekend'列,判断当天是否为周末 29 | df.loc[:, 'weekend'] = (df.datetime_column.dt.weekday >=5).astype(int) 30 | # 添加 'hour' 列,将 'datetime_column' 中的小时提取出来 31 | df.loc[:, 'hour'] = df['datetime_column'].dt.hour 32 | ``` 33 | 34 | 因此,我们将使用日期时间列创建一系列新列。让我们来看看可以创建的一些示例功能。 35 | 36 | ```python 37 | import pandas as pd 38 | # 创建日期时间序列,包含了从 '2020-01-06' 到 '2020-01-10' 的日期时间点,时间间隔为10小时 39 | s = pd.date_range('2020-01-06', '2020-01-10', freq='10H').to_series() 40 | # 提取对应时间特征 41 | features = { 42 | "dayofweek": s.dt.dayofweek.values, 43 | "dayofyear": s.dt.dayofyear.values, 44 | "hour": s.dt.hour.values, 45 | "is_leap_year": s.dt.is_leap_year.values, 46 | "quarter": s.dt.quarter.values, 47 | "weekofyear": s.dt.weekofyear.values 48 | } 49 | ``` 50 | 51 | 这将从给定系列中生成一个特征字典。您可以将此应用于 pandas 数据中的任何日期时间列。这些是 pandas 提供的众多日期时间特征中的一部分。在处理时间序列数据时,日期时间特征非常重要,例如,在预测一家商店的销售额时,如果想在聚合特征上使用 xgboost 等模型,日期时间特征就非常重要。 52 | 53 | 假设我们有一个如下所示的数据: 54 | 55 | ![](figures/AAAMLP_page142_image.png) 56 | 57 |

图 1:包含分类和日期特征的样本数据

58 | 59 | 在图 1 中,我们可以看到有一个日期列,从中可以轻松提取年、月、季度等特征。然后,我们有一个 customer_id 列,该列有多个条目,因此一个客户会被看到很多次(截图中看不到)。每个日期和客户 ID 都有三个分类特征和一个数字特征。我们可以从中创建大量特征: 60 | - 客户最活跃的月份是几月 61 | - 某个客户的 cat1、cat2、cat3 的计数是多少 62 | - 某年某月某周某客户的 cat1、cat2、cat3 数量是多少? 63 | - 某个客户的 num1 平均值是多少? 64 | - 等等。 65 | 66 | 使用 pandas 中的聚合,可以很容易地创建类似的功能。让我们来看看如何实现。 67 | 68 | ```python 69 | def generate_features(df): 70 | df.loc[:, 'year'] = df['date'].dt.year 71 | df.loc[:, 'weekofyear'] = df['date'].dt.weekofyear 72 | df.loc[:, 'month'] = df['date'].dt.month 73 | df.loc[:, 'dayofweek'] = df['date'].dt.dayofweek 74 | df.loc[:, 'weekend'] = (df['date'].dt.weekday >=5).astype(int) 75 | aggs = {} 76 | # 对 'month' 列进行 nunique 和 mean 聚合 77 | aggs['month'] = ['nunique', 'mean'] 78 | # 对 'weekofyear' 列进行 nunique 和 mean 聚合 79 | aggs['weekofyear'] = ['nunique', 'mean'] 80 | # 对 'num1' 列进行 sum、max、min、mean 聚合 81 | aggs['num1'] = ['sum','max','min','mean'] 82 | # 对 'customer_id' 列进行 size 聚合 83 | aggs['customer_id'] = ['size'] 84 | # 对 'customer_id' 列进行 nunique 聚合 85 | aggs['customer_id'] = ['nunique'] 86 | # 对数据应用不同的聚合函数 87 | agg_df = df.groupby('customer_id').agg(aggs) 88 | # 重置索引 89 | agg_df = agg_df.reset_index() 90 | return agg_df 91 | ``` 92 | 93 | 请注意,在上述函数中,我们跳过了分类变量,但您可以像使用其他聚合变量一样使用它们。 94 | 95 | ![](figures/AAAMLP_page144_image.png) 96 | 97 |

图 2:总体特征和其他特征

98 | 99 | 现在,我们可以将图 2 中的数据与带有 customer_id 列的原始数据帧连接起来,开始训练模型。在这里,我们并不是要预测什么;我们只是在创建通用特征。不过,如果我们试图在这里预测什么,创建特征会更容易。 100 | 101 | 例如,有时在处理时间序列问题时,您可能需要的特征不是单个值,而是一系列值。 例如,客户在特定时间段内的交易。在这种情况下,我们会创建不同类型的特征,例如:使用数值特征时,在对分类列进行分组时,会得到类似于时间分布值列表的特征。在这种情况下,您可以创建一系列统计特征,例如 102 | 103 | - 平均值 104 | - 最大值 105 | - 最小值 106 | - 独特性 107 | - 偏斜 108 | - 峰度 109 | - Kstat 110 | - 百分位数 111 | - 定量 112 | - 峰值到峰值 113 | - 以及更多 114 | 115 | 这些可以使用简单的 numpy 函数创建,如下面的 python 代码段所示。 116 | 117 | ```python 118 | import numpy as np 119 | # 创建字典,用于存储不同的统计特征 120 | feature_dict = {} 121 | # 计算 x 中元素的平均值,并将结果存储在 feature_dict 中的 'mean' 键下 122 | feature_dict['mean'] = np.mean(x) 123 | # 计算 x 中元素的最大值,并将结果存储在 feature_dict 中的 'max' 键下 124 | feature_dict['max'] = np.max(x) 125 | # 计算 x 中元素的最小值,并将结果存储在 feature_dict 中的 'min' 键下 126 | feature_dict['min'] = np.min(x) 127 | # 计算 x 中元素的标准差,并将结果存储在 feature_dict 中的 'std' 键下 128 | feature_dict['std'] = np.std(x) 129 | # 计算 x 中元素的方差,并将结果存储在 feature_dict 中的 'var' 键下 130 | feature_dict['var'] = np.var(x) 131 | # 计算 x 中元素的差值,并将结果存储在 feature_dict 中的 'ptp' 键下 132 | feature_dict['ptp'] = np.ptp(x) 133 | # 计算 x 中元素的第10百分位数(即百分之10分位数),并将结果存储在 feature_dict 中的 'percentile_10' 键下 134 | feature_dict['percentile_10'] = np.percentile(x, 10) 135 | # 计算 x 中元素的第60百分位数,将结果存储在 feature_dict 中的 'percentile_60' 键下 136 | feature_dict['percentile_60'] = np.percentile(x, 60) 137 | # 计算 x 中元素的第90百分位数,将结果存储在 feature_dict 中的 'percentile_90' 键下 138 | feature_dict['percentile_90'] = np.percentile(x, 90) 139 | # 计算 x 中元素的5%分位数(即0.05分位数),将结果存储在 feature_dict 中的 'quantile_5' 键下 140 | feature_dict['quantile_5'] = np.quantile(x, 0.05) 141 | # 计算 x 中元素的95%分位数(即0.95分位数),将结果存储在 feature_dict 中的 'quantile_95' 键下 142 | feature_dict['quantile_95'] = np.quantile(x, 0.95) 143 | # 计算 x 中元素的99%分位数(即0.99分位数),将结果存储在 feature_dict 中的 'quantile_99' 键下 144 | feature_dict['quantile_99'] = np.quantile(x, 0.99) 145 | ``` 146 | 147 | 时间序列数据(数值列表)可以转换成许多特征。在这种情况下,一个名为 tsfresh 的 python 库非常有用。 148 | 149 | ```python 150 | from tsfresh.feature_extraction import feature_calculators as fc 151 | # 计算 x 数列的绝对能量(abs_energy),并将结果存储在 feature_dict 字典中的 'abs_energy' 键下 152 | feature_dict['abs_energy'] = fc.abs_energy(x) 153 | # 计算 x 数列中高于均值的数据点数量,将结果存储在 feature_dict 字典中的 'count_above_mean' 键下 154 | feature_dict['count_above_mean'] = fc.count_above_mean(x) 155 | # 计算 x 数列中低于均值的数据点数量,将结果存储在 feature_dict 字典中的 'count_below_mean' 键下 156 | feature_dict['count_below_mean'] = fc.count_below_mean(x) 157 | # 计算 x 数列的均值绝对变化(mean_abs_change),并将结果存储在 feature_dict 字典中的 'mean_abs_change' 键下 158 | feature_dict['mean_abs_change'] = fc.mean_abs_change(x) 159 | # 计算 x 数列的均值变化率(mean_change),并将结果存储在 feature_dict 字典中的 'mean_change' 键下 160 | feature_dict['mean_change'] = fc.mean_change(x) 161 | 162 | ``` 163 | 164 | 这还不是全部;tsfresh 提供了数百种特征和数十种不同特征的变体,你可以将它们用于基于时间序列(值列表)的特征。在上面的例子中,x 是一个值列表。但这还不是全部。您还可以为包含或不包含分类数据的数值数据创建许多其他特征。生成许多特征的一个简单方法就是创建一堆多项式特征。例如,从两个特征 "a "和 "b "生成的二级多项式特征包括 "a"、"b"、"ab"、"a^2 "和 "b^2"。 165 | 166 | ```python 167 | import numpy as np 168 | df = pd.DataFrame( 169 | np.random.rand(100, 2), 170 | columns=[f"f_{i}" for i in range(1, 3)]) 171 | ``` 172 | 173 | 如图 3 所示,它给出了一个数据表。 174 | 175 | ![](figures/AAAMLP_page146_image.png) 176 | 177 |

图 3:包含两个数字特征的随机数据表

178 | 179 | 我们可以使用 scikit-learn 的 PolynomialFeatures 创建两次多项式特征。 180 | 181 | ```python 182 | from sklearn import preprocessing 183 | # 指定多项式的次数为 2,不仅考虑交互项,不包括偏差(include_bias=False) 184 | pf = preprocessing.PolynomialFeatures(degree=2, 185 | interaction_only=False, 186 | include_bias=False 187 | ) 188 | # 拟合,创建多项式特征 189 | pf.fit(df) 190 | # 转换数据 191 | poly_feats = pf.transform(df) 192 | # 获取生成的多项式特征的数量 193 | num_feats = poly_feats.shape[1] 194 | # 为新生成的特征命名 195 | df_transformed = pd.DataFrame(poly_feats,columns=[f"f_{i}" for i in range(1, num_feats + 1)] ) 196 | ``` 197 | 198 | 这样就得到了一个数据表,如图 4 所示。 199 | 200 | ![](figures/AAAMLP_page147_image.png) 201 | 202 |

图 4:带有多项式特征的样本数据表

203 | 204 | 现在,我们创建了一些多项式特征。如果创建的是三次多项式特征,最终总共会有九个特征。特征的数量越多,多项式特征的数量也就越多,而且你还必须记住,如果数据集中有很多样本,那么创建这类特征就需要花费一些时间。 205 | 206 | ![](figures/AAAMLP_page148_image.png) 207 | 208 |

图 5:数字特征列的直方图

209 | 210 | 另一个有趣的功能是将数字转换为类别。这就是所谓的**分箱**。让我们看一下图 5,它显示了一个随机数字特征的样本直方图。我们在该图中使用了10个分箱,可以看到我们可以将数据分为10个部分。这可以使用 pandas 的 *cut* 函数来实现。 211 | 212 | ```python 213 | # 创建10个分箱 214 | df["f_bin_10"] = pd.cut(df["f_1"], bins=10, labels=False) 215 | # 创建100个分箱 216 | df["f_bin_100"] = pd.cut(df["f_1"], bins=100, labels=False) 217 | ``` 218 | 219 | 如图 6 所示,这将在数据帧中生成两个新特征。 220 | 221 | ![](figures/AAAMLP_page148_image_1.png) 222 | 223 |

图 6:数值特征分箱

224 | 225 | 当你进行分类时,可以同时使用分箱和原始特征。我们将在本章后半部分学习更多关于选择特征的知识。分箱还可以将数字特征视为分类特征。 226 | 227 | 另一种可以从数值特征中创建的有趣特征类型是对数变换。请看图 7 中的特征 f_3。 228 | 229 | 与其他方差较小的特征相比(假设如此),f_3 是一种方差非常大的特殊特征。因此,我们希望降低这一列的方差,这可以通过对数变换来实现。 230 | 231 | f_3 列的值范围为 0 到 10000,直方图如图 8 所示。 232 | 233 | ![](figures/AAAMLP_page149_image_2.png) 234 | 235 |

图 8:特征 f_3 的直方图

236 | 237 | 我们可以对这一列应用 log(1 + x) 来减少其方差。图 9 显示了应用对数变换后直方图的变化。 238 | 239 | ![](figures/AAAMLP_page150_image.png) 240 | 241 |

图 9:应用对数变换后的 f_3 直方图

242 | 243 | 让我们来看看不使用对数变换和使用对数变换的方差。 244 | 245 | ```python 246 | In [X]: df.f_3.var() 247 | Out[X]: 8077265.875858586 248 | In [X]: df.f_3.apply(lambda x: np.log(1 + x)).var() 249 | Out[X]: 0.6058771732119975 250 | ``` 251 | 252 | 有时,也可以用指数来代替对数。一种非常有趣的情况是,您使用基于对数的评估指标,例如 RMSLE。在这种情况下,您可以在对数变换的目标上进行训练,然后在预测时使用指数值转换回原始值。这将有助于针对指标优化模型。 253 | 254 | 大多数情况下,这类数字特征都是基于直觉创建的。没有公式可循。如果您从事的是某一行业,您将创建特定行业的特征。 255 | 256 | 在处理分类变量和数值变量时,可能会遇到缺失值。在上一章中,我们介绍了一些处理分类特征中缺失值的方法,但还有更多方法可以处理缺失值/NaN 值。这也被视为特征工程。 257 | 258 | 如果在分类特征中遇到缺失值,就将其视为一个新的类别!这样做虽然简单,但(几乎)总是有效的! 259 | 260 | 在数值数据中填补缺失值的一种方法是选择一个在特定特征中没有出现的值,然后用它来填补。例如,假设特征中没有 0。这是其中一种方法,但可能不是最有效的。对于数值数据来说,比填充 0 更有效的方法之一是使用平均值进行填充。您也可以尝试使用该特征所有值的中位数来填充,或者使用最常见的值来填充缺失值。这样做的方法有很多。 261 | 262 | 填补缺失值的一种高级方法是使用 **K 近邻法**。 您可以选择一个有缺失值的样本,然后利用某种距离度量(例如欧氏距离)找到最近的邻居。然后取所有近邻的平均值来填补缺失值。您可以使用 KNN 来填补这样的缺失值。 263 | 264 | ![](figures/AAAMLP_page151_image.png) 265 | 266 |

图 10:有缺失值的二维数组

267 | 268 | 让我们看看 KNN 是如何处理图 10 所示的缺失值矩阵的。 269 | 270 | ```python 271 | import numpy as np 272 | from sklearn import impute 273 | # 生成维度为 (10, 6) 的随机整数矩阵 X,数值范围在 1 到 14 之间 274 | X = np.random.randint(1, 15, (10, 6)) 275 | # 数据类型转换为 float 276 | X = X.astype(float) 277 | # 在矩阵 X 中随机选择 10 个位置,将这些位置的元素设置为 NaN(缺失值) 278 | X.ravel()[np.random.choice(X.size, 10, replace=False)] = np.nan 279 | # 创建一个 KNNImputer 对象 knn_imputer,指定邻居数量为 2 280 | knn_imputer = impute.KNNImputer(n_neighbors=2) 281 | # # 使用 knn_imputer 对矩阵 X 进行拟合和转换,用 K-最近邻方法填补缺失值 282 | knn_imputer.fit_transform(X) 283 | ``` 284 | 285 | 如图 11 所示,它填充了上述矩阵。 286 | 287 | ![](figures/AAAMLP_page152_image.png) 288 | 289 |

图 11:KNN估算的数值

290 | 291 | 另一种弥补列缺失值的方法是训练回归模型,试图根据其他列预测某列的缺失值。因此,您可以从有缺失值的一列开始,将这一列作为无缺失值回归模型的目标列。现在,您可以使用所有其他列,对相关列中没有缺失值的样本进行模型训练,然后尝试预测之前删除的样本的目标列(同一列)。这样,基于模型的估算就会更加稳健。 292 | 293 | 请务必记住,对于基于树的模型,没有必要进行数值归一化,因为它们可以自行处理。 294 | 295 | 到目前为止,我所展示的只是创建一般特征的一些方法。现在,假设您正在处理一个预测不同商品(每周或每月)商店销售额的问题。您有商品,也有商店 ID。因此,您可以创建每个商店的商品等特征。现在,这是上文没有讨论的特征之一。这类特征不能一概而论,完全来自于领域、数据和业务知识。查看数据,找出适合的特征,然后创建相应的特征。如果您使用的是逻辑回归等线性模型或 SVM 等模型,请务必记住对特征进行缩放或归一化处理。基于树的模型无需对特征进行任何归一化处理即可正常工作。 -------------------------------------------------------------------------------- /docs/特征选择.md: -------------------------------------------------------------------------------- 1 | # 特征选择 2 | 3 | 当你创建了成千上万个特征后,就该从中挑选出几个了。但是,我们绝不应该创建成百上千个无用的特征。特征过多会带来一个众所周知的问题,即 "维度诅咒"。如果你有很多特征,你也必须有很多训练样本来捕捉所有特征。什么是 "大量 "并没有正确的定义,这需要您通过正确验证您的模型和检查训练模型所需的时间来确定。 4 | 5 | 选择特征的最简单方法是**删除方差非常小的特征**。如果特征的方差非常小(即非常接近于 0),它们就接近于常量,因此根本不会给任何模型增加任何价值。最好的办法就是去掉它们,从而降低复杂度。请注意,方差也取决于数据的缩放。 Scikit-learn 的 VarianceThreshold 实现了这一点。 6 | 7 | ```python 8 | from sklearn.feature_selection import VarianceThreshold 9 | data = ... 10 | # 创建 VarianceThreshold 对象 var_thresh,指定方差阈值为 0.1 11 | var_thresh = VarianceThreshold(threshold=0.1) 12 | # 使用 var_thresh 对数据 data 进行拟合和变换,将方差低于阈值的特征移除 13 | transformed_data = var_thresh.fit_transform(data) 14 | ``` 15 | 16 | 我们还可以删除相关性较高的特征。要计算不同数字特征之间的相关性,可以使用皮尔逊相关性。 17 | 18 | ```python 19 | import pandas as pd 20 | from sklearn.datasets import fetch_california_housing 21 | # 加载数据 22 | data = fetch_california_housing() 23 | # 从数据集中提取特征矩阵 X 24 | X = data["data"] 25 | # 从数据集中提取特征的列名 26 | col_names = data["feature_names"] 27 | # 从数据集中提取目标变量 y 28 | y = data["target"] 29 | df = pd.DataFrame(X, columns=col_names) 30 | # 添加 MedInc_Sqrt 列,是 MedInc 列中每个元素进行平方根运算的结果 31 | df.loc[:, "MedInc_Sqrt"] = df.MedInc.apply(np.sqrt) 32 | # 计算皮尔逊相关性矩阵 33 | df.corr() 34 | ``` 35 | 36 | 得出相关矩阵,如图 1 所示。 37 | 38 | ![](figures/AAAMLP_page155_image.png) 39 | 40 |

图 1:皮尔逊相关矩阵样本

41 | 42 | 我们看到,MedInc_Sqrt 与 MedInc 的相关性非常高。因此,我们可以删除其中一个特征。 43 | 44 | 现在我们可以转向一些**单变量特征选择方法**。单变量特征选择只不过是针对给定目标对每个特征进行评分。**互信息**、**方差分析 F 检验和卡方检验** 是一些最常用的单变量特征选择方法。在 scikit- learn 中,有两种方法可以使用这些方法。 45 | - SelectKBest:保留得分最高的 k 个特征 46 | - SelectPercentile:保留用户指定百分比内的顶级特征。 47 | 48 | 必须注意的是,只有非负数据才能使用卡方检验。在自然语言处理中,当我们有一些单词或基于 tf-idf 的特征时,这是一种特别有用的特征选择技术。最好为单变量特征选择创建一个包装器,几乎可以用于任何新问题。 49 | 50 | ```python 51 | from sklearn.feature_selection import chi2 52 | from sklearn.feature_selection import f_classif 53 | from sklearn.feature_selection import f_regression 54 | from sklearn.feature_selection import mutual_info_classif 55 | from sklearn.feature_selection import mutual_info_regression 56 | from sklearn.feature_selection import SelectKBest 57 | from sklearn.feature_selection import SelectPercentile 58 | class UnivariateFeatureSelction: 59 | def __init__(self, n_features, problem_type, scoring): 60 | # 若问题类型是分类问题 61 | if problem_type == "classification": 62 | # 创建字典 valid_scoring ,包含各种特征重要性衡量方式 63 | valid_scoring = { 64 | "f_classif": f_classif, 65 | "chi2": chi2, 66 | "mutual_info_classif": mutual_info_classif 67 | } 68 | # 若问题类型是回归问题 69 | else: 70 | # 创建字典 valid_scoring,包含各种特征重要性衡量方式 71 | valid_scoring = { 72 | "f_regression": f_regression, 73 | "mutual_info_regression": mutual_info_regression 74 | } 75 | # 检查特征重要性方式是否在字典中 76 | if scoring not in valid_scoring: 77 | raise Exception("Invalid scoring function") 78 | 79 | # 检查 n_features 的类型,如果是整数,则使用 SelectKBest 进行特征选择 80 | if isinstance(n_features, int): 81 | self.selection = SelectKBest( 82 | valid_scoring[scoring], 83 | k=n_features 84 | ) 85 | # 如果 n_features 是浮点数,则使用 SelectPercentile 进行特征选择 86 | elif isinstance(n_features, float): 87 | self.selection = SelectPercentile( 88 | valid_scoring[scoring], 89 | percentile=int(n_features * 100) 90 | ) 91 | # 如果 n_features 类型无效,引发异常 92 | else: 93 | raise Exception("Invalid type of feature") 94 | 95 | # 定义 fit 方法,用于拟合特征选择器 96 | def fit(self, X, y): 97 | return self.selection.fit(X, y) 98 | 99 | # 定义 transform 方法,用于对数据进行特征选择转换 100 | def transform(self, X): 101 | return self.selection.transform(X) 102 | 103 | # 定义 fit_transform 方法,用于拟合特征选择器并同时进行特征选择转换 104 | def fit_transform(self, X, y): 105 | return self.selection.fit_transform(X, y) 106 | ``` 107 | 108 | 使用该类非常简单。 109 | 110 | ```python 111 | # 实例化特征选择器,保留前10%的特征,回归问题,使用f_regression衡量特征重要性 112 | ufs = UnivariateFeatureSelction(n_features=0.1, 113 | problem_type="regression", 114 | scoring="f_regression" 115 | ) 116 | # 拟合特征选择器 117 | ufs.fit(X, y) 118 | # 特征转换 119 | X_transformed = ufs.transform(X) 120 | ``` 121 | 122 | 123 | 124 | 这样就能满足大部分单变量特征选择的需求。请注意,创建较少而重要的特征通常比创建数以百计的特征要好。单变量特征选择不一定总是表现良好。大多数情况下,人们更喜欢使用机器学习模型进行特征选择。让我们来看看如何做到这一点。 125 | 126 | 使用模型进行特征选择的最简单形式被称为贪婪特征选择。在贪婪特征选择中,第一步是选择一个模型。第二步是选择损失/评分函数。第三步也是最后一步是反复评估每个特征,如果能提高损失/评分,就将其添加到 "好 "特征列表中。没有比这更简单的了。但你必须记住,这被称为贪婪特征选择是有原因的。这种特征选择过程在每次评估特征时都会适合给定的模型。这种方法的计算成本非常高。完成这种特征选择也需要大量时间。如果不正确使用这种特征选择,甚至会导致模型过度拟合。 127 | 128 | 让我们来看看它是如何实现的。 129 | 130 | ```python 131 | import pandas as pd 132 | from sklearn import linear_model 133 | from sklearn import metrics 134 | from sklearn.datasets import make_classification 135 | class GreedyFeatureSelection: 136 | 137 | # 定义评估分数的方法,用于评估模型性能 138 | def evaluate_score(self, X, y): 139 | # 逻辑回归模型 140 | model = linear_model.LogisticRegression() 141 | # 训练模型 142 | model.fit(X, y) 143 | # 预测概率值 144 | predictions = model.predict_proba(X)[:, 1] 145 | # 计算 AUC 分数 146 | auc = metrics.roc_auc_score(y, predictions) 147 | return auc 148 | 149 | # 特征选择函数 150 | def _feature_selection(self, X, y): 151 | # 初始化空列表,用于存储最佳特征和最佳分数 152 | good_features = [] 153 | best_scores = [] 154 | # 获取特征数量 155 | num_features = X.shape[1] 156 | 157 | # 开始特征选择的循环 158 | while True: 159 | this_feature = None 160 | best_score = 0 161 | 162 | # 遍历每个特征 163 | for feature in range(num_features): 164 | if feature in good_features: 165 | continue 166 | selected_features = good_features + [feature] 167 | xtrain = X[:, selected_features] 168 | score = self.evaluate_score(xtrain, y) 169 | 170 | # 如果当前特征的得分优于之前的最佳得分,则更新 171 | if score > best_score: 172 | this_feature = feature 173 | best_score = score 174 | 175 | # 若找到了新的最佳特征 176 | if this_feature != None: 177 | # 特征添加到 good_features 列表 178 | good_features.append(this_feature) 179 | # 得分添加到 best_scores 列表 180 | best_scores.append(best_score) 181 | # 如果 best_scores 列表长度大于2,并且最后两个得分相比较差,则结束循环 182 | if len(best_scores) > 2: 183 | if best_scores[-1] < best_scores[-2]: 184 | break 185 | # 返回最佳特征的得分列表和最佳特征列表 186 | return best_scores[:-1], good_features[:-1] 187 | 188 | # 定义类的调用方法,用于执行特征选择 189 | def __call__(self, X, y): 190 | scores, features = self._feature_selection(X, y) 191 | return X[:, features], scores 192 | 193 | if __name__ == "__main__": 194 | # 生成一个示例的分类数据集 X 和标签 y 195 | X, y = make_classification(n_samples=1000, n_features=100) 196 | # 实例化 GreedyFeatureSelection 类,并使用 __call__ 方法进行特征选择 197 | X_transformed, scores = GreedyFeatureSelection()(X, y) 198 | ``` 199 | 200 | 这种贪婪特征选择方法会返回分数和特征索引列表。图 2 显示了在每次迭代中增加一个新特征后,分数是如何提高的。我们可以看到,在某一点之后,我们就无法提高分数了,这就是我们停止的地方。 201 | 202 | 另一种贪婪的方法被称为递归特征消除法(RFE)。在前一种方法中,我们从一个特征开始,然后不断添加新的特征,但在 RFE 中,我们从所有特征开始,在每次迭代中不断去除一个对给定模型提供最小值的特征。但我们如何知道哪个特征的价值最小呢?如果我们使用线性支持向量机(SVM)或逻辑回归等模型,我们会为每个特征得到一个系数,该系数决定了特征的重要性。而对于任何基于树的模型,我们得到的是特征重要性,而不是系数。在每次迭代中,我们都可以剔除最不重要的特征,直到达到所需的特征数量为止。因此,我们可以决定要保留多少特征。 203 | 204 | ![](figures/AAAMLP_page161_image.png) 205 | 206 |

图 2:增加新特征后,贪婪特征选择的 AUC 分数如何变化

207 | 208 | 当我们进行递归特征剔除时,在每次迭代中,我们都会剔除特征重要性较高的特征或系数接近 0 的特征。请记住,当你使用逻辑回归这样的模型进行二元分类时,如果特征对正分类很重要,其系数就会更正,而如果特征对负分类很重要,其系数就会更负。修改我们的贪婪特征选择类,创建一个新的递归特征消除类非常容易,但 scikit-learn 也提供了 RFE。下面的示例展示了一个简单的用法。 209 | 210 | ```python 211 | import pandas as pd 212 | from sklearn.feature_selection import RFE 213 | from sklearn.linear_model import LinearRegression 214 | from sklearn.datasets import fetch_california_housing 215 | data = fetch_california_housing() 216 | X = data["data"] 217 | col_names = data["feature_names"] 218 | y = data["target"] 219 | model = LinearRegression() 220 | # 创建 RFE(递归特征消除),指定模型为线性回归模型,要选择的特征数量为 3 221 | rfe = RFE( 222 | estimator=model, 223 | n_features_to_select=3 224 | ) 225 | # 训练模型 226 | rfe.fit(X, y) 227 | # 使用 RFE 选择的特征进行数据转换 228 | X_transformed = rfe.transform(X) 229 | ``` 230 | 231 | 我们看到了从模型中选择特征的两种不同的贪婪方法。但也可以根据数据拟合模型,然后通过特征系数或特征的重要性从模型中选择特征。如果使用系数,则可以选择一个阈值,如果系数高于该阈值,则可以保留该特征,否则将其剔除。 232 | 233 | 让我们看看如何从随机森林这样的模型中获取特征重要性。 234 | 235 | ```python 236 | import pandas as pd 237 | from sklearn.datasets import load_diabetes 238 | from sklearn.ensemble import RandomForestRegressor 239 | data = load_diabetes() 240 | X = data["data"] 241 | col_names = data["feature_names"] 242 | y = data["target"] 243 | # 实例化随机森林模型 244 | model = RandomForestRegressor() 245 | # 拟合模型 246 | model.fit(X, y) 247 | ``` 248 | 249 | 随机森林(或任何模型)的特征重要性可按如下方式绘制。 250 | 251 | ```python 252 | # 获取特征重要性 253 | importances = model.feature_importances_ 254 | # 降序排列 255 | idxs = np.argsort(importances) 256 | # 设定标题 257 | plt.title('Feature Importances') 258 | # 创建直方图 259 | plt.barh(range(len(idxs)), importances[idxs], align='center') 260 | # y轴标签 261 | plt.yticks(range(len(idxs)), [col_names[i] for i in idxs]) 262 | # x轴标签 263 | plt.xlabel('Random Forest Feature Importance') 264 | plt.show() 265 | ``` 266 | 267 | 结果如图 3 所示。 268 | 269 | ![](figures/AAAMLP_page163_image.png) 270 | 271 |

图 3:特征重要性图

272 | 273 | 从模型中选择最佳特征并不是什么新鲜事。您可以从一个模型中选择特征,然后使用另一个模型进行训练。例如,你可以使用逻辑回归系数来选择特征,然后使用随机森林(Random Forest)对所选特征进行模型训练。Scikit-learn 还提供了 SelectFromModel 类,可以帮助你直接从给定的模型中选择特征。您还可以根据需要指定系数或特征重要性的阈值,以及要选择的特征的最大数量。 274 | 275 | 请看下面的代码段,我们使用 SelectFromModel 中的默认参数来选择特征。 276 | 277 | ```python 278 | import pandas as pd 279 | from sklearn.datasets import load_diabetes 280 | from sklearn.ensemble import RandomForestRegressor 281 | from sklearn.feature_selection import SelectFromModel 282 | data = load_diabetes() 283 | X = data["data"] 284 | col_names = data["feature_names"] 285 | y = data["target"] 286 | # 创建随机森林模型回归模型 287 | model = RandomForestRegressor() 288 | # 创建 SelectFromModel 对象 sfm,使用随机森林模型作为估算器 289 | sfm = SelectFromModel(estimator=model) 290 | # 使用 sfm 对特征矩阵 X 和目标变量 y 进行特征选择 291 | X_transformed = sfm.fit_transform(X, y) 292 | # 获取经过特征选择后的特征掩码(True 表示特征被选择,False 表示特征未被选择) 293 | support = sfm.get_support() 294 | # 打印被选择的特征列名 295 | print([x for x, y in zip(col_names, support) if y == True ]) 296 | ``` 297 | 298 | 上面程序打印结果: ['bmi','s5']。我们再看图 3,就会发现这是最重要的两个特征。因此,我们也可以直接从随机森林提供的特征重要性中进行选择。我们还缺少一件事,那就是使用 **L1(Lasso)惩罚模型**进行特征选择。当我们使用 L1 惩罚进行正则化时,大部分系数都将为 0(或接近 0),因此我们要选择系数不为 0 的特征。只需将模型选择片段中的随机森林替换为支持 L1 惩罚的模型(如 lasso 回归)即可。所有基于树的模型都提供特征重要性,因此本章中展示的所有基于模型的片段都可用于 XGBoost、LightGBM 或 CatBoost。特征重要性函数的名称可能不同,产生结果的格式也可能不同,但用法是一样的。最后,在进行特征选择时必须小心谨慎。在训练数据上选择特征,并在验证数据上验证模型,以便在不过度拟合模型的情况下正确选择特征。 -------------------------------------------------------------------------------- /docs/组合和堆叠方法.md: -------------------------------------------------------------------------------- 1 | # 组合和堆叠方法 2 | 3 | 听到上面两个词,我们首先想到的就是在线(online)/离线(offline)机器学习竞赛。几年前是这样,但现在随着计算能力的进步和虚拟实例的廉价,人们甚至开始在行业中使用组合模型(ensemble models)。例如,部署多个神经网络并实时为它们提供服务非常容易,响应时间小于 500 毫秒。有时,一个庞大的神经网络或大型模型也可以被其他几个模型取代,这些模型体积小,性能与大型模型相似,速度却快一倍。如果是这种情况,你会选择哪个(些)模型呢?我个人更倾向于选择多个小机型,它们速度更快,性能与大机型和慢机型相同。请记住,较小的型号也更容易和更快地进行调整。 4 | 5 | 组合(ensembling)不过是不同模型的组合。模型可以通过预测/概率进行组合。组合模型最简单的方法就是求平均值。 6 | $$ 7 | Ensemble Probabilities = (M1\_proba + M2\_proba + ... + Mn\_Proba)/n 8 | $$ 9 | 这是最简单也是最有效的组合模型的方法。在简单平均法中,所有模型的权重都是相等的。无论采用哪种组合方法,您都应该牢记一点,那就是您应该始终将不同模型的预测/概率组合在一起。简单地说,组合相关性不高的模型比组合相关性很高的模型效果更好。 10 | 11 | 如果没有概率,也可以组合预测。最简单的方法就是投票。假设我们正在进行多类分类,有三个类别: 0、1 和 2。 12 | 13 | [0, 0, 1] : 最高票数: 0 14 | 15 | [0, 1, 2] : 最高票级: 无(随机选择一个) 16 | 17 | [2, 2, 2] : 最高票数: 2 18 | 19 | 以下简单函数可以完成这些简单操作。 20 | 21 | ```python 22 | import numpy as np 23 | def mean_predictions(probas): 24 | # 计算第二个维度(列)每行平均值 25 | return np.mean(probas, axis=1) 26 | def max_voting(preds): 27 | # 沿着第二个维度(列)查找每行中最大值的索引 28 | idxs = np.argmax(preds, axis=1) 29 | # 根据索引取出每行中最大值对应的元素 30 | return np.take_along_axis(preds, idxs[:, None], axis=1) 31 | ``` 32 | 33 | 请注意,probas 的每一列都只有一个概率(即二元分类,通常为类别 1)。因此,每一列都是一个新模型。同样,对于 preds,每一列都是来自不同模型的预测值。这两个函数都假设了一个 2 维 numpy 数组。您可以根据自己的需求对其进行修改。例如,您可能有一个 2 维数组,其中包含每个模型的概率。在这种情况下,函数会有一些变化。 另一种组合多个模型的方法是通过它们的**概率排序**。当相关指标是曲线下面积(AUC)时,这种组合方式非常有效,因为 AUC 就是对样本进行排序。 34 | 35 | ```python 36 | def rank_mean(probas): 37 | # 创建空列表ranked存储每个类别概率值排名 38 | ranked = [] 39 | # 遍历概率值每一列(每个类别的概率值) 40 | for i in range(probas.shape[1]): 41 | # 当前列概率值排名,rank_data是排名结果 42 | rank_data = stats.rankdata(probas[:, i]) 43 | # 将当前列排名结果添加到ranked列表中 44 | ranked.append(rank_data) 45 | # 将ranked列表中排名结果按列堆叠,形成二维数组 46 | ranked = np.column_stack(ranked) 47 | # 沿着第二个维度(列)计算样本排名平均值 48 | return np.mean(ranked, axis=1) 49 | ``` 50 | 51 | 请注意,在 scipy 的 rankdata 中,等级从 1 开始。 52 | 53 | 为什么这类集合有效?让我们看看图 1。 54 | 55 | ![](figures/AAAMLP_page273_image.png) 56 | 57 |

图 1:三人猜大象的身高

58 | 59 | 图 1 显示,如果有三个人在猜大象的高度,那么原始高度将非常接近三个人猜测的平均值。我们假设这些人都能猜到非常接近大象原来的高度。接近估计值意味着误差,但如果我们将三个预测值平均,就能将误差降到最低。这就是多个模型平均的主要思想。 60 | $$ 61 | Final\ Probabilities = w_1 \times M1\_proba + w_2 \times M2\_proba + \cdots + w_n \times Mn\_proba 62 | $$ 63 | 其中$(w_1 + w_2 + w_3 + \cdots + w_n)=1.0$ 64 | 65 | 例如,如果你有一个 AUC 非常高的随机森林模型和一个 AUC 稍低的逻辑回归模型,你可以把它们结合起来,随机森林模型占 70%,逻辑回归模型占 30%。那么,我是如何得出这些数字的呢?让我们再添加一个模型,假设现在我们也有一个 xgboost 模型,它的 AUC 比随机森林高。现在,我将把它们结合起来,xgboost:随机森林:逻辑回归的比例为 3:2:1。很简单吧?得出这些数字易如反掌。让我们看看是如何做到的。 66 | 67 | 假定我们有三只猴子,三只旋钮的数值在 0 和 1 之间。这些猴子转动旋钮,我们计算它们每转到一个数值时的 AUC 分数。最终,猴子们会找到一个能给出最佳 AUC 的组合。没错,这就是随机搜索!在进行这类搜索之前,你必须记住两个最重要的组合规则。 68 | 69 | 组合的第一条规则是,在开始合奏之前,一定要先创建折叠。 70 | 71 | 组合的第二条规则是,在开始合奏之前,一定要先创建折叠。 72 | 73 | 是的。这是最重要的两条规则。第一步是创建折叠。为了简单起见,假设我们将数据分为两部分:折叠 1 和折叠 2。请注意,这样做只是为了简化解释。在实际应用中,您应该创建更多的折叠。 74 | 75 | 现在,我们在折叠 1 上训练随机森林模型、逻辑回归模型和 xgboost 模型,并在折叠 2 上进行预测。之后,我们在折叠 2 上从头开始训练模型,并在折叠 1 上进行预测。这样,我们就为所有训练数据创建了预测结果。现在,为了合并这些模型,我们将折叠 1 和折叠 1 的所有预测数据合并在一起,然后创建一个优化函数,试图找到最佳权重,以便针对折叠 2 的目标最小化误差或最大化 AUC。因此,我们是用三个模型的预测概率在折叠 1 上训练一个优化模型,然后在折叠 2 上对其进行评估。让我们先来看看我们可以用来找到多个模型的最佳权重,以优化 AUC(或任何类型的预测指标组合)的类。 76 | 77 | ```python 78 | import numpy as np 79 | from functools import partial 80 | from scipy.optimize import fmin 81 | from sklearn import metrics 82 | 83 | class OptimizeAUC: 84 | def __init__(self): 85 | # 初始化系数 86 | self.coef_ = 0 87 | 88 | def _auc(self, coef, X, y): 89 | # 对输入数据乘以系数 90 | x_coef = X * coef 91 | # 计算每个样本预测值 92 | predictions = np.sum(x_coef, axis=1) 93 | # 计算AUC分数 94 | auc_score = metrics.roc_auc_score(y, predictions) 95 | # 返回负AUC以便最小化 96 | return -1.0 * auc_score 97 | 98 | def fit(self, X, y): 99 | # 创建带有部分参数的目标函数 100 | loss_partial = partial(self._auc, X=X, y=y) 101 | # 初始化系数 102 | initial_coef = np.random.dirichlet(np.ones(X.shape[1]), size=1) 103 | # 使用fmin函数优化AUC目标函数,找到最优系数 104 | self.coef_ = fmin(loss_partial, initial_coef, disp=True) 105 | 106 | def predict(self, X): 107 | # 对输入数据乘以训练好的系数 108 | x_coef = X * self.coef_ 109 | # 计算每个样本预测值 110 | predictions = np.sum(x_coef, axis=1) 111 | # 返回预测结果 112 | return predictions 113 | ``` 114 | 115 | 让我们来看看如何使用它,并将其与简单平均法进行比较。 116 | 117 | ```python 118 | import xgboost as xgb 119 | from sklearn.datasets import make_classification 120 | from sklearn import ensemble 121 | from sklearn import linear_model 122 | from sklearn import metrics 123 | from sklearn import model_selection 124 | 125 | # 生成一个分类数据集 126 | X, y = make_classification(n_samples=10000, n_features=25) 127 | 128 | # 划分数据集为两个交叉验证折叠 129 | xfold1, xfold2, yfold1, yfold2 = model_selection.train_test_split(X, 130 | y, 131 | test_size=0.5, 132 | stratify=y) 133 | # 初始化三个不同的分类器 134 | logreg = linear_model.LogisticRegression() 135 | rf = ensemble.RandomForestClassifier() 136 | xgbc = xgb.XGBClassifier() 137 | 138 | # 使用第一个折叠数据集训练分类器 139 | logreg.fit(xfold1, yfold1) 140 | rf.fit(xfold1, yfold1) 141 | xgbc.fit(xfold1, yfold1) 142 | 143 | # 对第二个折叠数据集进行预测 144 | pred_logreg = logreg.predict_proba(xfold2)[:, 1] 145 | pred_rf = rf.predict_proba(xfold2)[:, 1] 146 | pred_xgbc = xgbc.predict_proba(xfold2)[:, 1] 147 | 148 | # 计算平均预测结果 149 | avg_pred = (pred_logreg + pred_rf + pred_xgbc) / 3 150 | fold2_preds = np.column_stack((pred_logreg, pred_rf, pred_xgbc, avg_pred)) 151 | 152 | # 计算每个模型的AUC分数并打印 153 | aucs_fold2 = [] 154 | for i in range(fold2_preds.shape[1]): 155 | auc = metrics.roc_auc_score(yfold2, fold2_preds[:, i]) 156 | aucs_fold2.append(auc) 157 | print(f"Fold-2: LR AUC = {aucs_fold2[0]}") 158 | print(f"Fold-2: RF AUC = {aucs_fold2[1]}") 159 | print(f"Fold-2: XGB AUC = {aucs_fold2[2]}") 160 | print(f"Fold-2: Average Pred AUC = {aucs_fold2[3]}") 161 | 162 | # 重新初始化分类器 163 | logreg = linear_model.LogisticRegression() 164 | rf = ensemble.RandomForestClassifier() 165 | xgbc = xgb.XGBClassifier() 166 | 167 | # 使用第二个折叠数据集训练分类器 168 | logreg.fit(xfold2, yfold2) 169 | rf.fit(xfold2, yfold2) 170 | xgbc.fit(xfold2, yfold2) 171 | 172 | # 对第一个折叠数据集进行预测 173 | pred_logreg = logreg.predict_proba(xfold1)[:, 1] 174 | pred_rf = rf.predict_proba(xfold1)[:, 1] 175 | pred_xgbc = xgbc.predict_proba(xfold1)[:, 1] 176 | 177 | # 计算平均预测结果 178 | avg_pred = (pred_logreg + pred_rf + pred_xgbc) / 3 179 | fold1_preds = np.column_stack((pred_logreg, pred_rf, pred_xgbc, avg_pred)) 180 | 181 | # 计算每个模型的AUC分数并打印 182 | aucs_fold1 = [] 183 | for i in range(fold1_preds.shape[1]): 184 | auc = metrics.roc_auc_score(yfold1, fold1_preds[:, i]) 185 | aucs_fold1.append(auc) 186 | print(f"Fold-1: LR AUC = {aucs_fold1[0]}") 187 | print(f"Fold-1: RF AUC = {aucs_fold1[1]}") 188 | print(f"Fold-1: XGB AUC = {aucs_fold1[2]}") 189 | print(f"Fold-1: Average prediction AUC = {aucs_fold1[3]}") 190 | 191 | # 初始化AUC优化器 192 | opt = OptimizeAUC() 193 | # 使用第一个折叠数据集的预测结果来训练优化器 194 | opt.fit(fold1_preds[:, :-1], yfold1) 195 | # 使用优化器对第二个折叠数据集的预测结果进行优化 196 | opt_preds_fold2 = opt.predict(fold2_preds[:, :-1]) 197 | auc = metrics.roc_auc_score(yfold2, opt_preds_fold2) 198 | print(f"Optimized AUC, Fold 2 = {auc}") 199 | print(f"Coefficients = {opt.coef_}") 200 | 201 | # 初始化AUC优化器 202 | opt = OptimizeAUC() 203 | # 使用第二个折叠数据集的预测结果来 204 | opt.fit(fold2_preds[:, :-1], yfold2) 205 | # 使用优化器对第一个折叠数据集的预测结果进行优化 206 | opt_preds_fold1 = opt.predict(fold1_preds[:, :-1]) 207 | auc = metrics.roc_auc_score(yfold1, opt_preds_fold1) 208 | print(f"Optimized AUC, Fold 1 = {auc}") 209 | print(f"Coefficients = {opt.coef_}") 210 | ``` 211 | 212 | 让我们看一下输出: 213 | 214 | ```python 215 | ❯ python auc_opt.py 216 | Fold-2: LR AUC = 0.9145446769443348 217 | Fold-2: RF AUC = 0.9269918948683287 218 | Fold-2: XGB AUC = 0.9302436595508696 219 | Fold-2: Average Pred AUC = 0.927701495890154 220 | Fold-1: LR AUC = 0.9050872233256017 221 | Fold-1: RF AUC = 0.9179382818311258 222 | Fold-1: XGB AUC = 0.9195837242005629 223 | Fold-1: Average prediction AUC = 0.9189669233123695 224 | Optimization terminated successfully. 225 | Current function value: -0.920643 226 | Iterations: 50 227 | Function evaluations: 109 228 | Optimized AUC, Fold 2 = 0.9305386199756128 229 | Coefficients = [-0.00188194 0.19328336 0.35891836] 230 | Optimization terminated successfully. 231 | Current function value: -0.931232 232 | Iterations: 56 233 | Function evaluations: 113 234 | Optimized AUC, Fold 1 = 0.9192523637234037 235 | Coefficients = [-0.15655124 0.22393151 0.58711366] 236 | ``` 237 | 238 | 我们看到,平均值更好,但使用优化器找到阈值更好!有时,平均值是最好的选择。正如你所看到的,系数加起来并没有达到 1.0,但这没关系,因为我们要处理的是 AUC,而 AUC 只关心等级。 239 | 240 | 即使随机森林也是一个集合模型。随机森林只是许多简单决策树的组合。随机森林属于集合模型的一种,也就是俗称的**"bagging"**。在袋集模型中,我们创建小数据子集并训练多个简单模型。最终结果由所有这些小模型的预测结果(如平均值)组合而成。 241 | 242 | 我们使用的 xgboost 模型也是一个集合模型。所有梯度提升模型都是集合模型,统称为**提升模型(boosting models)**。提升模型的工作原理与装袋模型类似,不同之处在于提升模型中的连续模型是根据误差残差训练的,并倾向于最小化前面模型的误差。这样,提升模型就能完美地学习数据,因此容易出现过拟合。 243 | 244 | 到目前为止,我们看到的代码片段只考虑了一列。但情况并非总是如此,很多时候您需要处理多列预测。例如,您可能会遇到从多个类别中预测一个类别的问题,即多类分类问题。对于多类分类问题,你可以很容易地选择投票方法。但投票法并不总是最佳方法。如果要组合概率,就会有一个二维数组,而不是像我们之前优化 AUC 时的向量。如果有多个类别,可以尝试优化对数损失(或其他与业务相关的指标)。 要进行组合,可以在拟合函数 (X) 中使用 numpy 数组列表而不是 numpy 数组,随后还需要更改优化器和预测函数。我就把它作为一个练习留给大家吧。 245 | 246 | 现在,我们可以进入下一个有趣的话题,这个话题相当流行,被称为**堆叠**。图 2 展示了如何堆叠模型。 247 | 248 | ![](figures/AAAMLP_page280_image.png) 249 | 250 |

图2 : Stacking

251 | 252 | 堆叠不像制造火箭。它简单明了。如果您进行了正确的交叉验证,并在整个建模过程中保持折叠不变,那么就不会出现任何过度贴合的情况。 253 | 254 | 让我用简单的要点向你描述一下这个想法。 255 | - 将训练数据分成若干折叠。 256 | - 训练一堆模型: M1、M2.....Mn。 257 | - 创建完整的训练预测(使用非折叠训练),并使用所有这些模型进行测试预测。 258 | - 直到这里是第 1 层 (L1)。 259 | - 将这些模型的折叠预测作为另一个模型的特征。这就是二级模型(L2)。 260 | - 使用与之前相同的折叠来训练这个 L2 模型。 261 | - 现在,在训练集和测试集上创建 OOF(折叠外)预测。 262 | - 现在您就有了训练数据的 L2 预测和最终测试集预测。 263 | 264 | 您可以不断重复 L1 部分,也可以创建任意多的层次。 265 | 266 | 有时,你还会遇到一个叫混合的术语**blending**。如果你遇到了,不用太担心。它只不过是用一个保留组来堆叠,而不是多重折叠。必须指出的是,我在本章中所描述的内容可以应用于任何类型的问题:分类、回归、多标签分类等。 267 | -------------------------------------------------------------------------------- /docs/组织机器学习项目.md: -------------------------------------------------------------------------------- 1 | # 组织机器学习项目 2 | 3 | 终于,我们可以开始构建第一个机器学习模型了。 4 | 5 | 是这样吗? 6 | 7 | 在开始之前,我们必须注意几件事。请记住,我们将在集成开发环境/文本编辑器中工作,而不是在 jupyter notebook 中。你也可以在 jupyter notebook 中工作,这完全取决于你。不过,我将只使用 jupyter notebook 来探索数据、绘制图表和图形。我们将以这样一种方式构建分类框架,即插即用。您无需对代码做太多改动就能训练模型,而且当您改进模型时,还能使用 git 对其进行跟踪。 8 | 9 | 我们首先来看看文件的结构。对于你正在做的任何项目,都要创建一个新文件夹。在本例中,我将项目命名为 "project"。 10 | 11 | 项目文件夹内部应该如下所示。 12 | 13 | - input 14 | - train.csv 15 | - test.csv 16 | - src 17 | - create_folds.py 18 | - train.py 19 | - inference.py 20 | - models.py 21 | - config.py 22 | - model_dispatcher.py 23 | - models 24 | - model_rf.bin 25 | - model_et.bin 26 | - notebooks 27 | - exploration.ipynb 28 | - check_data.ipynb 29 | - README.md 30 | - LICENSE 31 | 32 | 让我们来看看这些文件夹和文件的内容。 33 | 34 | _input/_:该文件夹包含机器学习项目的所有输入文件和数据。如果您正在开发 NLP 项目,您可以将 embeddings 放在这里。如果是图像项目,所有图像都放在该文件夹下的子文件夹中。 35 | 36 | _src/_:我们将在这里保存与项目相关的所有 python 脚本。如果我说的是一个 python 脚本,即任何 \*.py 文件,它都存储在 src 文件夹中。 37 | 38 | _models/_:该文件夹保存所有训练过的模型。 39 | 40 | _notebook/_:所有 jupyter notebook(即任何 \*.ipynb 文件)都存储在笔记本 文件夹中。 41 | 42 | _README.md_:这是一个标记符文件,您可以在其中描述您的项目,并写明如何训练模型或在生产环境中使用。 43 | 44 | _LICENSE_:这是一个简单的文本文件,包含项目的许可证,如 MIT、Apache 等。关于许可证的详细介绍超出了本书的范围。 45 | 46 | 假设你正在建立一个模型来对 MNIST 数据集(几乎每本机器学习书籍都会用到的数据集)进行分类。如果你还记得,我们在交叉检验一章中也提到过 MNIST 数据集。所以,我就不解释这个数据集是什么样子了。网上有许多不同格式的 MNIST 数据集,但我们将使用 CSV 格式的数据集。 47 | 48 | 在这种格式的数据集中,CSV 的每一行都包含图像的标签和 784 个像素值,像素值范围从 0 到 255。数据集包含 60000 张这种格式的图像。 49 | 50 | 我们可以使用 pandas 轻松读取这种数据格式。 51 | 52 | 请注意,尽管图 1 显示所有像素值均为零,但事实并非如此。 53 | 54 | ![](figures/AAAMLP_page74_image.png) 55 | 56 |

图 1:CSV格式的 MNIST 数据集

57 | 58 | 让我们来看看这个数据集中标签列的计数。 59 | 60 | ![](figures/AAAMLP_page74_image_1.png) 61 | 62 |

图 2:MNIST 数据集中的标签计数

63 | 64 | 我们不需要对这个数据集进行更多的探索。我们已经知道了我们所拥有的数据,没有必要再对不同的像素值进行绘图。从图 2 中可以清楚地看出,标签的分布相当均匀。因此,我们可以使用准确率/F1 作为衡量标准。这就是处理机器学习问题的第一步:确定衡量标准! 65 | 66 | 现在,我们可以编写一些代码了。我们需要创建 _src/_ 文件夹和一些 python 脚本。 67 | 68 | 请注意,训练 CSV 文件位于 _input/_ 文件夹中,名为 _mnist_train.csv_。 69 | 70 | 对于这样一个项目,这些文件应该是什么样的呢? 71 | 72 | 首先要创建的脚本是 **create_folds.py**。 73 | 74 | 这将在 _input/_ 文件夹中创建一个名为 _mnist_train_folds.csv_ 的新文件,与 _mnist_train.csv_ 相同。唯一不同的是,这个 CSV 文件经过了随机排序,并新增了一列名为 _kfold_ 的内容。 75 | 76 | 一旦我们决定了要使用哪种评估指标并创建了折叠,就可以开始创建基本模型了。这可以在 train.py 中完成。 77 | 78 | ```python 79 | import joblib 80 | import pandas as pd 81 | from sklearn import metrics 82 | from sklearn import tree 83 | def run(fold): 84 | # 读取数据文件 85 | df = pd.read_csv("../input/mnist_train_folds.csv") 86 | # 选取df中kfold列不等于fold 87 | df_train = df[df.kfold != fold].reset_index(drop=True) 88 | # 选取df中kfold列等于fold 89 | df_valid = df[df.kfold == fold].reset_index(drop=True) 90 | # 训练集输入,删除label列 91 | x_train = df_train.drop("label", axis=1).values 92 | # 训练集输出,取label列 93 | y_train = df_train.label.values 94 | # 验证集输入,删除label列 95 | x_valid = df_valid.drop("label", axis=1).values 96 | # 验证集输出,取label列 97 | y_valid = df_valid.label.values 98 | # 实例化决策树模型 99 | clf = tree.DecisionTreeClassifier() 100 | # 使用训练集训练模型 101 | clf.fit(x_train, y_train) 102 | # 使用验证集输入得到预测结果 103 | preds = clf.predict(x_valid) 104 | # 计算验证集准确率 105 | accuracy = metrics.accuracy_score(y_valid, preds) 106 | # 打印fold信息和准确率 107 | print(f"Fold={fold}, Accuracy={accuracy}") 108 | # 保存模型 109 | joblib.dump(clf, f"../models/dt_{fold}.bin") 110 | 111 | if __name__ == "__main__": 112 | # 运行每个折叠 113 | run(fold=0) 114 | run(fold=1) 115 | run(fold=2) 116 | run(fold=3) 117 | run(fold=4) 118 | ``` 119 | 120 | 您可以在控制台调用 python train.py 运行该脚本。 121 | 122 | ```python 123 | ❯ python train.py 124 | Fold=0, Accuracy=0.8680833333333333 125 | Fold=1, Accuracy=0.8685 126 | Fold=2, Accuracy=0.8674166666666666 127 | Fold=3, Accuracy=0.8703333333333333 128 | Fold=4, Accuracy=0.8699166666666667 129 | ``` 130 | 131 | 查看训练脚本时,您会发现还有一些内容是硬编码的,例如折叠数、训练文件和输出文件夹。 132 | 133 | 因此,我们可以创建一个包含所有这些信息的配置文件:**config.py**。 134 | 135 | ```python 136 | TRAINING_FILE = "../input/mnist_train_folds.csv" 137 | MODEL_OUTPUT = "../models/" 138 | ``` 139 | 140 | 我们还对训练脚本进行了一些修改。训练文件现在使用配置文件。这样,更改数据或模型输出就更容易了。 141 | 142 | ```python 143 | import os 144 | import config 145 | import joblib 146 | import pandas as pd 147 | from sklearn import metrics 148 | from sklearn import tree 149 | def run(fold): 150 | # 使用config中的路径读取数据 151 | df = pd.read_csv(config.TRAINING_FILE) 152 | df_train = df[df.kfold != fold].reset_index(drop=True) 153 | df_valid = df[df.kfold == fold].reset_index(drop=True) 154 | x_train = df_train.drop("label", axis=1).values 155 | y_train = df_train.label.values 156 | x_valid = df_valid.drop("label", axis=1).values 157 | y_valid = df_valid.label.values 158 | clf = tree.DecisionTreeClassifier() 159 | clf.fit(x_train, y_train) 160 | preds = clf.predict(x_valid) 161 | accuracy = metrics.accuracy_score(y_valid, preds) 162 | print(f"Fold={fold}, Accuracy={accuracy}") 163 | joblib.dump(clf,os.path.join(config.MODEL_OUTPUT, f"dt_{fold}.bin") ) 164 | if __name__ == "__main__": 165 | # 运行每个折叠 166 | run(fold=0) 167 | run(fold=1) 168 | run(fold=2) 169 | run(fold=3) 170 | run(fold=4) 171 | ``` 172 | 173 | 请注意,我并没有展示这个培训脚本与之前脚本的区别。请仔细阅读这两个脚本,自己找出不同之处。区别并不多。 174 | 175 | 与训练脚本相关的还有一点可以改进。正如你所看到的,我们为每个折叠多次调用运行函数。有时,在同一个脚本中运行多个折叠并不可取,因为内存消耗可能会不断增加,程序可能会崩溃。为了解决这个问题,我们可以向训练脚本传递参数。我喜欢使用 argparse。 176 | 177 | ```python 178 | import argparse 179 | 180 | if __name__ == "__main__": 181 | # 实例化参数环境 182 | parser = argparse.ArgumentParser() 183 | # fold参数 184 | parser.add_argument( "--fold", type=int) 185 | # 读取参数 186 | args = parser.parse_args() 187 | run(fold=args.fold) 188 | ``` 189 | 190 | 现在,我们可以再次运行 python 脚本,但仅限于给定的折叠。 191 | 192 | ```python 193 | ❯ python train.py --fold 0 194 | Fold=0, Accuracy=0.8656666666666667 195 | ``` 196 | 197 | 仔细观察,我们的第 0 折得分与之前有些不同。这是因为模型中存在随机性。我们将在后面的章节中讨论如何处理随机性。 198 | 199 | 现在,如果你愿意,可以创建一个 **shell 脚本**,针对不同的折叠使用不同的命令,然后一起运行,如下图所示。 200 | 201 | ```python 202 | python train.py --fold 0 203 | python train.py --fold 1 204 | python train.py --fold 2 205 | python train.py --fold 3 206 | python train.py --fold 4 207 | ``` 208 | 209 | 您可以通过以下命令运行它。 210 | 211 | ```python 212 | ❯ sh run.sh 213 | Fold=0, Accuracy=0.8675 214 | Fold=1, Accuracy=0.8693333333333333 215 | Fold=2, Accuracy=0.8683333333333333 216 | Fold=3, Accuracy=0.8704166666666666 217 | Fold=4, Accuracy=0.8685 218 | ``` 219 | 220 | 我们现在已经取得了一些进展,但如果我们看一下我们的训练脚本,我们仍然受到一些东西的限制,例如模型。模型是硬编码在训练脚本中的,只有修改脚本才能改变它。因此,我们将创建一个新的 python 脚本,名为 **model_dispatcher.py**。model_dispatcher.py,顾名思义,将调度我们的模型到训练脚本中。 221 | 222 | ```python 223 | from sklearn import tree 224 | models = { 225 | # 以gini系数度量的决策树 226 | "decision_tree_gini": tree.DecisionTreeClassifier( 227 | criterion="gini" 228 | ), 229 | # 以entropy系数度量的决策树 230 | "decision_tree_entropy": tree.DecisionTreeClassifier( 231 | criterion="entropy" 232 | ), 233 | } 234 | ``` 235 | 236 | model_dispatcher.py 从 scikit-learn 中导入了 tree,并定义了一个字典,其中键是模型的名称,值是模型本身。在这里,我们定义了两种不同的决策树,一种使用基尼标准,另一种使用熵标准。要使用 py,我们需要对训练脚本做一些修改。 237 | 238 | ```python 239 | import argparse 240 | import os 241 | import joblib 242 | import pandas as pd 243 | from sklearn import metrics 244 | import config 245 | import model_dispatcher 246 | def run(fold, model): 247 | df = pd.read_csv(config.TRAINING_FILE) 248 | df_train = df[df.kfold != fold].reset_index(drop=True) 249 | df_valid = df[df.kfold == fold].reset_index(drop=True) 250 | x_train = df_train.drop("label", axis=1).values 251 | y_train = df_train.label.values 252 | x_valid = df_valid.drop("label", axis=1).values 253 | y_valid = df_valid.label.values 254 | # 根据model参数选择模型 255 | clf = model_dispatcher.models[model] 256 | clf.fit(x_train, y_train) 257 | preds = clf.predict(x_valid) 258 | accuracy = metrics.accuracy_score(y_valid, preds) 259 | print(f"Fold={fold}, Accuracy={accuracy}") 260 | joblib.dump( clf,os.path.join(config.MODEL_OUTPUT, f"dt_{fold}.bin")) 261 | 262 | if __name__ == "__main__": 263 | parser = argparse.ArgumentParser() 264 | # fold参数 265 | parser.add_argument("--fold", type=int) 266 | # model参数 267 | parser.add_argument("--model", type=str) 268 | args = parser.parse_args() 269 | run(fold=args.fold, model=args.model) 270 | ``` 271 | 272 | train.py 有几处重大改动: 273 | 274 | - 导入*model_dispatcher* 275 | - 为 ArgumentParser 添加 --model 参数 276 | - 为 run() 函数添加 model 参数 277 | - 使用调度程序获取指定名称的模型 278 | 279 | 现在,我们可以使用以下命令运行脚本: 280 | 281 | ```python 282 | ❯ python train.py --fold 0 --model decision_tree_gini 283 | Fold=0, Accuracy=0.8665833333333334 284 | ``` 285 | 286 | 或执行以下命令 287 | 288 | ```python 289 | ❯ python train.py --fold 0 --model decision_tree_entropy 290 | Fold=0, Accuracy=0.8705833333333334 291 | ``` 292 | 293 | 现在,如果要添加新模型,只需修改 _model_dispatcher.py_。让我们尝试添加随机森林,看看准确率会有什么变化。 294 | 295 | ```python 296 | from sklearn import ensemble 297 | from sklearn import tree 298 | models = { 299 | "decision_tree_gini": tree.DecisionTreeClassifier( 300 | criterion="gini" 301 | ), 302 | "decision_tree_entropy": tree.DecisionTreeClassifier( 303 | criterion="entropy" 304 | ), 305 | # 随机森林模型 306 | "rf": ensemble.RandomForestClassifier(), 307 | } 308 | ``` 309 | 310 | 让我们运行这段代码。 311 | 312 | ```python 313 | ❯ python train.py --fold 0 --model rf 314 | Fold=0, Accuracy=0.9670833333333333 315 | ``` 316 | 317 | 哇,一个简单的改动就能让分数有如此大的提升!现在,让我们使用 _run.sh_ 脚本运行 5 个折叠! 318 | 319 | ```python 320 | python train.py --fold 0 --model rf 321 | python train.py --fold 1 --model rf 322 | python train.py --fold 2 --model rf 323 | python train.py --fold 3 --model rf 324 | python train.py --fold 4 --model rf 325 | ``` 326 | 327 | 得分情况如下 328 | 329 | ```python 330 | ❯ sh run.sh 331 | Fold=0, Accuracy=0.9674166666666667 332 | Fold=1, Accuracy=0.9698333333333333 333 | Fold=2, Accuracy=0.96575 334 | Fold=3, Accuracy=0.9684166666666667 335 | Fold=4, Accuracy=0.9666666666666667 336 | ``` 337 | 338 | MNIST 几乎是每本书和每篇博客都会讨论的问题。但我试图将这个问题转换得更有趣,并向你展示如何为你正在做的或计划在不久的将来做的几乎所有机器学习项目编写一个基本框架。有许多不同的方法可以改进这个 MNIST 模型和这个框架,我们将在以后的章节中看到。 339 | 340 | 我使用了一些脚本,如 _model_dispatcher.py_ 和 _config.py_,并将它们导入到我的训练脚本中。请注意,我没有使用 `import *` 语法,你也不应该使用。如果我使用了 `import *`,你就永远不会知道模型字典是从哪里来的。编写优秀、易懂的代码是一个人必须具备的基本素质,但许多数据科学家却忽视了这一点。如果你所做的项目能让其他人理解并使用,而无需咨询你的意见,那么你就节省了他们的时间和自己的时间,可以将这些时间投入到改进你的项目或开发新项目中去。 341 | -------------------------------------------------------------------------------- /docs/评估指标.md: -------------------------------------------------------------------------------- 1 | # 评估指标 2 | 3 | 说到机器学习问题,你会在现实世界中遇到很多不同类型的指标。有时,人们甚至会根据业务问题创建度量标准。逐一介绍和解释每一种度量类型超出了本书的范围。相反,我们将介绍一些最常见的度量标准,供你在最初的几个项目中使用。 4 | 5 | 在本书的开头,我们介绍了监督学习和非监督学习。虽然无监督学习可以使用一些指标,但我们将只关注有监督学习。这是因为有监督问题比无监督问题多,而且对无监督方法的评估相当主观。 6 | 7 | 如果我们谈论分类问题,最常用的指标是: 8 | 9 | - 准确率(Accuracy) 10 | - 精确率(P) 11 | - 召回率(R) 12 | - F1 分数(F1) 13 | - AUC(AUC) 14 | - 对数损失(Log loss) 15 | - k 精确率(P@k) 16 | - k 平均精率(AP@k) 17 | - k 均值平均精确率(MAP@k) 18 | 19 | 说到回归,最常用的评价指标是 20 | 21 | - 平均绝对误差 (MAE) 22 | - 均方误差 (MSE) 23 | - 均方根误差 (RMSE) 24 | - 均方根对数误差 (RMSLE) 25 | - 平均百分比误差 (MPE) 26 | - 平均绝对百分比误差 (MAPE) 27 | - R2 28 | 29 | 了解上述指标的工作原理并不是我们必须了解的唯一事情。我们还必须知道何时使用哪些指标,而这取决于你有什么样的数据和目标。我认为这与目标有关,而与数据无关。 30 | 31 | 要进一步了解这些指标,让我们从一个简单的问题开始。假设我们有一个**二元分类**问题,即只有两个目标的问题,假设这是一个胸部 X 光图像分类问题。有的胸部 X 光图像没有问题,而有的胸部 X 光图像有肺塌陷,也就是所谓的气胸。因此,我们的任务是建立一个分类器,在给定胸部 X 光图像的情况下,它能检测出图像是否有气胸。 32 | 33 | ![](figures/AAAMLP_page30_image.png) 34 | 35 |

图 1:气胸肺部图像

36 | 37 | 我们还假设有相同数量的气胸和非气胸胸部 X 光图像,比如各 100 张。因此,我们有 100 张阳性样本和 100 张阴性样本,共计 200 张图像。 38 | 39 | 第一步是将上述数据分为两组,每组 100 张图像,即训练集和验证集。在这两个集合中,我们都有 50 个正样本和 50 个负样本。 40 | 41 | 在二元分类指标中,当正负样本数量相等时,我们通常使用准确率、精确率、召回率和 F1。 42 | 43 | **准确率**:这是机器学习中最直接的指标之一。它定义了模型的准确度。对于上述问题,如果你建立的模型能准确分类 90 张图片,那么你的准确率就是 90% 或 0.90。如果只有 83 幅图像被正确分类,那么模型的准确率就是 83% 或 0.83。 44 | 45 | 计算准确率的 Python 代码也非常简单。 46 | 47 | ```python 48 | def accuracy(y_true, y_pred): 49 | # 为正确预测数初始化一个简单计数器 50 | correct_counter = 0 51 | # 遍历y_true,y_pred中所有元素 52 | for yt, yp in zip(y_true, y_pred): 53 | if yt == yp: 54 | # 如果预测标签与真实标签相同,则增加计数器 55 | correct_counter += 1 56 | # 返回正确率,正确标签数/总标签数 57 | return correct_counter / len(y_true) 58 | ``` 59 | 60 | 我们还可以使用 scikit-learn 计算准确率。 61 | 62 | ```python 63 | In [X]: from sklearn import metrics 64 | ...: l1 = [0,1,1,1,0,0,0,1] 65 | ...: l2 = [0,1,0,1,0,1,0,0] 66 | ...: metrics.accuracy_score(l1, l2) 67 | Out[X]: 0.625 68 | ``` 69 | 70 | 现在,假设我们把数据集稍微改动一下,有 180 张没有气胸的胸部 X 光图像,只有 20 张有气胸。即使在这种情况下,我们也要创建正负(气胸与非气胸)目标比例相同的训练集和验证集。在每一组中,我们有 90 张非气胸图像和 10 张气胸图像。如果说验证集中的所有图像都是非气胸图像,那么您的准确率会是多少呢?让我们来看看;您对 90% 的图像进行了正确分类。因此,您的准确率是 90%。 71 | 72 | 但请再看一遍。 73 | 74 | 你甚至没有建立一个模型,就得到了 90% 的准确率。这似乎有点没用。如果我们仔细观察,就会发现数据集是偏斜的,也就是说,一个类别中的样本数量比另一个类别中的样本数量多很多。在这种情况下,使用准确率作为评估指标是不可取的,因为它不能代表数据。因此,您可能会获得很高的准确率,但您的模型在实际样本中的表现可能并不理想,而且您也无法向经理解释原因。 75 | 76 | 在这种情况下,最好还是看看**精确率**等其他指标。 77 | 78 | 在学习精确率之前,我们需要了解一些术语。在这里,我们假设有气胸的胸部 X 光图像为正类 (1),没有气胸的为负类 (0)。 79 | 80 | **真阳性 (TP)**: 给定一幅图像,如果您的模型预测该图像有气胸,而该图像的实际目标有气胸,则视为真阳性。 81 | 82 | **真阴性 (TN)**: 给定一幅图像,如果您的模型预测该图像没有气胸,而实际目标显示该图像没有气胸,则视为真阴性。 83 | 84 | 简单地说,如果您的模型正确预测了阳性类别,它就是真阳性;如果您的模型准确预测了阴性类别,它就是真阴性。 85 | 86 | **假阳性 (FP)**:给定一张图像,如果您的模型预测为气胸,而该图像的实际目标是非气胸,则为假阳性。 87 | 88 | **假阴性 (FN)**: 给定一幅图像,如果您的模型预测为非气胸,而该图像的实际目标是气胸,则为假阴性。 89 | 90 | 简单地说,如果您的模型错误地(或虚假地)预测了阳性类,那么它就是假阳性。如果模型错误地(或虚假地)预测了阴性类别,则是假阴性。 91 | 92 | 让我们逐一看看这些实现。 93 | 94 | ```python 95 | def true_positive(y_true, y_pred): 96 | # 初始化真阳性样本计数器 97 | tp = 0 98 | # 遍历y_true,y_pred中所有元素 99 | for yt, yp in zip(y_true, y_pred): 100 | # 若真实标签为正类且预测标签也为正类,计数器增加 101 | if yt == 1 and yp == 1: 102 | tp += 1 103 | # 返回真阳性样本数 104 | return tp 105 | 106 | def true_negative(y_true, y_pred): 107 | # 初始化真阴性样本计数器 108 | tn = 0 109 | # 遍历y_true,y_pred中所有元素 110 | for yt, yp in zip(y_true, y_pred): 111 | # 若真实标签为负类且预测标签也为负类,计数器增加 112 | if yt == 0 and yp == 0: 113 | tn += 1 114 | # 返回真阴性样本数 115 | return tn 116 | 117 | def false_positive(y_true, y_pred): 118 | # 初始化假阳性计数器 119 | fp = 0 120 | # 遍历y_true,y_pred中所有元素 121 | for yt, yp in zip(y_true, y_pred): 122 | # 若真实标签为负类而预测标签为正类,计数器增加 123 | if yt == 0 and yp == 1: 124 | fp += 1 125 | # 返回假阳性样本数 126 | return fp 127 | 128 | def false_negative(y_true, y_pred): 129 | # 初始化假阴性计数器 130 | fn = 0 131 | # 遍历y_true,y_pred中所有元素 132 | for yt, yp in zip(y_true, y_pred): 133 | # 若真实标签为正类而预测标签为负类,计数器增加 134 | if yt == 1 and yp == 0: 135 | fn += 1 136 | # 返回假阴性数 137 | return fn 138 | ``` 139 | 140 | 我在这里实现这些功能的方法非常简单,而且只适用于二元分类。让我们检查一下这些函数。 141 | 142 | ```python 143 | In [X]: l1 = [0,1,1,1,0,0,0,1] 144 | ...: l2 = [0,1,0,1,0,1,0,0] 145 | In [X]: true_positive(l1, l2) 146 | Out[X]: 2 147 | In [X]: false_positive(l1, l2) 148 | Out[X]: 1 149 | In [X]: false_negative(l1, l2) 150 | Out[X]: 2 151 | In [X]: true_negative(l1, l2) 152 | Out[X]: 3 153 | ``` 154 | 155 | 如果我们必须用上述术语来定义准确率,我们可以写为: 156 | 157 | $$ 158 | Accuracy Score = (TP + TN)/(TP + TN + FP +FN) 159 | $$ 160 | 161 | 现在,我们可以在 python 中使用 TP、TN、FP 和 FN 快速实现准确度得分。我们将其称为 accuracy_v2。 162 | 163 | ```python 164 | def accuracy_v2(y_true, y_pred): 165 | # 真阳性样本数 166 | tp = true_positive(y_true, y_pred) 167 | # 假阳性样本数 168 | fp = false_positive(y_true, y_pred) 169 | # 假阴性样本数 170 | fn = false_negative(y_true, y_pred) 171 | # 真阴性样本数 172 | tn = true_negative(y_true, y_pred) 173 | # 准确率 174 | accuracy_score = (tp + tn) / (tp + tn + fp + fn) 175 | return accuracy_score 176 | ``` 177 | 178 | 我们可以通过与之前的实现和 scikit-learn 版本进行比较,快速检查该函数的正确性。 179 | 180 | ```python 181 | In [X]: l1 = [0,1,1,1,0,0,0,1] 182 | ...: l2 = [0,1,0,1,0,1,0,0] 183 | In [X]: accuracy(l1, l2) 184 | Out[X]: 0.625 185 | In [X]: accuracy_v2(l1, l2) 186 | Out[X]: 0.625 187 | In [X]: metrics.accuracy_score(l1, l2) 188 | Out[X]: 0.625 189 | ``` 190 | 191 | 请注意,在这段代码中,metrics.accuracy_score 来自 scikit-learn。 192 | 193 | 很好。所有值都匹配。这说明我们在实现过程中没有犯任何错误。 194 | 195 | 现在,我们可以转向其他重要指标。 196 | 197 | 首先是精确率。精确率的定义是 198 | 199 | $$ 200 | Precision = TP/(TP + FP) 201 | $$ 202 | 203 | 假设我们在新的偏斜数据集上建立了一个新模型,我们的模型正确识别了 90 张图像中的 80 张非气胸图像和 10 张图像中的 8 张气胸图像。因此,我们成功识别了 100 张图像中的 88 张。因此,准确率为 0.88 或 88%。 204 | 205 | 但是,在这 100 张样本中,有 10 张非气胸图像被误判为气胸,2 张气胸图像被误判为非气胸。 206 | 207 | 因此,我们有 208 | 209 | - TP : 8 210 | - TN: 80 211 | - FP: 10 212 | - FN: 2 213 | 214 | 精确率为 8 / (8 + 10) = 0.444。这意味着我们的模型在识别阳性样本(气胸)时有 44.4% 的正确率。 215 | 216 | 现在,既然我们已经实现了 TP、TN、FP 和 FN,我们就可以很容易地在 python 中实现精确率了。 217 | 218 | ```python 219 | def precision(y_true, y_pred): 220 | # 真阳性样本数 221 | tp = true_positive(y_true, y_pred) 222 | # 假阳性样本数 223 | fp = false_positive(y_true, y_pred) 224 | # 精确率 225 | precision = tp / (tp + fp) 226 | return precision 227 | ``` 228 | 229 | 让我们试试这种精确率的实现方式。 230 | 231 | ```python 232 | In [X]: l1 = [0,1,1,1,0,0,0,1] 233 | ...: l2 = [0,1,0,1,0,1,0,0] 234 | In [X]: precision(l1, l2) 235 | Out[X]: 0.6666666666666666 236 | ``` 237 | 238 | 这似乎没有问题。 接下来,我们来看**召回率**。召回率的定义是: 239 | 240 | $$ 241 | Recall = TP/(TP + FN) 242 | $$ 243 | 244 | 在上述情况下,召回率为 8 / (8 + 2) = 0.80。这意味着我们的模型正确识别了 80% 的阳性样本。 245 | 246 | ```python 247 | def recall(y_true, y_pred): 248 | # 真阳性样本数 249 | tp = true_positive(y_true, y_pred) 250 | # 假阴性样本数 251 | fn = false_negative(y_true, y_pred) 252 | # 召回率 253 | recall = tp / (tp + fn) 254 | return recall 255 | ``` 256 | 257 | 就我们的两个小列表而言,召回率应该是 0.5。让我们检查一下。 258 | 259 | ```python 260 | In [X]: l1 = [0,1,1,1,0,0,0,1] 261 | ...: l2 = [0,1,0,1,0,1,0,0] 262 | In [X]: recall(l1, l2) 263 | Out[X]: 0.5 264 | ``` 265 | 266 | 这与我们的计算值相符! 267 | 268 | 对于一个 "好 "模型来说,精确率和召回值都应该很高。我们看到,在上面的例子中,召回值相当高。但是,精确率却很低!我们的模型产生了大量的误报,但漏报较少。在这类问题中,假阴性较少是好事,因为你不想在病人有气胸的情况下却说他们没有气胸。这样做会造成更大的伤害。但我们也有很多假阳性结果,这也不是好事。 269 | 270 | 大多数模型都会预测一个概率,当我们预测时,通常会将这个阈值选为 0.5。这个阈值并不总是理想的,根据这个阈值,精确率和召回率的值可能会发生很大的变化。如果我们选择的每个阈值都能计算出精确率和召回率,那么我们就可以在这些值之间绘制出曲线图。这幅图或曲线被称为 "精确率-召回率曲线"。 271 | 272 | 在研究精确率-调用曲线之前,我们先假设有两个列表。 273 | 274 | ```python 275 | In [X]: y_true = [0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 276 | ...: 1, 0, 0, 0, 0, 0, 0, 0, 1, 0] 277 | In [X]: y_pred = [0.02638412, 0.11114267, 0.31620708, 278 | ...: 0.0490937, 0.0191491, 0.17554844, 279 | ...: 0.15952202, 0.03819563, 0.11639273, 280 | ...: 0.079377, 0.08584789, 0.39095342, 281 | ...: 0.27259048, 0.03447096, 0.04644807, 282 | ...: 0.03543574, 0.18521942, 0.05934905, 283 | ...: 0.61977213, 0.33056815] 284 | ``` 285 | 286 | 因此,y_true 是我们的目标值,而 y_pred 是样本被赋值为 1 的概率值。因此,现在我们要看的是预测中的概率,而不是预测值(大多数情况下,预测值的计算阈值为 0.5)。 287 | 288 | ```python 289 | precisions = [] 290 | recalls = [] 291 | thresholds = [0.0490937 , 0.05934905, 0.079377, 292 | 0.08584789, 0.11114267, 0.11639273, 293 | 0.15952202, 0.17554844, 0.18521942, 294 | 0.27259048, 0.31620708, 0.33056815, 295 | 0.39095342, 0.61977213] 296 | 297 | # 遍历预测阈值 298 | for i in thresholds: 299 | # 若样本为正类(1)的概率大于阈值,为1,否则为0 300 | temp_prediction = [1 if x >= i else 0 for x in y_pred] 301 | # 计算精确率 302 | p = precision(y_true, temp_prediction) 303 | # 计算召回率 304 | r = recall(y_true, temp_prediction) 305 | # 加入精确率列表 306 | precisions.append(p) 307 | # 加入召回率列表 308 | recalls.append(r) 309 | ``` 310 | 311 | 现在,我们可以绘制精确率-召回率曲线。 312 | 313 | ```python 314 | # 创建画布 315 | plt.figure(figsize=(7, 7)) 316 | # x轴为召回率,y轴为精确率 317 | plt.plot(recalls, precisions) 318 | # 添加x轴标签,字体大小为15 319 | plt.xlabel('Recall', fontsize=15) 320 | # 添加y轴标签,字条大小为15 321 | plt.ylabel('Precision', fontsize=15) 322 | ``` 323 | 324 | 图 2 显示了我们通过这种方法得到的精确率-召回率曲线。 325 | 326 | ![](figures/AAAMLP_page39_image.png) 327 | 328 |

图 2:精确率-召回率曲线

329 | 330 | 这条**精确率-召回率曲线**与您在互联网上看到的曲线截然不同。这是因为我们只有 20 个样本,其中只有 3 个是阳性样本。但这没什么好担心的。这还是那条精确率-召回曲线。 331 | 332 | 你会发现,选择一个既能提供良好精确率又能提供召回值的阈值是很有挑战性的。如果阈值过高,真阳性的数量就会减少,而假阴性的数量就会增加。这会降低召回率,但精确率得分会很高。如果将阈值降得太低,则误报会大量增加,精确率也会降低。 333 | 334 | 精确率和召回率的范围都是从 0 到 1,越接近 1 越好。 335 | 336 | F1 分数是精确率和召回率的综合指标。它被定义为精确率和召回率的简单加权平均值(调和平均值)。如果我们用 P 表示精确率,用 R 表示召回率,那么 F1 分数可以表示为: 337 | 338 | $$ 339 | F1 = 2PR/(P + R) 340 | $$ 341 | 342 | 根据 TP、FP 和 FN,稍加数学计算就能得出以下 F1 等式: 343 | 344 | $$ 345 | F1 = 2TP/(2TP + FP + FN) 346 | $$ 347 | 348 | Python 实现很简单,因为我们已经实现了这些 349 | 350 | ```python 351 | def f1(y_true, y_pred): 352 | # 计算精确率 353 | p = precision(y_true, y_pred) 354 | # 计算召回率 355 | r = recall(y_true, y_pred) 356 | # 计算f1值 357 | score = 2 * p * r / (p + r) 358 | return score 359 | ``` 360 | 361 | 让我们看看其结果,并与 scikit-learn 进行比较。 362 | 363 | ```python 364 | In [X]: y_true = [0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 365 | ...: 1, 0, 0, 0, 0, 0, 0, 0, 1, 0] 366 | In [X]: y_pred = [0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 367 | ...: 1, 0, 0, 0, 0, 0, 0, 0, 1, 0] 368 | In [X]: f1(y_true, y_pred) 369 | Out[X]: 0.5714285714285715 370 | ``` 371 | 372 | 通过 scikit learn,我们可以得到相同的列表: 373 | 374 | ```python 375 | In [X]: from sklearn import metrics 376 | In [X]: metrics.f1_score(y_true, y_pred) 377 | Out[X]: 0.5714285714285715 378 | ``` 379 | 380 | 与其单独看精确率和召回率,您还可以只看 F1 分数。与精确率、召回率和准确度一样,F1 分数的范围也是从 0 到 1,完美预测模型的 F1 分数为 1。 381 | 382 | 此外,我们还应该了解其他一些关键术语。 383 | 384 | 第一个术语是 TPR 或真阳性率(True Positive Rate),它与召回率相同。 385 | 386 | $$ 387 | TPR = TP/(TP + FN) 388 | $$ 389 | 390 | 尽管它与召回率相同,但我们将为它创建一个 python 函数,以便今后使用这个名称。 391 | 392 | ```python 393 | def tpr(y_true, y_pred): 394 | # 真阳性率(TPR),与召回率计算公式一致 395 | return recall(y_true, y_pred) 396 | ``` 397 | 398 | TPR 或召回率也被称为灵敏度。 399 | 400 | 而 FPR 或假阳性率(False Positive Rate)的定义是: 401 | 402 | $$ 403 | FPR = FP / (TN + FP) 404 | $$ 405 | 406 | ```python 407 | def fpr(y_true, y_pred): 408 | # 假阳性样本数 409 | fp = false_positive(y_true, y_pred) 410 | # 真阴性样本数 411 | tn = true_negative(y_true, y_pred) 412 | # 返回假阳性率(FPR) 413 | return fp / (tn + fp) 414 | ``` 415 | 416 | 1 - FPR 被称为特异性或真阴性率或 TNR。这些术语很多,但其中最重要的只有 TPR 和 FPR。假设我们只有 15 个样本,其目标值为二元: 417 | 418 | Actual targets : [0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1] 419 | 420 | 我们训练一个类似随机森林的模型,就能得到样本呈阳性的概率。 421 | 422 | Predicted probabilities for 1: [0.1, 0.3, 0.2, 0.6, 0.8, 0.05, 0.9, 0.5, 0.3, 0.66, 0.3, 0.2, 0.85, 0.15, 0.99] 423 | 424 | 对于 >= 0.5 的典型阈值,我们可以评估上述所有精确率、召回率/TPR、F1 和 FPR 值。但是,如果我们将阈值选为 0.4 或 0.6,也可以做到这一点。事实上,我们可以选择 0 到 1 之间的任何值,并计算上述所有指标。 425 | 426 | 不过,我们只计算两个值: TPR 和 FPR。 427 | 428 | ```python 429 | # 初始化真阳性率列表 430 | tpr_list = [] 431 | # 初始化假阳性率列表 432 | fpr_list = [] 433 | 434 | # 真实样本标签 435 | y_true = [0, 0, 0, 0, 1, 0, 1, 436 | 0, 0, 1, 0, 1, 0, 0, 1] 437 | 438 | # 预测样本为正类(1)的概率 439 | y_pred = [0.1, 0.3, 0.2, 0.6, 0.8, 0.05, 440 | 0.9, 0.5, 0.3, 0.66, 0.3, 0.2, 441 | 0.85, 0.15, 0.99] 442 | 443 | # 预测阈值 444 | thresholds = [0, 0.1, 0.2, 0.3, 0.4, 0.5, 445 | 0.6, 0.7, 0.8, 0.85, 0.9, 0.99, 1.0] 446 | 447 | # 遍历预测阈值 448 | for thresh in thresholds: 449 | # 若样本为正类(1)的概率大于阈值,为1,否则为0 450 | temp_pred = [1 if x >= thresh else 0 for x in y_pred] 451 | # 真阳性率 452 | temp_tpr = tpr(y_true, temp_pred) 453 | # 假阳性率 454 | temp_fpr = fpr(y_true, temp_pred) 455 | # 将真阳性率加入列表 456 | tpr_list.append(temp_tpr) 457 | # 将假阳性率加入列表 458 | fpr_list.append(temp_fpr) 459 | ``` 460 | 461 | 因此,我们可以得到每个阈值的 TPR 值和 FPR 值。 462 | 463 | ![](figures/AAAMLP_page43_image.png) 464 | 465 |

图 3:阈值、TPR 和 FPR 值表

466 | 467 | 如果我们绘制如图 3 所示的表格,即以 TPR 为 Y 轴,FPR 为 X 轴,就会得到如图 4 所示的曲线。 468 | 469 | ![](figures/AAAMLP_page44_image.png) 470 | 471 |

图 4:ROC曲线

472 | 473 | 这条曲线也被称为 ROC 曲线。如果我们计算这条 ROC 曲线下的面积,就是在计算另一个指标,当数据集的二元目标偏斜时,这个指标就会非常常用。 474 | 475 | 这个指标被称为 ROC 曲线下面积或曲线下面积,简称 AUC。计算 ROC 曲线下面积的方法有很多。在此,我们将采用 scikit- learn 的奇妙实现方法。 476 | 477 | ```python 478 | In [X]: from sklearn import metrics 479 | In [X]: y_true = [0, 0, 0, 0, 1, 0, 1, 480 | ...: 0, 0, 1, 0, 1, 0, 0, 1] 481 | In [X]: y_pred = [0.1, 0.3, 0.2, 0.6, 0.8, 0.05, 482 | ...: 0.9, 0.5, 0.3, 0.66, 0.3, 0.2, 483 | ...: 0.85, 0.15, 0.99] 484 | In [X]: metrics.roc_auc_score(y_true, y_pred) 485 | Out[X]: 0.8300000000000001 486 | ``` 487 | 488 | AUC 值从 0 到 1 不等。 489 | 490 | - AUC = 1 意味着您拥有一个完美的模型。大多数情况下,这意味着你在验证时犯了一些错误,应该重新审视数据处理和验证流程。如果你没有犯任何错误,那么恭喜你,你已经拥有了针对数据集建立的最佳模型。 491 | - AUC = 0 意味着您的模型非常糟糕(或非常好!)。试着反转预测的概率,例如,如果您预测正类的概率是 p,试着用 1-p 代替它。这种 AUC 也可能意味着您的验证或数据处理存在问题。 492 | - AUC = 0.5 意味着你的预测是随机的。因此,对于任何二元分类问题,如果我将所有目标都预测为 0.5,我将得到 0.5 的 AUC。 493 | 494 | AUC 值介于 0 和 0.5 之间,意味着你的模型比随机模型更差。大多数情况下,这是因为你颠倒了类别。 如果您尝试反转预测,您的 AUC 值可能会超过 0.5。接近 1 的 AUC 值被认为是好值。 495 | 496 | 但 AUC 对我们的模型有什么影响呢? 497 | 498 | 假设您建立了一个从胸部 X 光图像中检测气胸的模型,其 AUC 值为 0.85。这意味着,如果您从数据集中随机选择一张有气胸的图像(阳性样本)和另一张没有气胸的图像(阴性样本),那么气胸图像的排名将高于非气胸图像,概率为 0.85。 499 | 500 | 计算概率和 AUC 后,您需要对测试集进行预测。根据问题和使用情况,您可能需要概率或实际类别。如果你想要概率,这并不难。如果您想要类别,则需要选择一个阈值。在二元分类的情况下,您可以采用类似下面的方法。 501 | 502 | $$ 503 | Prediction = Probability >= Threshold 504 | $$ 505 | 506 | 也就是说,预测是一个只包含二元变量的新列表。如果概率大于或等于给定的阈值,则预测中的一项为 1,否则为 0。 507 | 508 | 你猜怎么着,你可以使用 ROC 曲线来选择这个阈值!ROC 曲线会告诉您阈值对假阳性率和真阳性率的影响,进而影响假阳性和真阳性。您应该选择最适合您的问题和数据集的阈值。 509 | 510 | 例如,如果您不希望有太多的误报,那么阈值就应该高一些。不过,这也会带来更多的漏报。注意权衡利弊,选择最佳阈值。让我们看看这些阈值如何影响真阳性和假阳性值。 511 | 512 | ```python 513 | # 真阳性样本数列表 514 | tp_list = [] 515 | # 假阳性样本数列表 516 | fp_list = [] 517 | 518 | # 真实标签 519 | y_true = [0, 0, 0, 0, 1, 0, 1, 520 | 0, 0, 1, 0, 1, 0, 0, 1] 521 | 522 | # 预测样本为正类(1)的概率 523 | y_pred = [0.1, 0.3, 0.2, 0.6, 0.8, 0.05, 524 | 0.9, 0.5, 0.3, 0.66, 0.3, 0.2, 525 | 0.85, 0.15, 0.99] 526 | 527 | # 预测阈值 528 | thresholds = [0, 0.1, 0.2, 0.3, 0.4, 0.5, 529 | 0.6, 0.7, 0.8, 0.85, 0.9, 0.99, 1.0] 530 | 531 | # 遍历预测阈值 532 | for thresh in thresholds: 533 | # 若样本为正类(1)的概率大于阈值,为1,否则为0 534 | temp_pred = [1 if x >= thresh else 0 for x in y_pred] 535 | # 真阳性样本数 536 | temp_tp = true_positive(y_true, temp_pred) 537 | # 假阳性样本数 538 | temp_fp = false_positive(y_true, temp_pred) 539 | # 加入真阳性样本数列表 540 | tp_list.append(temp_tp) 541 | # 加入假阳性样本数列表 542 | fp_list.append(temp_fp) 543 | ``` 544 | 545 | 利用这一点,我们可以创建一个表格,如图 5 所示。 546 | 547 | ![](figures/AAAMLP_page47_image.png) 548 | 549 |

图 5:不同阈值的 TP 值和 FP 值

550 | 551 | 如图 6 所示,大多数情况下,ROC 曲线左上角的值应该是一个相当不错的阈值。 552 | 553 | 对比表格和 ROC 曲线,我们可以发现,0.6 左右的阈值相当不错,既不会丢失大量的真阳性结果,也不会出现大量的假阳性结果。 554 | 555 | ![](figures/AAAMLP_page48_image.png) 556 | 557 |

图 6:从 ROC 曲线最左侧的顶点选择最佳阈值

558 | 559 | AUC 是业内广泛应用于偏斜二元分类任务的指标,也是每个人都应该了解的指标。一旦理解了 AUC 背后的理念(如上文所述),也就很容易向业界可能会评估您的模型的非技术人员解释它了。 560 | 561 | 学习 AUC 后,你应该学习的另一个重要指标是对数损失。对于二元分类问题,我们将对数损失定义为: 562 | 563 | $$ 564 | LogLoss = -1.0 \times (target \times log(prediction) + (1-target) \times log(1-prediction)) 565 | $$ 566 | 567 | 其中,目标值为 0 或 1,预测值为样本属于类别 1 的概率。 568 | 569 | 对于数据集中的多个样本,所有样本的对数损失只是所有单个对数损失的平均值。需要记住的一点是,对数损失会对不正确或偏差较大的预测进行相当高的惩罚,也就是说,对数损失会对非常确定和非常错误的预测进行惩罚。 570 | 571 | ```python 572 | import numpy as np 573 | def log_loss(y_true, y_proba): 574 | # 极小值,防止0做分母 575 | epsilon = 1e-15 576 | 577 | # 对数损失列表 578 | loss = [] 579 | # 遍历y_true,y_pred中所有元素 580 | for yt, yp in zip(y_true, y_proba): 581 | # 限制yp范围,最小为epsilon,最大为1-epsilon 582 | yp = np.clip(yp, epsilon, 1 - epsilon) 583 | # 计算对数损失 584 | temp_loss = - 1.0 * (yt * np.log(yp)+ (1 - yt) * np.log(1 - yp)) 585 | # 加入对数损失列表 586 | loss.append(temp_loss) 587 | return np.mean(loss) 588 | ``` 589 | 590 | 让我们测试一下函数执行情况: 591 | 592 | ```python 593 | In [X]: y_true = [0, 0, 0, 0, 1, 0, 1, 594 | ...: 0, 0, 1, 0, 1, 0, 0, 1] 595 | In [X]: y_proba = [0.1, 0.3, 0.2, 0.6, 0.8, 0.05, 596 | ...: 0.9, 0.5, 0.3, 0.66, 0.3, 0.2, 597 | ...: 0.85, 0.15, 0.99] 598 | In [X]: log_loss(y_true, y_proba) 599 | Out[X]: 0.49882711861432294 600 | ``` 601 | 602 | 我们可以将其与 scikit-learn 进行比较: 603 | 604 | ```python 605 | In [X]: from sklearn import metrics 606 | In [X]: metrics.log_loss(y_true, y_proba) 607 | Out[X]: 0.49882711861432294 608 | ``` 609 | 610 | 因此,我们的实现是正确的。 对数损失的实现很容易。解释起来似乎有点困难。你必须记住,对数损失的惩罚要比其他指标大得多。 611 | 612 | 例如,如果您有 51% 的把握认为样本属于第 1 类,那么对数损失就是: 613 | 614 | $$ 615 | -1.0 \times (1 \times log(0.51) + (1 - 1) \times log(1 - 0.51))=0.67 616 | $$ 617 | 618 | 如果你对属于 0 类的样本有 49% 的把握,对数损失就是: 619 | 620 | $$ 621 | -1.0 \times (1 \times log(0.49) + (1 - 1) \times log(1 - 0.49))=0.67 622 | $$ 623 | 624 | 因此,即使我们可以选择 0.5 的截断值并得到完美的预测结果,我们仍然会有非常高的对数损失。因此,在处理对数损失时,你需要非常小心;任何不确定的预测都会产生非常高的对数损失。 625 | 626 | 我们之前讨论过的大多数指标都可以转换成多类版本。这个想法很简单。以精确率和召回率为例。我们可以计算多类分类问题中每一类的精确率和召回率。 627 | 628 | 有三种不同的计算方法,有时可能会令人困惑。假设我们首先对精确率感兴趣。我们知道,精确率取决于真阳性和假阳性。 629 | 630 | - **宏观平均精确率**(Macro averaged precision):分别计算所有类别的精确率然后求平均值 631 | - **微观平均精确率**(Micro averaged precision):按类计算真阳性和假阳性,然后用其计算总体精确率。然后以此计算总体精确率 632 | - **加权精确率**(Weighted precision):与宏观精确率相同,但这里是加权平均精确率 取决于每个类别中的项目数 633 | 634 | 这看似复杂,但在 python 实现中很容易理解。让我们看看宏观平均精确率是如何实现的。 635 | 636 | ```python 637 | import numpy as np 638 | def macro_precision(y_true, y_pred): 639 | # 种类数 640 | num_classes = len(np.unique(y_true)) 641 | # 初始化精确率 642 | precision = 0 643 | # 遍历0~(种类数-1) 644 | for class_ in range(num_classes): 645 | # 若真实标签为class_为1,否则为0 646 | temp_true = [1 if p == class_ else 0 for p in y_true] 647 | # 如预测标签为class_为1,否则为0 648 | temp_pred = [1 if p == class_ else 0 for p in y_pred] 649 | # 真阳性样本数 650 | tp = true_positive(temp_true, temp_pred) 651 | # 假阳性样本数 652 | fp = false_positive(temp_true, temp_pred) 653 | # 计算精确度 654 | temp_precision = tp / (tp + fp) 655 | # 各类精确率相加 656 | precision += temp_precision 657 | # 计算平均值 658 | precision /= num_classes 659 | return precision 660 | ``` 661 | 662 | 你会发现这并不难。同样,我们还有微平均精确率分数。 663 | 664 | ```python 665 | import numpy as np 666 | def micro_precision(y_true, y_pred): 667 | # 种类数 668 | num_classes = len(np.unique(y_true)) 669 | # 初始化真阳性样本数 670 | tp = 0 671 | # 初始化假阳性样本数 672 | fp = 0 673 | # 遍历0~(种类数-1) 674 | for class_ in range(num_classes): 675 | # 若真实标签为class_为1,否则为0 676 | temp_true = [1 if p == class_ else 0 for p in y_true] 677 | # 若预测标签为class_为1,否则为0 678 | temp_pred = [1 if p == class_ else 0 for p in y_pred] 679 | # 真阳性样本数相加 680 | tp += true_positive(temp_true, temp_pred) 681 | # 假阳性样本数相加 682 | fp += false_positive(temp_true, temp_pred) 683 | # 精确率 684 | precision = tp / (tp + fp) 685 | return precision 686 | ``` 687 | 688 | 这也不难。那什么难?什么都不难。机器学习很简单。现在,让我们来看看加权精确率的实现。 689 | 690 | ```python 691 | from collections import Counter 692 | import numpy as np 693 | def weighted_precision(y_true, y_pred): 694 | # 种类数 695 | num_classes = len(np.unique(y_true)) 696 | # 统计各种类样本数 697 | class_counts = Counter(y_true) 698 | # 初始化精确率 699 | precision = 0 700 | # 遍历0~(种类数-1) 701 | for class_ in range(num_classes): 702 | # 若真实标签为class_为1,否则为0 703 | temp_true = [1 if p == class_ else 0 for p in y_true] 704 | # 若预测标签为class_为1,否则为0 705 | temp_pred = [1 if p == class_ else 0 for p in y_pred] 706 | # 真阳性样本数 707 | tp = true_positive(temp_true, temp_pred) 708 | # 假阳性样本数 709 | fp = false_positive(temp_true, temp_pred) 710 | # 精确率 711 | temp_precision = tp / (tp + fp) 712 | # 根据该种类样本数分配权重 713 | weighted_precision = class_counts[class_] * temp_precision 714 | # 加权精确率求和 715 | precision += weighted_precision 716 | # 计算平均精确率 717 | overall_precision = precision / len(y_true) 718 | return overall_precision 719 | ``` 720 | 721 | 将我们的实现与 scikit-learn 进行比较,以了解实现是否正确。 722 | 723 | ```python 724 | In [X]: from sklearn import metrics 725 | In [X]: y_true = [0, 1, 2, 0, 1, 2, 0, 2, 2] 726 | In [X]: y_pred = [0, 2, 1, 0, 2, 1, 0, 0, 2] 727 | In [X]: macro_precision(y_true, y_pred) 728 | Out[X]: 0.3611111111111111 729 | In [X]: metrics.precision_score(y_true, y_pred, average="macro") 730 | Out[X]: 0.3611111111111111 731 | In [X]: micro_precision(y_true, y_pred) 732 | Out[X]: 0.4444444444444444 733 | In [X]: metrics.precision_score(y_true, y_pred, average="micro") 734 | Out[X]: 0.4444444444444444 735 | In [X]: weighted_precision(y_true, y_pred) 736 | Out[X]: 0.39814814814814814 737 | In [X]: metrics.precision_score(y_true, y_pred, average="weighted") 738 | Out[X]: 0.39814814814814814 739 | ``` 740 | 741 | 看来我们已经正确地实现了一切。 请注意,这里展示的实现可能不是最有效的,但却是最容易理解的。 742 | 743 | 同样,我们也可以实现**多类别的召回率指标**。精确率和召回率取决于真阳性、假阳性和假阴性,而 F1 则取决于精确率和召回率。 744 | 745 | 召回率的实现方法留待读者练习,这里实现的是多类 F1 的一个版本,即加权平均值。 746 | 747 | ```python 748 | from collections import Counter 749 | import numpy as np 750 | def weighted_f1(y_true, y_pred): 751 | # 种类数 752 | num_classes = len(np.unique(y_true)) 753 | # 统计各种类样本数 754 | class_counts = Counter(y_true) 755 | # 初始化F1值 756 | f1 = 0 757 | # 遍历0~(种类数-1) 758 | for class_ in range(num_classes): 759 | # 若真实标签为class_为1,否则为0 760 | temp_true = [1 if p == class_ else 0 for p in y_true] 761 | # 若预测标签为class_为1,否则为0 762 | temp_pred = [1 if p == class_ else 0 for p in y_pred] 763 | # 计算精确率 764 | p = precision(temp_true, temp_pred) 765 | # 计算召回率 766 | r = recall(temp_true, temp_pred) 767 | # 若精确率+召回率不为0,则使用公式计算F1值 768 | if p + r != 0: 769 | temp_f1 = 2 * p * r / (p + r) 770 | # 否则直接为0 771 | else: 772 | temp_f1 = 0 773 | # 根据样本数分配权重 774 | weighted_f1 = class_counts[class_] * temp_f1 775 | # 加权F1值相加 776 | f1 += weighted_f1 777 | # 计算加权平均F1值 778 | overall_f1 = f1 / len(y_true) 779 | return overall_f1 780 | ``` 781 | 782 | 请注意,上面有几行代码是新写的。因此,你应该仔细阅读这些代码。 783 | 784 | ```python 785 | In [X]: from sklearn import metrics 786 | In [X]: y_true = [0, 1, 2, 0, 1, 2, 0, 2, 2] 787 | In [X]: y_pred = [0, 2, 1, 0, 2, 1, 0, 0, 2] 788 | In [X]: weighted_f1(y_true, y_pred) 789 | Out[X]: 0.41269841269841273 790 | In [X]: metrics.f1_score(y_true, y_pred, average="weighted") 791 | Out[X]: 0.41269841269841273 792 | ``` 793 | 794 | 因此,我们已经为多类问题实现了精确率、召回率和 F1。同样,您也可以将 AUC 和对数损失转换为多类格式。这种转换格式被称为 **one-vs-all**。这里我不打算实现它们,因为实现方法与我们已经讨论过的很相似。 795 | 796 | 在二元或多类分类中,看一下**混淆矩阵**也很流行。不要困惑,这很简单。混淆矩阵只不过是一个包含 TP、FP、TN 和 FN 的表格。使用混淆矩阵,您可以快速查看有多少样本被错误分类,有多少样本被正确分类。也许有人会说,混淆矩阵应该在本章很早就讲到,但我没有这么做。如果了解了 TP、FP、TN、FN、精确率、召回率和 AUC,就很容易理解和解释混淆矩阵了。让我们看看图 7 中二元分类问题的混淆矩阵。 797 | 798 | 我们可以看到,混淆矩阵由 TP、FP、FN 和 TN 组成。我们只需要这些值来计算精确率、召回率、F1 分数和 AUC。有时,人们也喜欢把 FP 称为**第一类错误**,把 FN 称为**第二类错误**。 799 | 800 | ![](figures/AAAMLP_page57_image.png) 801 | 802 |

图 7:二元分类任务的混淆矩阵

803 | 804 | 我们还可以将二元混淆矩阵扩展为多类混淆矩阵。它会是什么样子呢?如果我们有 N 个类别,它将是一个大小为 NxN 的矩阵。对于每个类别,我们都要计算相关类别和其他类别的样本总数。举个例子可以让我们更好地理解这一点。 805 | 806 | 假设我们有以下真实标签: 807 | 808 | $$ 809 | [0, 1, 2, 0, 1, 2, 0, 2, 2] 810 | $$ 811 | 812 | 我们的预测标签是: 813 | 814 | $$ 815 | [0, 2, 1, 0, 2, 1, 0, 0, 2] 816 | $$ 817 | 818 | 那么,我们的混淆矩阵将如图 8 所示。 819 | 820 | ![](figures/AAAMLP_page58_image.png) 821 | 822 |

图 8:多分类问题的混淆矩阵

823 | 824 | 图 8 说明了什么? 825 | 826 | 让我们来看看 0 类。我们看到,在真实标签中,有 3 个样本属于 0 类。然而,在预测中,我们有 3 个样本属于第 0 类,1 个样本属于第 1 类。理想情况下,对于真实标签中的类别 0,预测标签 1 和 2 应该没有任何样本。让我们看看类别 2。在真实标签中,这个数字加起来是 4,而在预测标签中,这个数字加起来是 3。 827 | 828 | 一个完美的混淆矩阵只能从左到右斜向填充。 829 | 830 | **混淆矩阵**提供了一种简单的方法来计算我们之前讨论过的不同指标。Scikit-learn 提供了一种简单直接的方法来生成混淆矩阵。请注意,我在图 8 中显示的混淆矩阵是 scikit-learn 混淆矩阵的转置,原始版本可以通过以下代码绘制。 831 | 832 | ```python 833 | import matplotlib.pyplot as plt 834 | import seaborn as sns 835 | from sklearn import metrics 836 | 837 | # 真实样本标签 838 | y_true = [0, 1, 2, 0, 1, 2, 0, 2, 2] 839 | # 预测样本标签 840 | y_pred = [0, 2, 1, 0, 2, 1, 0, 0, 2] 841 | 842 | # 计算混淆矩阵 843 | cm = metrics.confusion_matrix(y_true, y_pred) 844 | 845 | # 创建画布 846 | plt.figure(figsize=(10, 10)) 847 | # 创建方格 848 | cmap = sns.cubehelix_palette(50, hue=0.05, rot=0, light=0.9, dark=0, 849 | as_cmap=True) 850 | # 规定字体大小 851 | sns.set(font_scale=2.5) 852 | # 绘制热图 853 | sns.heatmap(cm, annot=True, cmap=cmap, cbar=False) 854 | # y轴标签,字体大小为20 855 | plt.ylabel('Actual Labels', fontsize=20) 856 | # x轴标签,字体大小为20 857 | plt.xlabel('Predicted Labels', fontsize=20) 858 | ``` 859 | 860 | 因此,到目前为止,我们已经解决了二元分类和多类分类的度量问题。接下来,我们将讨论另一种类型的分类问题,即多标签分类。在多标签分类中,每个样本都可能与一个或多个类别相关联。这类问题的一个简单例子就是要求你预测给定图像中的不同物体。 861 | 862 | 图 9 显示了一个著名数据集的图像示例。请注意,该数据集的目标有所不同,但我们暂且不去讨论它。我们假设其目的只是预测图像中是否存在某个物体。在图 9 中,我们有椅子、花盆、窗户,但没有其他物体,如电脑、床、电视等。因此,一幅图像可能有多个相关目标。这类问题就是多标签分类问题。 863 | 864 | ![](figures/AAAMLP_page59_image.png) 865 | 866 |

图 9:图像中的不同物体

867 | 868 | 这类分类问题的衡量标准有些不同。一些合适的 最常见的指标有: 869 | 870 | - k 精确率(P@k) 871 | - k 平均精确率(AP@k) 872 | - k 均值平均精确率(MAP@k) 873 | - 对数损失(Log loss) 874 | 875 | 让我们从**k 精确率或者 P@k**我们不能将这一精确率与前面讨论的精确率混淆。如果您有一个给定样本的原始类别列表和同一个样本的预测类别列表,那么精确率的定义就是预测列表中仅考虑前 k 个预测结果的命中数除以 k。 876 | 877 | 如果您对此感到困惑,使用 python 代码后就会明白。 878 | 879 | ```python 880 | def pk(y_true, y_pred, k): 881 | # 如果k为0 882 | if k == 0: 883 | # 返回0 884 | return 0 885 | # 取预测标签前k个 886 | y_pred = y_pred[:k] 887 | # 将预测标签转换为集合 888 | pred_set = set(y_pred) 889 | # 将真实标签转换为集合 890 | true_set = set(y_true) 891 | # 预测标签集合与真实标签集合交集 892 | common_values = pred_set.intersection(true_set) 893 | # 计算精确率 894 | return len(common_values) / len(y_pred[:k]) 895 | ``` 896 | 897 | 有了代码,一切都变得更容易理解了。 898 | 899 | 现在,我们有了**k 平均精确率或 AP@k**。AP@k 是通过 P@k 计算得出的。例如,如果要计算 AP@3,我们要先计算 P@1、P@2 和 P@3,然后将总和除以 3。 900 | 901 | 让我们来看看它的实现。 902 | 903 | ```python 904 | def apk(y_true, y_pred, k): 905 | # 初始化P@k列表 906 | pk_values = [] 907 | # 遍历1~k 908 | for i in range(1, k + 1): 909 | # 将P@k加入列表 910 | pk_values.append(pk(y_true, y_pred, i)) 911 | # 若长度为0 912 | if len(pk_values) == 0: 913 | # 返回0 914 | return 0 915 | # 否则计算AP@K 916 | return sum(pk_values) / len(pk_values) 917 | ``` 918 | 919 | 这两个函数可以用来计算两个给定列表的 k 平均精确率 (AP@k);让我们看看如何计算。 920 | 921 | ```python 922 | In [X]: y_true = [ 923 | ...: [1, 2, 3], 924 | ...: [0, 2], 925 | ...: [1], 926 | ...: [2, 3], 927 | ...: [1, 0], 928 | ...: [] 929 | ...: ] 930 | In [X]: y_pred = [ 931 | ...: [0, 1, 2], 932 | ...: [1], 933 | ...: [0, 2, 3], 934 | ...: [2, 3, 4, 0], 935 | ...: [0, 1, 2], 936 | ...: [0] 937 | ...: ] 938 | In [X]: for i in range(len(y_true)): 939 | ...: for j in range(1, 4): 940 | ...: print( 941 | ...: f""" 942 | ...: y_true={y_true[i]}, 943 | ...: y_pred={y_pred[i]}, 944 | ...: AP@{j}={apk(y_true[i], y_pred[i], k=j)} 945 | ...: """ 946 | ...: ) 947 | ...: 948 | y_true=[1, 2, 3], 949 | y_pred=[0, 1, 2], 950 | AP@1=0.0 951 | y_true=[1, 2, 3], 952 | y_pred=[0, 1, 2], 953 | AP@2=0.25 954 | y_true=[1, 2, 3], 955 | y_pred=[0, 1, 2], 956 | AP@3=0.38888888888888884 957 | ``` 958 | 959 | 请注意,我省略了输出结果中的许多数值,但你会明白其中的意思。这就是我们如何计算 AP@k 的方法,即每个样本的 AP@k。在机器学习中,我们对所有样本都感兴趣,这就是为什么我们有**均值平均精确率 k 或 MAP@k**。MAP@k 只是 AP@k 的平均值,可以通过以下 python 代码轻松计算。 960 | 961 | ```python 962 | def mapk(y_true, y_pred, k): 963 | # 初始化AP@k列表 964 | apk_values = [] 965 | # 遍历0~(真实标签数-1) 966 | for i in range(len(y_true)): 967 | # 将AP@K加入列表 968 | apk_values.append( 969 | apk(y_true[i], y_pred[i], k=k) 970 | ) 971 | # 计算平均AP@k 972 | return sum(apk_values) / len(apk_values) 973 | ``` 974 | 975 | 现在,我们可以针对相同的列表计算 k=1、2、3 和 4 时的 MAP@k。 976 | 977 | ```python 978 | In [X]: y_true = [ 979 | ...: [1, 2, 3], 980 | ...: [0, 2], 981 | ...: [1], 982 | ...: [2, 3], 983 | ...: [1, 0], 984 | ...: [] 985 | ...: ] 986 | In [X]: y_pred = [ 987 | ...: [0, 1, 2], 988 | ...: [1], 989 | ...: [0, 2, 3], 990 | ...: [2, 3, 4, 0], 991 | ...: [0, 1, 2], 992 | ...: [0] 993 | ...: ] 994 | In [X]: mapk(y_true, y_pred, k=1) 995 | Out[X]: 0.3333333333333333 996 | In [X]: mapk(y_true, y_pred, k=2) 997 | Out[X]: 0.375 998 | In [X]: mapk(y_true, y_pred, k=3) 999 | Out[X]: 0.3611111111111111 1000 | In [X]: mapk(y_true, y_pred, k=4) 1001 | Out[X]: 0.34722222222222215 1002 | ``` 1003 | 1004 | P@k、AP@k 和 MAP@k 的范围都是从 0 到 1,其中 1 为最佳。 1005 | 1006 | 请注意,有时您可能会在互联网上看到 P@k 和 AP@k 的不同实现方式。 例如,让我们来看看其中一种实现方式。 1007 | 1008 | ```python 1009 | import numpy as np 1010 | def apk(actual, predicted, k=10): 1011 | # 若预测标签长度大于k 1012 | if len(predicted)>k: 1013 | # 取前k个标签 1014 | predicted = predicted[:k] 1015 | 1016 | score = 0.0 1017 | num_hits = 0.0 1018 | 1019 | for i,p in enumerate(predicted): 1020 | if p in actual and p not in predicted[:i]: 1021 | num_hits += 1.0 1022 | score += num_hits / (i+1.0) 1023 | if not actual: 1024 | return 0.0 1025 | return score / min(len(actual), k) 1026 | ``` 1027 | 1028 | 这种实现方式是 AP@k 的另一个版本,其中顺序很重要,我们要权衡预测结果。这种实现方式的结果与我的介绍略有不同。 1029 | 1030 | 现在,我们来看看**多标签分类的对数损失**。这很容易。您可以将目标转换为二元分类,然后对每一列使用对数损失。最后,你可以求出每列对数损失的平均值。这也被称为平均列对数损失。当然,还有其他方法可以实现这一点,你应该在遇到时加以探索。 1031 | 1032 | 我们现在可以说已经掌握了所有二元分类、多类分类和多标签分类指标,现在我们可以转向回归指标。 1033 | 1034 | 回归中最常见的指标是**误差(Error)**。误差很简单,也很容易理解。 1035 | 1036 | $$ 1037 | Error = True\ Value - Predicted\ Value 1038 | $$ 1039 | 1040 | **绝对误差(Absolute error)**只是上述误差的绝对值。 1041 | 1042 | $$ 1043 | Absolute\ Error = Abs(True\ Value - Predicted\ Value) 1044 | $$ 1045 | 1046 | 接下来我们讨论**平均绝对误差(MAE)**。它只是所有绝对误差的平均值。 1047 | 1048 | ```python 1049 | import numpy as np 1050 | def mean_absolute_error(y_true, y_pred): 1051 | #初始化误差 1052 | error = 0 1053 | # 遍历y_true, y_pred 1054 | for yt, yp in zip(y_true, y_pred): 1055 | # 累加绝对误差 1056 | error += np.abs(yt - yp) 1057 | # 返回平均绝对误差 1058 | return error / len(y_true) 1059 | ``` 1060 | 1061 | 同样,我们还有平方误差和**均方误差 (MSE)**。 1062 | 1063 | $$ 1064 | Squared\ Error = (True Value - Predicted\ Value)^2 1065 | $$ 1066 | 1067 | 均方误差(MSE)的计算方式如下 1068 | 1069 | ```python 1070 | def mean_squared_error(y_true, y_pred): 1071 | # 初始化误差 1072 | error = 0 1073 | # 遍历y_true, y_pred 1074 | for yt, yp in zip(y_true, y_pred): 1075 | # 累加误差平方和 1076 | error += (yt - yp) ** 2 1077 | # 计算均方误差 1078 | return error / len(y_true) 1079 | ``` 1080 | 1081 | MSE 和 **RMSE(均方根误差)**是评估回归模型最常用的指标。 1082 | 1083 | $$ 1084 | RMSE = SQRT(MSE) 1085 | $$ 1086 | 1087 | 同一类误差的另一种类型是**平方对数误差**。有人称其为 **SLE**,当我们取所有样本中这一误差的平均值时,它被称为 MSLE(平均平方对数误差),实现方法如下。 1088 | 1089 | ```python 1090 | import numpy as np 1091 | def mean_squared_log_error(y_true, y_pred): 1092 | # 初始化误差 1093 | error = 0 1094 | # 遍历y_true, y_pred 1095 | for yt, yp in zip(y_true, y_pred): 1096 | # 计算平方对数误差 1097 | error += (np.log(1 + yt) - np.log(1 + yp)) ** 2 1098 | # 计算平均平方对数误差 1099 | return error / len(y_true) 1100 | ``` 1101 | 1102 | **均方根对数误差**只是其平方根。它也被称为 **RMSLE**。 1103 | 1104 | 然后是百分比误差: 1105 | 1106 | $$ 1107 | Percentage\ Error = (( True\ Value – Predicted\ Value ) / True\ Value ) \times 100 1108 | $$ 1109 | 1110 | 同样可以转换为所有样本的平均百分比误差。 1111 | 1112 | ```python 1113 | def mean_percentage_error(y_true, y_pred): 1114 | # 初始化误差 1115 | error = 0 1116 | # 遍历y_true, y_pred 1117 | for yt, yp in zip(y_true, y_pred): 1118 | # 计算百分比误差 1119 | error += (yt - yp) / yt 1120 | # 返回平均百分比误差 1121 | return error / len(y_true) 1122 | ``` 1123 | 1124 | 绝对误差的绝对值(也是更常见的版本)被称为**平均绝对百分比误差或 MAPE**。 1125 | 1126 | ```python 1127 | import numpy as np 1128 | def mean_abs_percentage_error(y_true, y_pred): 1129 | # 初始化误差 1130 | error = 0 1131 | # 遍历y_true, y_pred 1132 | for yt, yp in zip(y_true, y_pred): 1133 | # 计算绝对百分比误差 1134 | error += np.abs(yt - yp) / yt 1135 | #返回平均绝对百分比误差 1136 | return error / len(y_true) 1137 | ``` 1138 | 1139 | 回归的最大优点是,只有几个最常用的指标,几乎可以应用于所有回归问题。与分类指标相比,回归指标更容易理解。 1140 | 1141 | 让我们来谈谈另一个回归指标 $R^2$(R 方),也称为**判定系数**。 1142 | 1143 | 简单地说,R 方表示模型与数据的拟合程度。R 方接近 1.0 表示模型与数据的拟合程度相当好,而接近 0 则表示模型不是那么好。当模型只是做出荒谬的预测时,R 方也可能是负值。 1144 | 1145 | R 方的计算公式如下所示,但 Python 的实现总是能让一切更加清晰。 1146 | 1147 | $$ 1148 | R^2 = \frac{\sum^{N}_{i=1}(y_{t_i}-y_{p_i})^2}{\sum^{N}_{i=1}(y_{t_i} - y_{t_{mean}})} 1149 | $$ 1150 | 1151 | ```python 1152 | import numpy as np 1153 | def r2(y_true, y_pred): 1154 | # 计算平均真实值 1155 | mean_true_value = np.mean(y_true) 1156 | # 初始化平方误差 1157 | numerator = 0 1158 | denominator = 0 1159 | # 遍历y_true, y_pred 1160 | for yt, yp in zip(y_true, y_pred): 1161 | numerator += (yt - yp) ** 2 1162 | denominator += (yt - mean_true_value) ** 2 1163 | ratio = numerator / denominator 1164 | # 计算R方 1165 | return 1 – ratio 1166 | ``` 1167 | 1168 | 还有更多的评价指标,这个清单永远也列不完。我可以写一本书,只介绍不同的评价指标。也许我会的。现在,这些评估指标几乎可以满足你想尝试解决的所有问题。请注意,我已经以最直接的方式实现了这些指标,这意味着它们不够高效。你可以通过正确使用 numpy 以非常高效的方式实现其中大部分指标。例如,看看平均绝对误差的实现,不需要任何循环。 1169 | 1170 | ```python 1171 | import numpy as np 1172 | def mae_np(y_true, y_pred): 1173 | return np.mean(np.abs(y_true - y_pred)) 1174 | ``` 1175 | 1176 | 我本可以用这种方法实现所有指标,但为了学习,最好还是看看底层实现。一旦你学会了纯 python 的底层实现,并且不使用大量 numpy,你就可以很容易地将其转换为 numpy,并使其变得更快。 1177 | 1178 | 然后是一些高级度量。 1179 | 1180 | 其中一个应用相当广泛的指标是**二次加权卡帕**,也称为 **QWK**。它也被称为科恩卡帕。**QWK** 衡量两个 "评分 "之间的 "一致性"。评分可以是 0 到 N 之间的任何实数,预测也在同一范围内。一致性可以定义为这些评级之间的接近程度。因此,它适用于有 N 个不同类别的分类问题。如果一致度高,分数就更接近 1.0。Cohen's kappa 在 scikit-learn 中有很好的实现,关于该指标的详细讨论超出了本书的范围。 1181 | 1182 | ```python 1183 | In [X]: from sklearn import metrics 1184 | In [X]: y_true = [1, 2, 3, 1, 2, 3, 1, 2, 3] 1185 | In [X]: y_pred = [2, 1, 3, 1, 2, 3, 3, 1, 2] 1186 | In [X]: metrics.cohen_kappa_score(y_true, y_pred, weights="quadratic") 1187 | Out[X]: 0.33333333333333337 1188 | In [X]: metrics.accuracy_score(y_true, y_pred) 1189 | Out[X]: 0.4444444444444444 1190 | ``` 1191 | 1192 | 您可以看到,尽管准确度很高,但 QWK 却很低。QWK 大于 0.85 即为非常好! 1193 | 1194 | 一个重要的指标是**马修相关系数(MCC)**。1 代表完美预测,-1 代表不完美预测,0 代表随机预测。MCC 的计算公式非常简单。 1195 | 1196 | $$ 1197 | MCC = \frac{TP \times TN - FP \times FN}{\sqrt{(TP + FP) \times (FN + TN) \times (FP + TN) \times (TP + FN)}} 1198 | $$ 1199 | 1200 | 我们看到,MCC 考虑了 TP、FP、TN 和 FN,因此可用于处理类偏斜的问题。您可以使用我们已经实现的方法在 python 中快速实现它。 1201 | 1202 | ```python 1203 | def mcc(y_true, y_pred): 1204 | # 真阳性样本数 1205 | tp = true_positive(y_true, y_pred) 1206 | # 真阴性样本数 1207 | tn = true_negative(y_true, y_pred) 1208 | # 假阳性样本数 1209 | fp = false_positive(y_true, y_pred) 1210 | # 假阴性样本数 1211 | fn = false_negative(y_true, y_pred) 1212 | numerator = (tp * tn) - (fp * fn) 1213 | denominator = ( 1214 | (tp + fp) * 1215 | (fn + tn) * 1216 | (fp + tn) * 1217 | (tp + fn) 1218 | ) 1219 | denominator = denominator ** 0.5 1220 | return numerator/denominator 1221 | ``` 1222 | 1223 | 这些指标可以帮助你入门,几乎适用于所有机器学习问题。 1224 | 1225 | 需要注意的一点是,在评估非监督方法(例如某种聚类)时,最好创建或手动标记测试集,并将其与建模部分的所有内容分开。完成聚类后,就可以使用任何一种监督学习指标来评估测试集的性能了。 1226 | 1227 | 一旦我们了解了特定问题应该使用什么指标,我们就可以开始更深入地研究我们的模型,以求改进。 1228 | -------------------------------------------------------------------------------- /docs/超参数优化.md: -------------------------------------------------------------------------------- 1 | # 超参数优化 2 | 3 | 有了优秀的模型,就有了优化超参数以获得最佳得分模型的难题。那么,什么是超参数优化呢?假设您的机器学习项目有一个简单的流程。有一个数据集,你直接应用一个模型,然后得到结果。模型在这里的参数被称为超参数,即控制模型训练/拟合过程的参数。如果我们用 SGD 训练线性回归,模型的参数是斜率和偏差,超参数是学习率。你会发现我在本章和本书中交替使用这些术语。假设模型中有三个参数 a、b、c,所有这些参数都可以是 1 到 10 之间的整数。这些参数的 "正确 "组合将为您提供最佳结果。因此,这就有点像一个装有三拨密码锁的手提箱。不过,三拨密码锁只有一个正确答案。而模型有很多正确答案。那么,如何找到最佳参数呢?一种方法是对所有组合进行评估,看哪种组合能提高指标。让我们看看如何做到这一点。 4 | 5 | ```python 6 | # 初始化最佳准确度 7 | best_accuracy = 0 8 | # 初始化最佳参数的字典 9 | best_parameters = {"a": 0, "b": 0, "c": 0} 10 | # 循环遍历 a 的取值范围 1~10 11 | for a in range(1, 11): 12 | # 循环遍历 b 的取值范围 1~10 13 | for b in range(1, 11): 14 | # 循环遍历 c 的取值范围 1~10 15 | for c in range(1, 11): 16 | # 创建模型,使用 a、b、c 参数 17 | model = MODEL(a, b, c) 18 | # 使用训练数据拟合模型 19 | model.fit(training_data) 20 | # 使用模型对验证数据进行预测 21 | preds = model.predict(validation_data) 22 | # 计算预测的准确度 23 | accuracy = metrics.accuracy_score(targets, preds) 24 | # 如果当前准确度优于之前的最佳准确度,则更新最佳准确度和最佳参数 25 | if accuracy > best_accuracy: 26 | best_accuracy = accuracy 27 | best_parameters["a"] = a 28 | best_parameters["b"] = b 29 | best_parameters["c"] = c 30 | ``` 31 | 32 | 在上述代码中,我们从 1 到 10 对所有参数进行了拟合。因此,我们总共要对模型进行 1000 次(10 x 10 x 10)拟合。这可能会很昂贵,因为模型的训练需要很长时间。不过,在这种情况下应该没问题,但在现实世界中,并不是只有三个参数,每个参数也不是只有十个值。 大多数模型参数都是实数,不同参数的组合可以是无限的。 33 | 34 | 让我们看看 scikit-learn 的随机森林模型。 35 | 36 | ```python 37 | RandomForestClassifier( 38 | n_estimators=100, 39 | criterion='gini', 40 | max_depth=None, 41 | min_samples_split=2, 42 | min_samples_leaf=1, 43 | min_weight_fraction_leaf=0.0, 44 | max_features='auto', 45 | max_leaf_nodes=None, 46 | min_impurity_decrease=0.0, 47 | min_impurity_split=None, 48 | bootstrap=True, 49 | oob_score=False, 50 | n_jobs=None, 51 | random_state=None, 52 | verbose=0, 53 | warm_start=False, 54 | class_weight=None, 55 | ccp_alpha=0.0, 56 | max_samples=None, 57 | ) 58 | ``` 59 | 60 | 有 19 个参数,而所有这些参数的所有组合,以及它们可以承担的所有值,都将是无穷无尽的。通常情况下,我们没有足够的资源和时间来做这件事。因此,我们指定了一个参数网格。在这个网格上寻找最佳参数组合的搜索称为网格搜索。我们可以说,n_estimators 可以是 100、200、250、300、400、500;max_depth 可以是 1、2、5、7、11、15;criterion 可以是 gini 或 entropy。这些参数看起来并不多,但如果数据集过大,计算起来会耗费大量时间。我们可以像之前一样创建三个 for 循环,并在验证集上计算得分,这样就能实现网格搜索。还必须注意的是,如果要进行 k 折交叉验证,则需要更多的循环,这意味着需要更多的时间来找到完美的参数。因此,网格搜索并不流行。让我们以根据**手机配置预测手机价格范围**数据集为例,看看它是如何实现的。 61 | 62 | ![](figures/AAAMLP_page168_image.png) 63 | 64 |

图 1:手机配置预测手机价格范围数据集展示

65 | 66 | 训练集中只有 2000 个样本。我们可以轻松地使用分层 kfold 和准确率作为评估指标。我们将使用具有上述参数范围的随机森林模型,并在下面的示例中了解如何进行网格搜索。 67 | 68 | ```python 69 | # rf_grid_search.py 70 | import numpy as np 71 | import pandas as pd 72 | from sklearn import ensemble 73 | from sklearn import metrics 74 | from sklearn import model_selection 75 | if __name__ == "__main__": 76 | # 读取数据 77 | df = pd.read_csv("../input/mobile_train.csv") 78 | # 删除 price_range 列 79 | X = df.drop("price_range", axis=1).values 80 | # 取目标变量 y("price_range"列) 81 | y = df.price_range.values 82 | # 创建随机森林分类器,使用所有可用的 CPU 核心进行训练 83 | classifier = ensemble.RandomForestClassifier(n_jobs=-1) 84 | # 定义要进行网格搜索的参数网格 85 | param_grid = { 86 | "n_estimators": [100, 200, 250, 300, 400, 500], 87 | "max_depth": [1, 2, 5, 7, 11, 15], 88 | "criterion": ["gini", "entropy"] 89 | } 90 | # 创建 GridSearchCV 对象 model,用于在参数网格上进行网格搜索 91 | model = model_selection.GridSearchCV( 92 | estimator=classifier, 93 | param_grid=param_grid, 94 | scoring="accuracy", 95 | verbose=10, 96 | n_jobs=1, 97 | cv=5 98 | ) 99 | # 使用网格搜索对象 model 拟合数据,寻找最佳参数组合 100 | model.fit(X, y) 101 | # 打印出最佳模型的最佳准确度分数 102 | print(f"Best score: {model.best_score_}") 103 | # 打印最佳参数集合 104 | print("Best parameters set:") 105 | best_parameters = model.best_estimator_.get_params() 106 | for param_name in sorted(param_grid.keys()): 107 | print(f"\t{param_name}: {best_parameters[param_name]}") 108 | ``` 109 | 110 | 这里打印了很多内容,让我们看看最后几行。 111 | 112 | ```python 113 | [CV] criterion=entropy, max_depth=15, n_estimators=500, score=0.895, 114 | total= 1.0s 115 | [CV] criterion=entropy, max_depth=15, n_estimators=500 ............... 116 | [CV] criterion=entropy, max_depth=15, n_estimators=500, score=0.890, 117 | total= 1.1s 118 | [CV] criterion=entropy, max_depth=15, n_estimators=500 ............... 119 | [CV] criterion=entropy, max_depth=15, n_estimators=500, score=0.910, 120 | total= 1.1s 121 | [CV] criterion=entropy, max_depth=15, n_estimators=500 ............... 122 | [CV] criterion=entropy, max_depth=15, n_estimators=500, score=0.880, 123 | total= 1.1s 124 | [CV] criterion=entropy, max_depth=15, n_estimators=500 ............... 125 | [CV] criterion=entropy, max_depth=15, n_estimators=500, score=0.870, 126 | total= 1.1s 127 | [Parallel(n_jobs=1)]: Done 360 out of 360 | elapsed: 3.7min finished 128 | Best score: 0.889 129 | Best parameters set: 130 | criterion: 'entropy' 131 | max_depth: 15 132 | n_estimators: 500 133 | ``` 134 | 135 | 最后,我们可以看到,5折交叉检验最佳得分是 0.889,我们的网格搜索得到了最佳参数。我们可以使用的下一个最佳方法是**随机搜索**。在随机搜索中,我们随机选择一个参数组合,然后计算交叉验证得分。这里消耗的时间比网格搜索少,因为我们不对所有不同的参数组合进行评估。我们选择要对模型进行多少次评估,这就决定了搜索所需的时间。代码与上面的差别不大。除 GridSearchCV 外,我们使用 RandomizedSearchCV。 136 | 137 | ```python 138 | if __name__ == "__main__": 139 | classifier = ensemble.RandomForestClassifier(n_jobs=-1) 140 | # 更改搜索空间 141 | param_grid = { 142 | "n_estimators": np.arange(100, 1500, 100), 143 | "max_depth": np.arange(1, 31), 144 | "criterion": ["gini", "entropy"] 145 | } 146 | # 随机参数搜索 147 | model = model_selection.RandomizedSearchCV( 148 | estimator=classifier, 149 | param_distributions=param_grid, 150 | n_iter=20, 151 | scoring="accuracy", 152 | verbose=10, 153 | n_jobs=1, 154 | cv=5 155 | ) 156 | # 使用网格搜索对象 model 拟合数据,寻找最佳参数组合 157 | model.fit(X, y) 158 | print(f"Best score: {model.best_score_}") 159 | print("Best parameters set:") 160 | best_parameters = model.best_estimator_.get_params() 161 | for param_name in sorted(param_grid.keys()): 162 | print(f"\t{param_name}: {best_parameters[param_name]}") 163 | ``` 164 | 165 | 我们更改了随机搜索的参数网格,结果似乎有了些许改进。 166 | 167 | ```python 168 | Best score: 0.8905 169 | Best parameters set: 170 | criterion: entropy 171 | max_depth: 25 172 | n_estimators: 300 173 | ``` 174 | 175 | 如果迭代次数较少,随机搜索比网格搜索更快。使用这两种方法,你可以为各种模型找到最优参数,只要它们有拟合和预测功能,这也是 scikit-learn 的标准。有时,你可能想使用管道。例如,假设我们正在处理一个多类分类问题。在这个问题中,训练数据由两列文本组成,你需要建立一个模型来预测类别。让我们假设你选择的管道是首先以半监督的方式应用 tf-idf,然后使用 SVD 和 SVM 分类器。现在的问题是,我们必须选择 SVD 的成分,还需要调整 SVM 的参数。下面的代码段展示了如何做到这一点。 176 | 177 | ```python 178 | import numpy as np 179 | import pandas as pd 180 | from sklearn import metrics 181 | from sklearn import model_selection 182 | from sklearn import pipeline 183 | from sklearn.decomposition import TruncatedSVD 184 | from sklearn.feature_extraction.text import TfidfVectorizer 185 | from sklearn.preprocessing import StandardScaler 186 | from sklearn.svm import SVC 187 | # 计算加权二次 Kappa 分数 188 | def quadratic_weighted_kappa(y_true, y_pred): 189 | return metrics.cohen_kappa_score( 190 | y_true, 191 | y_pred, 192 | weights="quadratic" 193 | ) 194 | 195 | if __name__ == '__main__': 196 | # 读取训练集 197 | train = pd.read_csv('../input/train.csv') 198 | # 从测试数据中提取 id 列的值,并将其转换为整数类型,存储在变量 idx 中 199 | idx = test.id.values.astype(int) 200 | 201 | # 从训练数据中删除 'id' 列 202 | train = train.drop('id', axis=1) 203 | # 从测试数据中删除 'id' 列 204 | test = test.drop('id', axis=1) 205 | # 从训练数据中提取目标变量 'relevance' ,存储在变量 y 中 206 | y = train.relevance.values 207 | 208 | # 将训练数据中的文本特征 'text1' 和 'text2' 合并成一个新的特征列,并存储在列表 traindata 中 209 | traindata = list(train.apply(lambda x:'%s %s' % (x['text1'], x['text2']),axis=1)) 210 | # 将测试数据中的文本特征 'text1' 和 'text2' 合并成一个新的特征列,并存储在列表 testdata 中 211 | testdata = list(test.apply(lambda x:'%s %s' % (x['text1'], x['text2']),axis=1)) 212 | 213 | # 创建一个 TfidfVectorizer 对象 tfv,用于将文本数据转换为 TF-IDF 特征 214 | tfv = TfidfVectorizer( 215 | min_df=3, 216 | max_features=None, 217 | strip_accents='unicode', 218 | analyzer='word', 219 | token_pattern=r'\w{1,}', 220 | ngram_range=(1, 3), 221 | use_idf=1, 222 | smooth_idf=1, 223 | sublinear_tf=1, 224 | stop_words='english' 225 | ) 226 | 227 | # 使用训练数据拟合 TfidfVectorizer,将文本特征转换为 TF-IDF 特征 228 | tfv.fit(traindata) 229 | # 将训练数据中的文本特征转换为 TF-IDF 特征矩阵 X 230 | X = tfv.transform(traindata) 231 | # 将测试数据中的文本特征转换为 TF-IDF 特征矩阵 X_test 232 | X_test = tfv.transform(testdata) 233 | # 创建 TruncatedSVD 对象 svd,用于进行奇异值分解 234 | svd = TruncatedSVD() 235 | # 创建 StandardScaler 对象 scl,用于进行特征缩放 236 | scl = StandardScaler() 237 | # 创建支持向量机分类器对象 svm_model 238 | svm_model = SVC() 239 | 240 | # 创建机器学习管道 clf,包含奇异值分解、特征缩放和支持向量机分类器 241 | clf = pipeline.Pipeline( 242 | [ 243 | ('svd', svd), 244 | ('scl', scl), 245 | ('svm', svm_model) 246 | ] 247 | ) 248 | 249 | # 定义要进行网格搜索的参数网格 param_grid 250 | param_grid = { 251 | 'svd__n_components' : [200, 300], 252 | 'svm__C': [10, 12] 253 | } 254 | 255 | # 创建自定义的评分函数 kappa_scorer,用于评估模型性能 256 | kappa_scorer = metrics.make_scorer( 257 | quadratic_weighted_kappa, 258 | greater_is_better=True 259 | ) 260 | 261 | # 创建 GridSearchCV 对象 model,用于在参数网格上进行网格搜索,寻找最佳参数组合 262 | model = model_selection.GridSearchCV( 263 | estimator=clf, 264 | param_grid=param_grid, 265 | scoring=kappa_scorer, 266 | verbose=10, 267 | n_jobs=-1, 268 | refit=True, 269 | cv=5 270 | ) 271 | 272 | # 使用 GridSearchCV 对象 model 拟合数据,寻找最佳参数组合 273 | model.fit(X, y) 274 | # 打印出最佳模型的最佳准确度分数 275 | print("Best score: %0.3f" % model.best_score_) 276 | # 打印最佳参数集合 277 | print("Best parameters set:") 278 | best_parameters = model.best_estimator_.get_params() 279 | for param_name in sorted(param_grid.keys()): 280 | print("\t%s: %r" % (param_name, best_parameters[param_name])) 281 | 282 | # 获取最佳模型 283 | best_model = model.best_estimator_ 284 | best_model.fit(X, y) 285 | # 使用最佳模型进行预测 286 | preds = best_model.predict(...) 287 | ``` 288 | 289 | 这里显示的管道包括 SVD(奇异值分解)、标准缩放和 SVM(支持向量机)模型。请注意,由于没有训练数据,您无法按原样运行上述代码。当我们进入高级超参数优化技术时,我们可以使用不同类型的**最小化算法**来研究函数的最小化。这可以通过使用多种最小化函数来实现,如下坡单纯形算法、内尔德-梅德优化算法、使用贝叶斯技术和高斯过程寻找最优参数或使用遗传算法。我将在 "集合与堆叠(ensembling and stacking) "一章中详细介绍下坡单纯形算法和 Nelder-Mead 算法的应用。首先,让我们看看高斯过程如何用于超参数优化。这类算法需要一个可以优化的函数。大多数情况下,都是最小化这个函数,就像我们最小化损失一样。 290 | 291 | 因此,比方说,你想找到最佳参数以获得最佳准确度,显然,准确度越高越好。现在,我们不能最小化精确度,但我们可以将精确度乘以-1。这样,我们是在最小化精确度的负值,但事实上,我们是在最大化精确度。 在高斯过程中使用贝叶斯优化,可以使用 scikit-optimize (skopt) 库中的 gp_minimize 函数。让我们看看如何使用该函数调整随机森林模型的参数。 292 | 293 | ```python 294 | import numpy as np 295 | import pandas as pd 296 | from functools import partial 297 | from sklearn import ensemble 298 | from sklearn import metrics 299 | from sklearn import model_selection 300 | from skopt import gp_minimize 301 | from skopt import space 302 | def optimize(params, param_names, x, y): 303 | # 将参数名称和对应的值打包成字典 304 | params = dict(zip(param_names, params)) 305 | # 创建随机森林分类器模型,使用传入的参数配置 306 | model = ensemble.RandomForestClassifier(**params) 307 | # 创建 StratifiedKFold 交叉验证对象,将数据分为 5 折 308 | kf = model_selection.StratifiedKFold(n_splits=5) 309 | 310 | # 初始化用于存储每个折叠的准确度的列表 311 | accuracies = [] 312 | 313 | # 循环遍历每个折叠的训练和测试数据 314 | for idx in kf.split(X=x, y=y): 315 | train_idx, test_idx = idx[0], idx[1] 316 | 317 | xtrain = x[train_idx] 318 | ytrain = y[train_idx] 319 | 320 | xtest = x[test_idx] 321 | ytest = y[test_idx] 322 | 323 | # 在训练数据上拟合模型 324 | model.fit(xtrain, ytrain) 325 | 326 | # 使用模型对测试数据进行预测 327 | preds = model.predict(xtest) 328 | 329 | # 计算折叠的准确度 330 | fold_accuracy = metrics.accuracy_score(ytest, preds) 331 | accuracies.append(fold_accuracy) 332 | 333 | # 返回平均准确度的负数(因为 skopt 使用负数来最小化目标函数) 334 | return -1 * np.mean(accuracies) 335 | 336 | if __name__ == "__main__": 337 | # 读取数据 338 | df = pd.read_csv("../input/mobile_train.csv") 339 | # 取特征矩阵 X(去掉"price_range"列) 340 | X = df.drop("price_range", axis=1).values 341 | # 目标变量 y("price_range"列) 342 | y = df.price_range.values 343 | 344 | # 定义超参数搜索空间 param_space 345 | param_space = [ 346 | space.Integer(3, 15, name="max_depth"), 347 | space.Integer(100, 1500, name="n_estimators"), 348 | space.Categorical(["gini", "entropy"], name="criterion"), 349 | space.Real(0.01, 1, prior="uniform", name="max_features") 350 | ] 351 | 352 | # 定义超参数的名称列表 param_names 353 | param_names = [ 354 | "max_depth", 355 | "n_estimators", 356 | "criterion", 357 | "max_features" 358 | ] 359 | 360 | # 创建函数 optimization_function,用于传递给 gp_minimize 361 | optimization_function = partial( 362 | optimize, 363 | param_names=param_names, 364 | x=X, 365 | y=y 366 | ) 367 | 368 | # 使用 Bayesian Optimization(基于贝叶斯优化)来搜索最佳超参数 369 | result = gp_minimize( 370 | optimization_function, 371 | dimensions=param_space, 372 | n_calls=15, 373 | n_random_starts=10, 374 | verbose=10 375 | ) 376 | 377 | # 获取最佳超参数的字典 378 | best_params = dict( 379 | zip( 380 | param_names, 381 | result.x 382 | ) 383 | ) 384 | # 打印出找到的最佳超参数 385 | print(best_params) 386 | ``` 387 | 388 | 这同样会产生大量输出,最后一部分如下所示。 389 | 390 | ```python 391 | Iteration No: 14 started. Searching for the next optimal point. 392 | Iteration No: 14 ended. Search finished for the next optimal point. 393 | Time taken: 4.7793 394 | Function value obtained: -0.9075 395 | Current minimum: -0.9075 396 | Iteration No: 15 started. Searching for the next optimal point. 397 | Iteration No: 15 ended. Search finished for the next optimal point. 398 | Time taken: 49.4186 399 | Function value obtained: -0.9075 400 | Current minimum: -0.9075 401 | {'max_depth': 12, 'n_estimators': 100, 'criterion': 'entropy', 402 | 'max_features': 1.0} 403 | ``` 404 | 405 | 看来我们已经成功突破了 0.90 的准确率。这真是太神奇了! 406 | 我们还可以通过以下代码段查看(绘制)我们是如何实现收敛的。 407 | 408 | ```python 409 | from skopt.plots import plot_convergence 410 | plot_convergence(result) 411 | ``` 412 | 413 | 收敛图如图 2 所示。 414 | 415 | ![](figures/AAAMLP_page179_image.png) 416 | 417 |

图 2:随机森林参数优化的收敛图

418 | 419 | Scikit- optimize 就是这样一个库。 hyperopt 使用树状结构贝叶斯估计器(TPE)来找到最优参数。请看下面的代码片段,我在使用 hyperopt 时对之前的代码做了最小的改动。 420 | 421 | ```python 422 | import numpy as np 423 | import pandas as pd 424 | from functools import partial 425 | from sklearn import ensemble 426 | from sklearn import metrics 427 | from sklearn import model_selection 428 | from hyperopt import hp, fmin, tpe, Trials 429 | from hyperopt.pyll.base import scope 430 | def optimize(params, x, y): 431 | model = ensemble.RandomForestClassifier(**params) 432 | kf = model_selection.StratifiedKFold(n_splits=5) 433 | ... 434 | return -1 * np.mean(accuracies) 435 | 436 | if __name__ == "__main__": 437 | df = pd.read_csv("../input/mobile_train.csv") 438 | X = df.drop("price_range", axis=1).values 439 | y = df.price_range.values 440 | # 定义搜索空间(整型、浮点数型、选择型) 441 | param_space = { 442 | "max_depth": scope.int(hp.quniform("max_depth", 1, 15, 1)), 443 | "n_estimators": scope.int( 444 | hp.quniform("n_estimators", 100, 1500, 1) 445 | ), 446 | "criterion": hp.choice("criterion", ["gini", "entropy"]), 447 | "max_features": hp.uniform("max_features", 0, 1) 448 | } 449 | 450 | # 包装函数 451 | optimization_function = partial( 452 | optimize, 453 | x=X, 454 | y=y 455 | ) 456 | 457 | # 开始训练 458 | trials = Trials() 459 | 460 | # 最小化目标值 461 | hopt = fmin( 462 | fn=optimization_function, 463 | space=param_space, 464 | algo=tpe.suggest, 465 | max_evals=15, 466 | trials=trials 467 | ) 468 | #打印最佳参数 469 | print(hopt) 470 | ``` 471 | 472 | 正如你所看到的,这与之前的代码并无太大区别。你必须以不同的格式定义参数空间,还需要改变实际优化部分,用 hyperopt 代替 gp_minimize。结果相当不错! 473 | 474 | ```python 475 | ❯ python rf_hyperopt.py 476 | 100%|██████████████████| 15/15 [04:38<00:00, 18.57s/trial, best loss: - 477 | 0.9095000000000001] 478 | {'criterion': 1, 'max_depth': 11.0, 'max_features': 0.821163568049807, 479 | 'n_estimators': 806.0} 480 | ``` 481 | 482 | 我们得到了比以前更好的准确度和一组可以使用的参数。请注意,最终结果中的标准是 1。这意味着选择了 1,即熵。 上述调整超参数的方法是最常见的,几乎适用于所有模型:线性回归、逻辑回归、基于树的方法、梯度提升模型(如 xgboost、lightgbm),甚至神经网络! 483 | 484 | 虽然这些方法已经存在,但学习时必须从手动调整超参数开始,即手工调整。手动调整可以帮助你学习基础知识,例如,在梯度提升中,当你增加深度时,你应该降低学习率。如果使用自动工具,就无法学习到这一点。请参考下表,了解应如何调整。RS* 表示随机搜索应该更好。 485 | 486 | 一旦你能更好地手动调整参数,你甚至可能不需要任何自动超参数调整。创建大型模型或引入大量特征时,也容易造成训练数据的过度拟合。为避免过度拟合,需要在训练数据特征中引入噪声或对代价函数进行惩罚。这种惩罚称为**正则化**,有助于泛化模型。在线性模型中,最常见的正则化类型是 L1 和 L2。L1 也称为 Lasso 回归,L2 称为 Ridge 回归。说到神经网络,我们会使用dropout、添加增强、噪声等方法对模型进行正则化。利用超参数优化,还可以找到正确的惩罚方法。 487 | 488 | | Model | Optimize | Range of values | 489 | | ------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | 490 | | Linear Regression | - fit_intercept
- normalize | - True/False
- True/False | 491 | | Ridge | - alpha
- fit_intercept
- normalize | - 0.01, 0.1, 1.0, 10, 100
- True/False
- True/False | 492 | | k-neighbors | - n_neighbors
- p | - 2, 4, 8, 16, ...
- 2, 3, ... | 493 | | SVM | - C
- gamma
- class_weight | - 0.001, 0.01, ...,10, 100, 1000
- 'auto', RS*
- 'balanced', None | 494 | | Logistic Regression | - Penalyt
- C | - L1 or L2
- 0.001, 0.01, ..., 10, ..., 100 | 495 | | Lasso | - Alpha
- Normalize | - 0.1, 1.0, 10
- True/False | 496 | | Random Forest | - n_estimators
- max_depth
- min_samples_split
- min_samples_leaf
- max features | - 120, 300, 500, 800, 1200
- 5, 8, 15, 25, 30, None
- 1, 2, 5, 10, 15, 100
- log2, sqrt, None | 497 | | XGBoost | - eta
- gamma
- max_depth
- min_child_weight
- subsample
- colsample_bytree
- lambda
- alpha | - 0.01, 0.015, 0.025, 0.05, 0.1
- 0.05, 0.1, 0.3, 0.5, 0.7, 0.9, 1.0
- 3, 5, 7, 9, 12, 15, 17, 25
- 1, 3, 5, 7
- 0.6, 0.7, 0.8, 0.9, 1.0
- 0.6, 0.7, 0.8, 0.9, 1.0
- 0.01, 0.1, 1.0, RS*
- 0, 0.1, 0.5, 1.0, RS* | 498 | 499 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: AAAMLP 中译版 2 | docs_dir: docs/ 3 | repo_name: 'ytzfhqs/AAAMLP-CN' 4 | repo_url: https://github.com/ytzfhqs/AAAMLP-CN 5 | theme: 6 | name: material 7 | language: zh 8 | static_templates: 9 | - 404.html 10 | include_search_page: false 11 | search_index_only: true 12 | features: 13 | - header.autohide 14 | - navigation.tracking 15 | - navigation.top 16 | - search.highlight 17 | - search.share 18 | - search.suggest 19 | - content.code.annotate 20 | palette: 21 | - media: '(prefers-color-scheme: dark)' 22 | scheme: slate 23 | primary: deep blue 24 | accent: cyan 25 | toggle: 26 | icon: material/weather-night 27 | name: Switch to light mode 28 | - media: '(prefers-color-scheme: light)' 29 | scheme: default 30 | primary: cyan 31 | accent: deep blue 32 | toggle: 33 | icon: material/weather-sunny 34 | name: Switch to dark mode 35 | 36 | markdown_extensions: 37 | - pymdownx.highlight: 38 | anchor_linenums: true 39 | - pymdownx.inlinehilite 40 | - pymdownx.snippets 41 | - admonition 42 | - pymdownx.arithmatex: 43 | generic: true 44 | - footnotes 45 | - pymdownx.details 46 | - pymdownx.superfences 47 | - pymdownx.mark 48 | - attr_list 49 | 50 | extra_javascript: 51 | - javascripts/katex.js 52 | - https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.7/katex.min.js 53 | - https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.7/contrib/auto-render.min.js 54 | 55 | extra_css: 56 | - https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.7/katex.min.css 57 | 58 | nav: 59 | - 前言: 'index.md' 60 | - 准备环境: '准备环境.md' 61 | - 有监督和无监督学习: '无监督和有监督学习.md' 62 | - 交叉检验: '交叉检验.md' 63 | - 评估指标: '评估指标.md' 64 | - 组织机器学习项目: '组织机器学习项目.md' 65 | - 处理分类变量: '处理分类变量.md' 66 | - 特征工程: '特征工程.md' 67 | - 特征选择: '特征选择.md' 68 | - 图像分类和分割方法: '图像分类和分割方法.md' 69 | - 文本分类或回归方法: '文本分类或回归方法.md' 70 | - 超参数优化: '超参数优化.md' 71 | - 组合和堆叠方法: '组合和堆叠方法.md' 72 | - 可重复代码和模型方法: '可重复代码和模型方法.md' 73 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | mkdocs-material==7.3.6 2 | mkdocs-minify-plugin == 0.4.0 3 | mkdocs-git-revision-date-localized-plugin == 0.9.3 4 | jinja2==3.0.0 5 | mkdocs-static-i18n==0.46 --------------------------------------------------------------------------------