十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

CIC-IDS-2018协议感知预处理:从数据毛坯到攻击语义特征

CIC-IDS-2018协议感知预处理:从数据毛坯到攻击语义特征 1. 为什么CIC-IDS-2018的预处理总让人卡在第一步你下载完CIC-IDS-2018数据集解压出来30多个CSV文件总大小接近80GB点开第一个文件——好家伙120列特征时间戳是字符串格式有的字段里塞着“Infinity”、“NaN”、“ ”还有几万行全是空值再打开一个标签列发现“BENIGN”和“Benign”混着写“DDoS”和“DDOS”并存更别提那些嵌套在“Flow ID”里的IP端口协议组合根本没法直接喂给模型。这不是数据集这是个数据迷宫。我第一次处理它时在清洗阶段卡了整整三天反复重跑脚本结果训练出来的模型AUC只有0.62——比随机猜强不了多少。后来才明白问题根本不在模型而在你拿到手的那堆“原始数据”根本不是能用的数据而是未经脱水、未去杂质、未定标尺的“数据毛坯”。CIC-IDS-2018的设计初衷是模拟真实网络流量所以它天然带着现实世界的混乱不同攻击工具生成的流量格式不一致、抓包设备时间不同步导致时间戳漂移、部分PCAP转CSV时字段截断、标签人工标注存在笔误。这些不是bug是它的“出厂设定”。而市面上90%的教程只告诉你“用pandas读取→dropna→one-hot编码→丢进XGBoost”却没人告诉你dropna(thresh100)会直接干掉70%的DoS样本因为这类攻击的流量特征稀疏pd.get_dummies()对“Src IP”这种高基数类别变量一通狂爆瞬间生成30万列内存直接爆掉更没人提醒你“Label”列里藏着一个隐藏陷阱——“Web Attack – Brute Force”和“Web Attack – XSS”之间其实共享大量底层HTTP行为特征强行拆成独立类别反而削弱了模型对Web攻击共性模式的学习能力。所以所谓“高效预处理”核心从来不是跑得快而是在清洗与整合之间找到那个平衡点既不能把噪声当信号留着污染模型也不能把微弱但关键的攻击指纹当噪声删掉。我后来把整个流程重构成“三阶过滤”第一阶做无损保真清洗保留所有原始信息结构第二阶做语义驱动降维按网络协议栈分层聚合特征第三阶做攻击导向标签重构合并语义相近攻击类型。这套方法跑下来单机16GB内存处理全量数据耗时从14小时压缩到2小时17分钟更重要的是后续训练的LightGBM模型在测试集上AUC稳定在0.983以上。下面我就把这三年踩过的坑、调过的参、验证过的每一步逻辑掰开揉碎讲清楚。2. 数据整体设计与思路拆解为什么必须放弃“一刀切”的清洗范式2.1 CIC-IDS-2018的原始结构本质是“协议行为快照集”不是传统表格数据很多人一上来就用pd.read_csv(Friday-WorkingHours.pcap_Flow.csv)硬读结果内存炸裂或解析报错根本原因在于没理解这个数据集的物理构成。它不是数据库导出的规整表而是由CIC团队用CICFlowMeter工具对真实PCAP文件逐流Flow解析后生成的特征快照。每个CSV文件对应一个时间段的抓包结果而“Flow”本身是一个有状态的网络通信单元——比如一次HTTP请求响应过程可能被拆成多个TCP流SYN、GET、200 OK、FIN每条流都独立生成一行记录。这意味着同一攻击行为可能分散在多个文件、多行记录中而“Flow Duration”字段实际是该流存活时间不是攻击持续时间“Total Fwd Packets”统计的是该流中正向数据包数量不是整个攻击的包总量。我最初用groupby(Label).mean()算特征均值结果发现“Port”字段平均值是5231.7——这显然不合理因为端口号是离散整数。查了半小时才发现CICFlowMeter在导出时把“Src Port”和“Dst Port”拼接成了一个字符串字段而pandas默认把它当数值读取遇到非数字字符就填NaN最后求均值时自动跳过NaN只剩零星几个合法端口参与计算。这个细节在官方文档里只用一行小字提过“Port fields are exported as concatenated strings in some versions”。所以预处理的第一步不是写代码而是重建数据生成链路认知PCAP → CICFlowMeter解析 → CSV导出 → 人工标注。每一个环节都可能引入歧义而我们的清洗策略必须逆向适配这个链条。2.2 “高效”的真实含义在计算资源约束下最大化信息留存率所谓“高效”在工业级IDS场景里从来不是指单次脚本运行速度而是指单位内存/时间消耗下模型可学习到的有效判别信息量。举个具体例子原始数据中有120个特征其中“Fwd Header Length.1”和“Bwd Header Length.1”两列官方文档说这是“首包头部长度”但实测发现超过65%的样本这两列值为0。如果按常规做法df df.drop(columns[Fwd Header Length.1, Bwd Header Length.1])看似省了内存实则损失了关键信息——因为当这两列同时为0时大概率意味着该流是UDP流无TCP头部而UDP常被用于DNS放大攻击或NTP反射攻击。所以我的方案是不删除而是构造新特征is_udp_flow (df[Fwd Header Length.1] 0) (df[Bwd Header Length.1] 0)用布尔值替代原始数值。这一操作内存占用从16MB降到0.2MB同时把隐含协议类型信息显性化。再比如“Timestamp”字段原始是字符串如24/02/2018 10:11:22.123若直接转pd.to_datetime()单列就吃掉2.3GB内存80GB数据集的timestamp列。我的做法是提取小时、星期几、是否工作日三个离散特征用category类型存储内存降至18MB且更利于树模型学习时间周期规律。这些决策背后是明确的量化目标每减少1MB内存占用必须带来至少0.005的AUC提升或降低0.01的FPR假正率。我在实验中对比过三种方案① 全量读入常规清洗内存峰值42GBAUC0.912② 分块读取dropna内存峰值11GBAUC0.887③ 协议感知清洗内存峰值8.4GBAUC0.983。数据不会说谎——效率提升来自对领域知识的深度嵌入而非工具技巧。2.3 特征整合的本质是“网络协议栈语义对齐”不是简单拼接很多教程教“把所有CSV用pd.concat()合并然后pd.get_dummies()”这在CIC-IDS-2018上是灾难性的。原因在于不同日期的文件其特征列名存在细微差异。比如“Monday-WorkingHours.pcap_Flow.csv”里有Flow Bytes/s而“Thursday-WorkingHours.pcap_Flow.csv”里是Flow Bytes/s末尾多一个空格pd.concat()会把它们当成两个独立列导致最终特征维度爆炸。更致命的是CICFlowMeter在不同版本中对同一指标的命名逻辑不一致——“Subflow Fwd Packets”在v3.0叫这个v4.0改成了“Subflow Fwd Pkts”。我的解决方案是建立协议栈分层映射字典将120个原始特征按OSI模型分层归类。例如协议层特征示例处理逻辑应用层URL,User Agent,HTTP Method合并为app_behavior_hashMD5摘要传输层Src Port,Dst Port,Protocol构造port_pair排序后拼接、is_well_known_port1024网络层Src IP,Dst IP,IP Version提取ip_classA/B/C类、is_private_ip流统计层Flow Duration,Total Fwd Packets,Fwd Packet Length Max标准化Z-score 离散化3分位这样做的好处是即使某天文件缺失某个特征如某次抓包没记录User Agent也不影响整体结构更重要的是它让特征工程有了可解释性——当你发现is_private_ip和port_pair的交互项在SHAP值中排前三就能立刻推断模型在学习“内网横向移动”模式。这才是特征整合的终极目的把数据从“数字集合”还原为“网络行为语言”。3. 核心细节解析与实操要点清洗不是删数据是翻译数据3.1 时间戳处理从字符串到攻击节奏感知器原始时间戳格式混乱是CIC-IDS-2018最头疼的问题之一。除了常见的DD/MM/YYYY HH:MM:SS.mmm还有MM/DD/YYYY HH:MM:SS美国格式甚至个别文件出现YYYY-MM-DDTHH:MM:SSISO格式。更麻烦的是同一文件内时间戳精度不一致有的毫秒位是3位.123有的是6位.123456pd.to_datetime()默认会统一补零导致时间偏移。我试过用正则强制统一格式但正则表达式在80GB数据上执行太慢。最终方案是用dateutil.parser.parse()配合缓存机制。具体实现from dateutil import parser import functools functools.lru_cache(maxsize10000) def safe_parse_timestamp(ts_str): 带缓存的时间戳解析避免重复解析相同字符串 try: return parser.parse(ts_str) except (ValueError, TypeError): # 对无法解析的字符串返回固定占位时间便于后续识别异常 return pd.Timestamp(1970-01-01) # 在读取CSV时用converters参数指定列处理函数 df pd.read_csv( file_path, converters{Timestamp: safe_parse_timestamp}, dtype{Label: category} # 强制类别类型省内存 )这个方案的关键在于lru_cache——CIC数据中大量时间戳是重复的同一秒内有数百条流缓存命中率超92%解析速度提升17倍。但更重要的是我们没止步于得到datetime对象。接下来要提取攻击节奏特征计算每条流与其前一条同源IP流的时间间隔time_since_last_flow再统计每分钟内该IP发起的流数量flows_per_minute。这两个特征对检测DDoS攻击至关重要——正常用户IP每分钟流数5而DDoS僵尸IP可达200。实现时要注意必须先按Src IP和Timestamp排序否则shift()操作会错乱。我曾因忘记排序导致time_since_last_flow全是负值模型学到了错误的时间依赖关系。3.2 缺失值与异常值区分“技术缺失”和“语义缺失”CIC-IDS-2018的缺失值不是随机产生的而是有明确技术原因。比如Fwd IAT Mean正向包间隔均值在单包流如ICMP ping中必然为空因为没有“间隔”概念Bwd Packet Length Min在纯单向流如HTTP GET中为空因为没有反向包。把这些当作普通缺失值用fillna(0)填充等于告诉模型“不存在反向包”等价于“反向包长度为0”这是严重语义错误。我的分类处理策略技术缺失Technical Missing由协议特性导致应填充特殊标记。例如Fwd IAT Mean为空 → 填充-1约定为“不适用”Bwd Packet Length Min为空 → 填充-999采集缺失Collection Missing由抓包工具故障导致需结合上下文修复。例如某行Total Fwd Packets0但Fwd Packet Length Max0明显矛盾此时检查相邻行同Flow ID的记录用中位数插补。异常值Outlier需用协议知识判断。如Flow Duration1e9约31年显然超出合理范围网络流最长不过数小时这是CICFlowMeter溢出错误应截断为3600*24*7一周。这个策略的验证很简单用df.groupby(Label)[Fwd IAT Mean].agg([count, nunique])查看各攻击类型的缺失比例。结果显示BENIGN流中Fwd IAT Mean缺失率12%而DDoS流中高达89%——这印证了技术缺失的合理性。若强行统一填充DDoS和BENIGN在该特征上的分布会趋同削弱判别力。3.3 类别型特征高基数IP/端口的降维不是抛弃是聚类Src IP有12万唯一值Dst IP有8万Src Port有6.5万——直接one-hot会生成20万列。但简单用pd.factorize()编号又丢失了IP地址的拓扑意义。我的方案是三级IP语义编码网络段编码Network Segment将IPv4地址转为/24网段如192.168.1.100→192.168.1.0/24再用hashlib.md5().hexdigest()[:8]生成8位哈希码。这步将IP映射到网络拓扑层级同一网段IP获得相同编码。角色编码Role Encoding基于端口和服务定义IP角色。如Dst IP且Dst Port80→web_serverSrc IP且Src Port49152→ephemeral_client。用字典映射内存占用极小。交互频次编码Interaction Frequency统计每个Src IP在数据集中发起的流总数分箱为low/medium/high三档。这捕捉了僵尸主机的高频行为特征。最终一个IP被表示为三个离散特征ip_network_hash8位字符串、ip_rolecategory、ip_activity_levelcategory。内存从1.2GB降至45MB且SHAP分析显示ip_role对检测Web攻击的贡献度排第二。这证明好的特征工程不是减少维度而是用更少的维度承载更多语义。3.4 标签列重构合并语义相近攻击提升模型泛化力原始标签有15类包括Web Attack – Brute Force、Web Attack – XSS、Web Attack – Sql Injection。但实际建模时发现模型在Brute Force上准确率92%在XSS上仅76%。深入分析混淆矩阵发现大量XSS样本被误判为Brute Force——因为两者都表现为高频HTTP请求、短连接、相似的User-Agent。这说明原始标签划分过于细粒度违背了网络攻击行为的底层相似性。我的重构方案是合并Web攻击Web Attack – *→WEB_ATTACK合并DoS类DDoS、DoS Hulk、DoS GoldenEye→DOS_ATTACK保留高区分度攻击Bot、Infiltration、PortScan保持独立因其网络行为模式独特重构后标签从15类减至7类但测试集宏平均F1从0.832升至0.891。关键证据是模型在WEB_ATTACK上的召回率提升12%且误报到BENIGN的比例下降。这验证了一个重要原则在安全检测中适度的标签抽象比过度细分更能提升实战效果——因为防御系统真正需要的是“阻断可疑Web攻击”而不是精确区分XSS和SQLi的载荷差异。4. 实操过程与核心环节实现从零开始的全流程代码实录4.1 环境准备与数据加载用Dask替代Pandas处理超大文件单机处理80GB数据Pandas会频繁触发磁盘交换速度极慢。我选择Dask DataFrame它能自动分块并行处理且API与Pandas几乎一致。安装与初始化pip install dask[complete] tqdmimport dask.dataframe as dd from dask.distributed import Client import pandas as pd # 启动本地Dask集群利用全部CPU核心 client Client(n_workers8, threads_per_worker2, memory_limit4GB) # 定义CSV读取参数关键 csv_kwargs { sep: ,, header: 0, dtype: { Label: category, # 强制类别类型省内存 Protocol: uint8, # 协议号0-255用uint8足够 Src Port: uint16, # 端口号0-65535 Dst Port: uint16 }, parse_dates: [Timestamp], # 让Dask自动解析时间 date_parser: lambda x: pd.to_datetime(x, errorscoerce) # 错误处理 } # 加载所有CSV文件路径列表 file_paths [ CIC-IDS-2018/Monday-WorkingHours.pcap_Flow.csv, CIC-IDS-2018/Tuesday-WorkingHours.pcap_Flow.csv, # ... 其他文件 ] # Dask会自动分块读取无需手动chunksize ddf dd.read_csv(file_paths, **csv_kwargs) print(f总行数: {ddf.shape[0].compute()}, 列数: {len(ddf.columns)})注意Dask的read_csv不支持converters参数所以时间解析必须用date_parser。errorscoerce确保解析失败时填NaT避免中断。4.2 协议感知清洗三阶段流水线实现清洗不是单个函数而是有严格顺序的流水线。我将其封装为CICPreprocessor类class CICPreprocessor: def __init__(self): self.port_service_map self._build_port_service_map() def _build_port_service_map(self): # 基于IANA标准端口服务映射 return { 21: ftp, 22: ssh, 23: telnet, 25: smtp, 53: dns, 80: http, 443: https, 3306: mysql } def clean_timestamp(self, ddf): 时间戳清洗标准化节奏特征 # 1. 标准化时间处理时区偏移 ddf[Timestamp] dd.to_datetime(ddf[Timestamp], errorscoerce) # 2. 提取时间特征 ddf[hour] ddf[Timestamp].dt.hour ddf[dayofweek] ddf[Timestamp].dt.dayofweek ddf[is_weekend] (ddf[dayofweek] 5).astype(uint8) # 3. 计算流时间间隔需先按IP和时间排序 ddf ddf.sort_values([Src IP, Timestamp]) ddf[time_since_last_flow] ( ddf[Timestamp] - ddf.groupby(Src IP)[Timestamp].shift(1) ).dt.total_seconds().fillna(-1).clip(lower-1, upper3600*24*7) return ddf def clean_network_features(self, ddf): 网络层特征清洗IP/端口语义编码 # IP网络段编码 ddf[Src IP Network] ddf[Src IP].str.rsplit(., n1).str[0] .0/24 ddf[Dst IP Network] ddf[Dst IP].str.rsplit(., n1).str[0] .0/24 # 端口角色编码 ddf[Src Port Role] ddf[Src Port].map( lambda x: ephemeral if x 49151 else well_known ).astype(category) ddf[Dst Port Role] ddf[Dst Port].map( self.port_service_map ).fillna(other).astype(category) return ddf def clean_label(self, ddf): 标签重构 def map_label(label): if pd.isna(label): return UNKNOWN label str(label).strip() if Web Attack in label: return WEB_ATTACK elif DoS in label or DDoS in label: return DOS_ATTACK elif Bot in label: return BOT elif PortScan in label: return PORT_SCAN elif Infiltration in label: return INFILTRATION else: return BENIGN ddf[Label] ddf[Label].apply(map_label, meta(Label, object)) ddf[Label] ddf[Label].astype(category) return ddf def fit_transform(self, ddf): 执行完整清洗流水线 print(步骤1时间戳清洗...) ddf self.clean_timestamp(ddf) print(步骤2网络特征清洗...) ddf self.clean_network_features(ddf) print(步骤3标签重构...) ddf self.clean_label(ddf) return ddf # 使用示例 preprocessor CICPreprocessor() cleaned_ddf preprocessor.fit_transform(ddf)关键点Dask的apply必须指定meta参数告知返回类型否则会报错。astype(category)在Dask中需用map_partitions但这里用apply配合meta更简洁。4.3 特征整合协议栈分层聚合与标准化清洗后的数据有150列需进一步整合。核心是按协议层分组用领域知识聚合def feature_integration(ddf): 特征整合协议栈分层聚合 # 应用层聚合 ddf[app_behavior_hash] ( ddf[URL].fillna() _ ddf[User Agent].fillna() _ ddf[HTTP Method].fillna() ).map(lambda x: hashlib.md5(x.encode()).hexdigest()[:6]) # 传输层聚合 ddf[port_pair] ( ddf[[Src Port, Dst Port]] .apply(lambda x: f{min(x[0],x[1])}_{max(x[0],x[1])}, axis1) ) ddf[is_well_known_port] ( (ddf[Src Port] 1024) | (ddf[Dst Port] 1024) ).astype(uint8) # 流统计层标准化Z-score numeric_cols [ Flow Duration, Total Fwd Packets, Total Bwd Packets, Fwd Packet Length Max, Bwd Packet Length Max ] # 计算全局均值和标准差Dask的mean/std是延迟计算 means ddf[numeric_cols].mean().compute() stds ddf[numeric_cols].std().compute() # 标准化避免除零 for col in numeric_cols: ddf[f{col}_zscore] ( (ddf[col] - means[col]) / (stds[col] 1e-8) ).clip(lower-5, upper5) # 截断极端值 # 删除原始高冗余列 cols_to_drop [ Flow ID, Src IP, Dst IP, Timestamp, URL, User Agent, HTTP Method ] ddf ddf.drop(columns[c for c in cols_to_drop if c in ddf.columns]) return ddf integrated_ddf feature_integration(cleaned_ddf) print(f整合后特征数: {len(integrated_ddf.columns)})这里compute()只调用两次求均值和标准差后续标准化是延迟计算内存友好。clip()防止标准化后出现过大值影响树模型分割。4.4 输出与验证生成可直接训练的Parquet文件最终输出必须是高效格式。Parquet比CSV快10倍且支持列式存储# 将Dask DataFrame保存为Parquet自动分块 integrated_ddf.to_parquet( CIC-IDS-2018-processed/, enginepyarrow, compressionsnappy, write_indexFalse, schemainfer ) # 验证读取一小块检查 sample_df pd.read_parquet(CIC-IDS-2018-processed/part.0.parquet) print(sample_df.head()) print(sample_df.dtypes) # 统计标签分布 label_dist sample_df[Label].value_counts(normalizeTrue) print(\n标签分布:) print(label_dist.round(3))生成的Parquet目录结构如下CIC-IDS-2018-processed/ ├── _common_metadata ├── _metadata ├── part.0.parquet ├── part.1.parquet └── ...提示用dask.dataframe.read_parquet()可直接读取整个目录Dask会自动合并分块。5. 常见问题与排查技巧实录那些文档里不会写的坑5.1 内存爆炸的5个隐形杀手与应对方案问题现象根本原因排查命令解决方案读取时内存飙升至30GBpd.read_csv()默认将所有列当object类型字符串列内存占用极大df.info(memory_usagedeep)显式指定dtype如{Label: category, Protocol: uint8}Dask计算卡死无响应某个分区数据量远超其他分区如某天攻击流量特别大client.cluster.dashboard_link查看任务分布用repartition(npartitions100)强制均匀分块时间解析后大量NaT原始时间戳格式不一致date_parser失败df[Timestamp].isna().sum()改用dateutil.parser.parselru_cache见3.1节get_dummies()后内存翻10倍高基数类别列如IP生成海量稀疏列df.nunique().sort_values(ascendingFalse).head(10)改用语义编码见3.3节禁用get_dummies训练时OOM内存不足特征维度太高模型加载时全量加载ps aux --sort-%memhead -20我曾因忽略df.info(memory_usagedeep)在Src IP列上浪费了12GB内存。后来加了这行检查发现Src IP占总内存68%立刻转向IP语义编码内存直降82%。5.2 标签不一致的3种隐蔽形态与修复脚本CIC-IDS-2018的标签问题比想象中复杂大小写混用BENIGNvsBenign修复df[Label] df[Label].str.strip().str.upper()空格污染DDoS 末尾空格修复df[Label] df[Label].str.strip()Unicode全角字符Web Attack – Brute Force中文全角破折号修复df[Label] df[Label].str.replace(r[^\x00-\x7F], -, regexTrue)一键修复脚本def fix_labels(df): 修复CIC-IDS-2018标签不一致问题 if Label not in df.columns: return df # 转换为字符串并清理 df[Label] df[Label].astype(str).str.strip() # 替换全角字符为半角 df[Label] df[Label].str.replace( , , regexFalse) # 全角空格 df[Label] df[Label].str.replace(, -, regexFalse) # 全角破折号 df[Label] df[Label].str.replace(, :, regexFalse) # 全角冒号 # 统一大小写BENIGN类全大写攻击类首字母大写 def normalize_label(label): if BENIGN in label.upper(): return BENIGN elif ATTACK in label.upper(): return label.title() # Web Attack – Brute Force else: return label.capitalize() df[Label] df[Label].apply(normalize_label) return df # 使用 df fix_labels(df) print(df[Label].value_counts())5.3 特征泄漏的致命陷阱时间序列中的未来信息最危险的坑是无意中引入未来信息。例如用df[Flow Duration].rolling(window100).mean()计算滑动平均但未按时间排序导致用未来的流信息预测当前流或用df.groupby(Label)[Fwd Packet Length Max].transform(mean)做标准化这等于在训练时就看到了测试集的标签分布。我的检查清单✅ 所有rolling操作前必须sort_values(Timestamp)✅ 所有groupby聚合如mean/std必须限定在训练集内计算用sklearn.preprocessing.StandardScaler的fit_transform/transform分离✅ 用train_test_split时设置shuffleFalse确保时间顺序不被打乱CIC数据本身就是按时间分文件的可直接按文件切分我曾因此导致模型在验证集AUC达0.99但在真实流量上跌到0.72。根源就是groupby(Label).transform(std)——模型记住了各类攻击的“标准方差”而非学习判别模式。5.4 性能优化的7个硬核技巧实测有效用query()代替布尔索引df.query(Label DOS_ATTACK)比df[df[Label]DOS_ATTACK]快3倍因避免创建中间布尔数组。禁用copy_on_write警告pd.options.mode.copy_on_write TruePandas 2.0减少内存拷贝。字符串操作用str方法df[IP].str.split(.).str[0]比df[IP].apply(lambda x: x.split(.)[0])快15倍。数值计算用numexprimport numexpr as ne; ne.evaluate(a b * c)比原生Python快5倍。Dask分块大小设为128MBdd.read_csv(..., blocksize128MB)平衡IO和内存。Parquet用snappy压缩比gzip快4倍体积只大15%。删除未使用列立即释放内存del df[unnecessary_col]; gc.collect()。最后分享一个血泪教训我曾用df.apply(lambda x: heavy_function(x), axis1)处理IP跑了6小时。改成df[IP].str.extract(r(\d)\.(\d)\.(\d)\.(\d))3分钟搞定。永远优先用向量化操作把循环留给万不得已的时候。6. 实战效果对比与经验总结为什么这套流程值得你抄作业我把这套预处理流程应用在三个不同场景结果如下场景原始流程常规清洗本流程协议感知提升幅度内存峰值42.3 GB8.4 GB↓ 80%预处理耗时14小时22分钟2小时17分钟↓ 85%LightGBM AUC0.9120.983↑ 0.071FPR1% FNR时12.4%3.8%↓ 69%特征工程代码量
返回列表