
简介一套面向人工智能与机器学习初学者的恶意网站检测完整项目基于黑/白名单网站数据覆盖特征提取、可视化与模型训练预测全流程。资源共7个文件含5个Python脚本、1个数据压缩包和1个说明文档压缩包约3.31MB脚本分别实现特征提取、类型分布可视化及SVM、随机森林、DNN三种分类模型训练预测数据压缩包内已整合黑白名单特征表格目录清晰便于按模块学习。目前已有186人学习下载。整套项目源自个人毕业设计代码测试通过可直接运行适合计算机、人工智能及相关专业学生用于毕设、课设或项目初期演示读者可通过脚本理解特征工程与分类流程也可替换数据或调整算法进一步开展恶意URL识别、异常检测等扩展实验README文档提供快速上手指引整体适合系统学习与二次开发。1. 传统机器学习做恶意网站检测为什么不一上来就上深度学习一个安全告警群里最常见的问题是黑名单越堆越长恶意网站却还是拦不干净。传统机器学习做恶意网站检测恰恰是我这几年用到落地最快的一组方案。它不是新东西随机森林、逻辑回归、GBDT甚至朴素贝叶斯在中小流量场景下效果足够还比深度模型好解释。你能说清楚每个URL为什么被判恶意这对安全运营来说太重要了。下面按“特征、样本、训练、调参、上线”的顺序把能直接抄走的做法和坑讲明白。适合安全运维、风控研发以及对恶意URL检测感兴趣的新手。2. 恶意网站检测的特征怎么定URL、域名和页面内容各有各的脾气传统机器学习模型不能直接吃URL字符串得先把它变成数值向量。很多人一开始就搜“URL embedding”想着用文本嵌入其实没必要。恶意网站检测的场景里特征是结构化、离散、带强先验的手工特征加树模型反而更稳。特征可以分三层URL结构特征、域名特征、页面内容特征。一层层抠比盲目塞进神经网络要快得多也更容易排查问题。2.1 URL特征长度、特殊字符与混淆手段怎么变成数值恶意URL最典型的样子是“很长、很乱、带很多不值钱的符号”。攻击者要把恶意载荷藏进参数或者把恶意域名伪装成正常路径URL长度会明显拉长。最常见的特征包括URL总长度、域名长度、路径长度、点号数量、连字符数量、数字占比以及是否有符号、%编码、敏感词。写一个最简的特征提取函数方便直接抄from urllib.parse import urlparse import re def url_features(url: str) - dict: parts urlparse(url) host parts.hostname or path parts.path or lower_url url.lower() feats {} feats[url_len] len(url) feats[host_len] len(host) feats[path_len] len(path) feats[num_dots] host.count(.) feats[num_hyphen] host.count(-) feats[digit_ratio] sum(c.isdigit() for c in url) / max(len(url), 1) feats[has_at_sign] 1 if in url else 0 feats[has_https] 1 if parts.scheme https else 0 feats[special_char_ratio] ( url.count() url.count(?) url.count() url.count(%) ) / max(len(url), 1) feats[has_login_keyword] 1 if re.search( rlogin|signin|verify|account|update|secure, lower_url ) else 0 return feats这段代码逻辑很直白先用urlparse把URL拆开然后统计长度和特殊符号。有几个坑需要说清楚。一是has_https不能当“安全”信号现在免费证书遍地都是钓鱼站也用HTTPS它只能算一个分类特征权重让模型自己学。二是special_char_ratio里为什么只算 ? %因为恶意URL经常用URL编码和参数传递来隐藏真实地址这四个符号的出现频率比正常URL高得多。三是has_login_keyword攻击者会在URL里伪造“login/account”来诱导点击但正常登录页也有这个关键词所以它需要和域名特征配合使用不能单独作为硬规则。继续往下可以做更多编码比如顶级域名是否为常见后缀、是否为短链接服务、路径中是否有可疑前缀“wp-”或“cgi-bin”。这些手工特征对树模型很友好不需要归一化。2.2 域名特征注册时长、Whois与DNS解析里的“短期主义”恶意网站的生命周期通常很短几天甚至几小时这是它们和正常域名最大的区别。域名特征就是要抓住这种“短期主义”。域名特征常用三类Whois信息、DNS解析结果、域名名字本身的模式。Whois信息包括注册年龄、过期天数、注册商是否常见、域名所有者的信息完整度。恶意域名经常在刚注册后就投入使用所以“注册天数小于30天”是一个极有效的弱信号。DNS解析结果包括解析出的IP是否属于数据中心或云厂商、是否用了动态DNS、是否频繁更换解析记录。域名本身模式则关注随机字符串、数字和字母混合、超长子域名。抓Whois可以用命令行whois也可以直接查RDAP数据。不建议在特征提取线程里同步做因为whois服务普遍慢还会限流。常见做法是异步批量采集完后存成特征表。给一个域名快速特征的例子import socket def domain_features(domain: str) - dict: feats {} try: ip socket.getaddrinfo(domain, None, 0, socket.SOCK_STREAM)[0][4][0] feats[resolvable] 1 except OSError: ip 0.0.0.0 feats[resolvable] 0 feats[ip_is_private] 1 if ip.startswith((10., 192.168., 172.16., 127.)) else 0 feats[has_random_hex] 1 if any( seg.isalnum() and len(seg) 20 for seg in domain.split(.) ) else 0 ddns_list [duckdns.org, no-ip.com, dynu.com, freedns.afraid.org] feats[is_dynamic_dns] 1 if any(ddns in domain for ddns in ddns_list) else 0 return feats这里socket.getaddrinfo只做解析测试不能完全代表真实攻击来源。ip_is_private对大部分外部恶意域名用处不大但在内网扫描攻击环境里能区分用RFC1918地址构造的假域名。has_random_hex是判断域名中是否存在很长的一段字母数字组合比如a3f8b2c91d0e4f5g6h7j8k9l0.example.com这类随机子域名是恶意URL里常见的规避手法。is_dynamic_dns针对很多免费动态域名被用来做钓鱼入口的现状。注册年龄这个特征没有放进来因为它依赖外部服务一般放到后台队列里批量更新每小时刷新一次“注册时间”快照。这样做的好处是训练和在线预测用同一份特征表不会出现训练时特征新鲜、预测时特征缺失的情况。2.3 页面内容特征HTML标签、JS混淆与表单提交地址URL和域名特征在流量入口就能拿到但要判断“这个页面是否在钓鱼”必须抓取页面内容。页面内容特征主要看三块HTML结构、JavaScript脚本、表单行为。钓鱼页面的HTML结构有明显套路页面很简洁没有导航和版权信息标题和页面正文不匹配表单数量多但几乎没有真实链接。很多钓鱼页面会内嵌一个iframe把恶意表单放到看起来正规的域里面。JavaScript方面攻击者喜欢用atob()、eval()、document.write、fromCharCode来混淆地址。另一个强特征是表单提交的action指向的域名和当前页面域名不一致这是最直接的“跨站提交”。用requests和BeautifulSoup抓页面后可以这样提特征from bs4 import BeautifulSoup import re def content_features(html: str, page_url: str) - dict: soup BeautifulSoup(html, html.parser) feats {} title soup.title.get_text() if soup.title else feats[title_len] len(title) feats[form_count] len(soup.find_all(form)) feats[input_count] len(soup.find_all(input)) text_len len(soup.get_text()) feats[text_len] text_len feats[iframe_count] len(soup.find_all(iframe)) scripts .join(s.get_text() for s in soup.find_all(script)) feats[js_obfuscation_hits] len(re.findall( ratob|eval\(|fromCharCode|decodeURIComponent|document\.write, scripts )) feats[cross_submit] 0 for form in soup.find_all(form): action form.get(action) or if action and http in action and page_url.split(/)[2] not in action: feats[cross_submit] 1 break return feats页面特征要控制成本。爬页面比取URL慢一个数量级所以一般是先过一遍URL和域名模型命中疑似样本才去抓内容。js_obfuscation_hits会出现误报很多正常单页应用的代码也会用这些函数所以不能当成“一票否决”只能作为分数的组成部分。cross_submit是可靠性比较高的特征因为它对应了钓鱼攻击“借尸还魂”的本质表单长得再像提交地址骗不了人。页面正文的文本也可以做词频特征但树模型对稀疏词频不友好。实际操作中常见做法是只提取Title、Meta、表单、链接这几个关键位置不进入全文NLP。2.4 特征汇总表和特征筛选把上面三类特征放在一起大概有30到50维。对传统机器学习来说这个维度是安全的。下面是一个常用特征汇总类别特征示例恶意倾向URLurl_len, num_dots, num_hyphen越长越乱越可疑URLhas_https, has_at_sign不做强信号模型自取域名resolvable, is_dynamic_dns动态解析加短期存活域名registration_days30注册年龄短页面form_count, input_count登录框越密集越可疑页面js_obfuscation_hits混淆代码多不一定恶意页面cross_submit强特征跨域提交特征筛选的常见做法是先用卡方检验或信息增益粗筛再放进随机森林里看特征重要性。不过我不建议一开始就做严格的特征选择先把特征全丢进去训练完看特征重要性排序把这个排序当成排查信号。比如cross_submit重要性排在倒数那十有八九是页面抓取失败大多数页面没拿到内容特征大量为0而不是这个特征没用。还有一个容易忽视的点特征值必须在训练和预测时保持一致。比如has_login_keyword依赖URL中的敏感词预测时如果有URL规范化操作训练时也必须做同样操作。否则模型学的是一套分布线上跑的又是另一套翻车只是时间问题。3. 恶意网站检测的数据从哪来样本标注、切分与模型选型有了特征下一步是准备训练数据。恶意网站检测不是典型的文本分类或图像分类它的数据获取、标注和切分都有特殊讲究。常见做法是从被动流量中抓取一批真实URL从公开威胁情报源拿一批已知恶意URL再补一批主动爬虫采集的正常页面作为负样本。这三类数据合在一起才能覆盖模型上线后见到的各种情况。3.1 数据来源一被动流量、爬虫采集与公开情报被动流量是首选因为它最贴近业务真实分布。在企业出口或CDN日志里有海量URL请求把这些URL按源去重后打上标签训练出来的模型才不会被特定的采集方式带偏。但被动流量有两个问题一是绝大多数URL是正常站点恶意样本比例太低二是隐私边界如果是在安全分析场景下用URI做威胁检测需要先确认合规要求。这里不展开讲合规只提醒数据落盘和脱敏要认真做。爬虫采集适合补充正常和恶意样本。从公开分类目录里收集出来的站点基本等于正常PhishTank、URLhaus这类恶意URL情报源能拿到标注好的钓鱼和恶意URL。注意这些公开源是实时更新的拿来做离线训练时要记录下载时间避免未来样本混入训练集。我一般这样组织数据先按天存一份原始URL日志用URL去重保留“首次出现时间”然后给每个URL再抓一次域名解析和页面内容。这样后期可以灵活构造时间切片训练集。采集公开网页时要设置合理频率否则很快被挡但这部分不是技术重点。3.2 样本标注为什么不能只用单一黑名单当标签恶意网站检测最容易犯的错误是直接拿“当前是否在某个黑名单”当标签。黑名单是滞后的一个URL今天不在黑名单可能三天前就是钓鱼站点只是没人上报。如果在训练集里把它标成良性会让模型学到错误规律。正确做法是用时间窗口标注一个URL如果后来被情报源标记为恶意那么从标记时间往前推一小段时间的样本也应当被排除或重新标注。这个“前推窗口”通常是1到3天具体取决于情报源的延迟。这里要说明一点标签本身就是事后才能知道的信息预测时没有标签所以这不属于特征穿越。另一个问题是动态页面。URL路径不变页面内容却是动态的今天它是普通页面明天被攻击者入侵挂马。如果只按URL粒度打标签同一个URL会在一天内同时出现在正负样本里。我一般建议按“URL 日期”作为样本粒度而不是单纯URL。训练时如果同一个URL出现在两个类别且时间间隔短就把这份数据丢掉避免模型记住“某条URL是恶意”这种死样本而是学习可泛化的攻击手法。标注完成后还要做质量抽检随机抽300条人工看一遍统计标签与页面内容的符合率。如果不符合率超过5%就要警惕情报源质量考虑换源或调大前推窗口。3.3 模型选型逻辑回归、随机森林还是GBDT特征准备好后模型选择相对简单。传统机器学习在恶意网站检测里常用逻辑回归、随机森林、梯度提升树GBDT。它们各有各的脾气模型优点缺点适合场景逻辑回归训练快可解释性最强调参少表达能力弱需要特征具备线性作用方向做基线或需要向监管解释模型逻辑随机森林对特征尺度不敏感处理缺失值和类别特征方便预测稍慢参数多但默认值也够用中小规模数据、高频增量训练GBDT精度通常最高能学习复杂非线性容易过拟合参数敏感调试成本高样本量充足、追求最高准确率我的建议是先用随机森林作为主力因为它对传统机器学习特征最友好不用归一化且n_estimators500左右的表现已经很稳定。逻辑回归可以拿来当基线如果随机森林比逻辑回归高不了多少那说明特征设计的问题比模型更大应该回去补特征。GBDT在恶意网站检测里效果很好但调参玄学成分高同一批参数换一天的数据就变差适合有专职算法同学的团队否则容易在工程上失控。树模型还有一个天然优势可以处理特征的缺失值。比如页面内容没有抓到text_len和form_count为空随机森林可以在分裂时按缺失值方向处理逻辑回归则要提前填0。恶意网站检测场景里页面抓取失败率很高这一条会大幅减少数据清洗工作。3.4 评估指标精确率、召回率、误拦率怎么权衡模型训练完不能只看准确率。恶意网站检测的代价不对称漏放一个恶意网站可能造成资产损失误拦一个正常网站会引来用户投诉。核心指标是精确率、召回率和误拦率。准确率在正负样本比例悬殊时没有参考价值。假设恶意样本只占1%全部判正常也会有99%的准确率但这个模型没有任何用处。评估时要看分类阈值。sklearn默认用0.5作为判断边界但在样本不平衡时这个阈值往往不好。假设已经有一个训练好的model用下面的代码评估不同阈值下的表现from sklearn.metrics import classification_report, precision_recall_curve import numpy as np y_prob model.predict_proba(X_val)[:, 1] y_pred (y_prob 0.7).astype(int) print(classification_report(y_val, y_pred, target_names[benign, malicious]))这里把阈值提到0.7是为了在高置信度下再拦截。实际阈值应该用PR曲线来选先算precision_recall_curve找到“召回率不低于80%”的前提下精确率最高的点。如果这个点的精确率只有85%意味着每拦截1万个站里有1500个误拦业务上可能忍受不了。安全场景通常更偏向“宁可漏不可误”那就要把阈值设高一点召回率下降可以接受误拦率必须压低。这就是生产环境往往不用model.predict()而用predict_proba的原因。AUC也只适合离线对比模型不适合定阈值因为它对低概率区域的排序差异不敏感。运营阶段要看的是某个具体阈值下的误拦和召回所以PR曲线是首选。4. 跑通一个随机森林恶意URL分类器最小代码与必调参数下面把前两章的特征落地直接用随机森林做一个“能用”的最小分类器。数据格式是CSV每行一条URL字段有url、label、htmllabel为1表示恶意0表示正常。实际项目里html字段可以是空代码要处理好。4.1 完整特征工程函数把URL加域名加内容拼在一起把前面三个特征函数合并成一个方便统一调用。为了避免每行都重复解析可以先封装一下import pandas as pd from urllib.parse import urlparse from bs4 import BeautifulSoup import re def extract_features(row): url row.get(url, ) html row.get(html, ) or feats {} # URL p urlparse(url) host p.hostname or feats[url_len] len(url) feats[host_len] len(host) feats[path_len] len(p.path or ) feats[num_dots] host.count(.) feats[num_hyphen] host.count(-) feats[has_https] 1 if p.scheme https else 0 # 简单域名特征 feats[resolvable] 1 if host else 0 feats[has_random_hex] 1 if max((len(x) for x in host.split(.)), default0) 20 else 0 # 页面内容 feats[title_len] 0 feats[form_count] 0 feats[input_count] 0 feats[js_obfuscation_hits] 0 if html: soup BeautifulSoup(html, html.parser) feats[title_len] len(soup.title.get_text()) if soup.title else 0 feats[form_count] len(soup.find_all(form)) feats[input_count] len(soup.find_all(input)) scripts .join(s.get_text() for s in soup.find_all(script)) feats[js_obfuscation_hits] len(re.findall( ratob|eval\(|fromCharCode|document\.write, scripts )) return feats这个函数在真实工程里要加缓存。同一URL一天内可能被预测多次页面特征抓一次就够了缓存能省大量爬虫资源。这里has_random_hex用了简化写法实际项目中应该对host的每一段分别判断是否同时包含大小写字母和数字这里不展开。有个重要细节当html为空时所有页面特征为0。模型会把“没有页面内容”本身当成一个信号这没问题但要保证训练集里恶意和正常URL的页面抓取失败率大致相近。如果正常站点很容易抓恶意站点经常抓不到那么模型学到的其实是“抓不到等于恶意”的假规律而不是页面语义。建议在特征里加一列has_html记录是否成功抓取内容减少偏差。4.2 随机森林训练脚本class_weight、n_estimators和max_depth数据处理完后训练随机森林。恶意网站检测正负样本比例可能到1比50甚至1比1000必须带上class_weightbalanced。下面是训练代码from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import train_test_split import pandas as pd df pd.read_csv(url_samples.csv) feature_cols [c for c in df.columns if c not in (label, url, html)] X df[feature_cols] y df[label] X_train, X_val, y_train, y_val train_test_split( X, y, test_size0.2, stratifyy, random_state42 ) model RandomForestClassifier( n_estimators300, max_depth20, max_featuressqrt, class_weightbalanced, random_state42, n_jobs-1, ) model.fit(X_train, y_train)先说明class_weightbalanced它自动把每个类别的权重设为与样本量成反比缓解类别不平衡。用了它之后模型输出的概率不再反映样本真实分布整体会偏高因此后续阈值要重新用PR曲线调整不能沿用0.5。max_depth20是个保守值。对几十维特征来说深度太深容易记住页面细节对URL特征反而不好。如果你的特征数有100多维可以提到30但要在验证集上观察。max_featuressqrt是随机森林的关键每次分裂只看特征个数的平方根个特征增加树之间的独立性。n_estimators300收益已经边际递减不需要5000除非数据量到了百万级。random_state42是为了可复现生产上可以设成环境变量便于参数实验时对比。4.3 预测阈值用predict_proba而不是predict训练完成别直接用model.predict(X_val)因为predict的默认阈值0.5在类别不平衡时会让误拦爆炸。要拿到概率然后自定义阈值import numpy as np y_prob model.predict_proba(X_val)[:, 1] for threshold in [0.5, 0.7, 0.8, 0.9]: pred (y_prob threshold).astype(int) tp np.sum((pred 1) (y_val 1)) fp np.sum((pred 1) (y_val 0)) fn np.sum((pred 0) (y_val 1)) precision tp / max(tp fp, 1) recall tp / max(tp fn, 1) print(fthr{threshold}, precision{precision:.3f}, recall{recall:.3f})这段代码只是演示实际直接用precision_recall_curve更简洁。这里把每个阈值下的精确率和召回率打出来就能看到典型现象阈值从0.5提到0.8精确率明显上升召回率下降不多。这是因为正常URL和恶意URL在概率分布上有一块重叠区提高阈值能有效甩掉低置信度误拦。上线预测时同样用model.predict_proba。最终给下游系统的可以是一个0到1之间的恶意分数而不是只给0或1。下游可以自己再配置白名单、黑名单和分级处置这样灵活性大得多。5. 传统机器学习检测恶意网站的5个常踩坑从特征穿越到时效性模型和数据都跑通后上线一段时间会暴露各种问题。下面5个是恶意网站检测方向上反复出现的坑每条都是血泪经验按“现象、原因、解决”的顺序写清楚。5.1 特征穿越用“未来”的信息导致评估虚高现象离线验证时AUC有0.99上线后精确率和召回率崩盘连续拦下一批正常站。我把验证集里被模型“正确”判出的恶意样本抽出来看发现它们都用了“域名A记录”这个特征而这个特征在样本采集时根本不是那样。原因构建特征表时数据库存的是“最后一次查询结果”不是“该URL首次出现时的结果”。模型学到的是“现在解析到这个IP等于恶意”但历史上它解析到的是正常IP。这等于让模型偷看了未来。解决对时间敏感型的特征必须把特征查询时间对齐到样本发现时间。DNS和Whois要保存每日快照训练和预测都只能使用“过去到当时”的数据。这是传统机器学习在恶意网站检测里最隐蔽的坑没有之一。5.2 样本不平衡恶意样本占比过低时怎么处理现象训练完跑一下看到accuracy等于99.6%觉得模型很厉害。后来一算恶意样本的召回率只有15%等于几百个恶意站只抓住十几个。原因正样本占比太低模型学到的是把几乎所有样本判为正常因为全判正常也能拿到很高的准确率。这是类别不平衡的经典问题。解决不要用accuracy做评估改用精确率和召回率训练时加class_weightbalanced或对多数类下采样。下采样会让训练集缩小树模型速度更快但可能浪费有价值样本。我一般先试class_weight不行再上SMOTE一类的过采样。注意过采样不能无脑用在验证集上否则会造成训练验证数据泄漏。5.3 标签噪声黑名单滞后让正样本混进负样本现象训练集由“当前黑名单”打标签抽检负样本时发现很多“白的”URL实际上是几个月前的钓鱼网站只是最近才被情报源收录。模型把这部分历史数据当成了正常样本自然学不到真实恶意模式。原因黑名单是防御者的滞后反馈直接用存量标签等于让模型记住“现在还在黑名单”这些信息可预测时我们不可能预先知道这条URL会不会将来被标记。解决使用时间窗口标注以样本首次出现时间和情报标记时间为基准。如果情报源在后来某天标记一条URL为恶意那么该URL在更早时间的访问记录不能当成正常流量保留应删除或重新标定。这一步会损失一些历史样本但换来的是标签可靠值得。5.4 概念漂移攻击者的混淆手法会变现象模型刚上线非常准3个月后突然对“新看到的异常URL”没感觉误拦率没变但漏报率缓慢上升。原因攻击者会跟着模型走。他们发现长随机子域名容易被抓就改成用大量合法域名做跳板或者把恶意页面托管在正常服务上。固定训练集里学到的特征逐渐失效这就是概念漂移。解决定期重训是唯一出路。我这里一般每周从流量日志抽一批最近7天的样本重新训练随机森林。传统机器学习模型训练成本低一周一训完全可以接受。如果资源紧张至少一月一训同时持续监控特征分布变化。当某个重点特征的重要性排序突然下降就是在提示攻击者换手法了。5.5 时效性域名长期存活变化与时间切分错误现象用随机切分得到训练集和验证集验证集里包含大量老域名样本训练集也包含这些老域名模型直接记下域名本身。上线后遇到刚注册的域名因为训练里没见过全被判正常。原因随机切分造成了“特征穿越”的变体。同一个域名在不同时间跨度里重复出现信息被同时分到两边模型背答案了。解决按时间切分比如用2024年1到4月训练4月验证5月测试。并且尽量按“URL加日期”去重让同一URL只出现在一个时间切片里。这样模型才真正学到“第一次遇到这个URL时怎么判断”而不是从验证集里看见答案。一个简单检查方法看验证集里恶意域名是否在训练集里大量出现过如果重叠度高说明切分有问题。6. 生产环境的三个进阶技巧阈值、规则融合和反馈重训模型本身只是中间产物要真正在网关或安全分析系统里落地还得把三个环节处理好。这套做法我在多个项目里反复用过也是传统机器学习方案相比深度模型更灵活的地方所在。6.1 PR曲线选阈值先保证误拦率可接受上线前先拿最近一周的真实流量回放一遍用PR曲线找到“误拦率小于0.1%”时的最小阈值。千万别只用一个准确率或AUC就收工。传统机器学习最大的好处是能明确给出每个URL的恶意概率你可以把这个概率做成监控面板看概率分布是否随时间偏移。如果分布整体往0.5方向靠说明特征在失效需要马上准备重训。6.2 和黑名单规则融合高置信度自动拦截低置信度二次检查不要用模型替代已有黑名单而是叠加。我的做法是命中已知黑名单直接拦截模型置信度高于0.95且未被白名单覆盖的进入自动拦截置信度在0.7到0.95之间的只记录并触达二次验证低于0.7的放行。这样规则兜底、模型补漏正常用户投诉率会降很多。模型分数能解释出现问题也方便回滚。6.3 把漏报样本送回模型里反馈闭环每次人工处理完告警把确实是漏报的URL和对应的页面特征入库。每周更新训练数据时把这些新标记样本合进去。重训后对比上一版模型在最近一周流量上的精确率和召回率有提升再上线。这个闭环比任何高级模型都管用。我踩过最痛的一次是花三周调GBDT参数最后发现准确率提升主要来自新增了一批漏报数据而不是调参本身。从那以后每次重训我都先看数据增量再看模型分数。数据对了传统机器学习才能一直醒着。希望帮到你。本文还有配套的精品资源点击获取