news 2026/10/22 3:20:13

Kaggle房价预测实战:数据清洗、LightGBM建模与模型融合全流程复现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Kaggle房价预测实战:数据清洗、LightGBM建模与模型融合全流程复现

简介:一份完整的Kaggle房价预测比赛源码包,面向希望入门数据挖掘与回归建模的初学者和参赛者,集中展示了从原始数据处理到预测结果评估的完整流程。包内共13个文件,含7个CSV数据文件、5个Python脚本和1个Markdown说明文档,整体仅274KB,结构轻盈,便于快速阅读。目前已有251人学习下载,适合作为房价预测任务或Kaggle竞赛的起步参考。代码对训练与测试数据进行缺失值清洗、类型转换和标准化,再结合面积、房间数量及距市中心距离等信息构建派生特征;随后在线性回归、随机森林、XGBoost/LightGBM集成、Stacking与神经网络等多个模型中展开训练与交叉验证,利用RMSE等指标评估效果,同时涉及学习率、树深度等超参数调优。整体按数据预处理、特征工程、模型选择与评估分层组织,有助于复现赛题全流程,也可直接在此框架上扩展新想法。

1. 拿到 kaggle 房价预测比赛代码.zip:先别急着跑,里面没有模型文件

把 kaggle 房价预测比赛代码.zip 解压之后,你大概率会看到一堆脚本、几个 CSV 和一份写得不算友好的 README,而不是一个打包好的 .pkl 模型文件。这个比赛的常见形态是一张不到 1500 行的训练表、80 来个特征,目标列是 SalePrice,绝大多数参赛者最终用的是 LightGBM、XGBoost 再加 Ridge 做融合。zip 里真正值钱的不是某个模型的黑匣子,而是从缺失值填充、目标值对数变换、五折交叉验证到提交文件生成的一整套可复现流程。这篇笔记就是顺着这套流程讲清楚:代码包里的每个剧本在做什么、怎么改成自己的、以及复现过程中最容易翻车的几个地方。适合想冲进 Top 档位但不想照抄网上的「调参玄学」的从业者。

2. 从数据清洗开始复现:缺失率、分布、目标变换决定后面所有事

2.1 拿到数据先做三件事:读列名、看缺失率、看目标分布

很多新手拿到这份代码包,习惯性先打开训练脚本找模型参数,结果跑出来的分数跟 README 里贴的完全对不上。我一般会先做三件事:确认数据形态、按列统计缺失率、绘制目标值分布图。这三件事做完,特征工程的基本盘就清楚了。

import pandas as pd import numpy as np train = pd.read_csv("train.csv") test = pd.read_csv("test.csv") print("train shape:", train.shape) print("test shape:", test.shape) # 按列计算缺失率,只看确实有缺失的列 miss = train.isnull().sum() miss = miss[miss > 0].sort_values(ascending=False) print(miss.head(25)) # 目标变量分布 import matplotlib.pyplot as plt plt.figure(figsize=(8, 4)) plt.hist(train["SalePrice"], bins=60, edgecolor="white") plt.title("SalePrice distribution") plt.tight_layout() plt.show()

train 和 test 的行数差异是这个比赛的重要信号:test 比 train 多出不少行,很多全局统计量必须基于合并后的全量数据计算,否则填充结果会失真。缺失率的统计用来区分两类特征:一类是「缺失本身有语义」的,比如 Alley、PoolQC、Fence,缺失代表「没有」;另一类是「缺失就是脏数据」的,比如 LotFrontage、GarageYrBlt,需要按业务关系推断填充。

目标分布这一步常被跳过。SalePrice 是典型的右偏分布,如果你直接在这个分布上训练回归模型,误差会被高价房主导,最终提交的 RMSLE 会很难看。所以代码包里几乎必然有一步目标值对数变换,对应在代码里通常是np.log1p(train["SalePrice"])。

2.2 缺失值不是脏数据,是特征:按语义拆成两组填充方案

缺失值处理的常见错误是统一fillna(0)或者统一fillna("None")。在这个比赛里,这两招单独使用都会丢分。我习惯把缺失列分成「类型缺失」和「数值缺失」两组:

  • 类型缺失:Alley、MasVnrType、BsmtQual、FireplaceQu、GarageType 等,缺失代表该房屋没有对应设施,填"None"字符串最合理。
  • 数值缺失:LotFrontage 与街道宽度相关,应按 Neighborhood 分组取中位数填充;GarageYrBlt 在 GarageType 为 None 时填 0,否则按 GarageArea 是否有值来处理。
# 类型缺失列:填充为 "None",再加入一个指示列标记缺失 categorical_missing = ["Alley", "MasVnrType", "BsmtQual", "BsmtCond", "FireplaceQu", "GarageType", "PoolQC", "Fence"] for col in categorical_missing: train[col + "_na"] = train[col].isnull().astype(int) test[col + "_na"] = test[col].isnull().astype(int) train[col] = train[col].fillna("None") test[col] = test[col].fillna("None") # 数值缺失列:分业务处理,不统一填中位数 train["LotFrontage"] = train.groupby("Neighborhood")["LotFrontage"].transform( lambda x: x.fillna(x.median())) test["LotFrontage"] = test.groupby("Neighborhood")["LotFrontage"].transform( lambda x: x.fillna(x.median())) garage_mask = train["GarageType"] == "None" train.loc[garage_mask, "GarageYrBlt"] = 0 test.loc[test["GarageType"] == "None", "GarageYrBlt"] = 0

这段代码有两个关键点。第一,缺失指示列_na是单独构造的特征,模型可以学出「这个字段缺失与否」与房价的关系,在 PoolQC、Fence 这类稀疏特征上尤其有用。第二,LotFrontage 的填充按 Neighborhood 分组进行,因为同一街区的临街宽度分布更接近,直接全表中位数填充相当于人为引入噪声。GarageYrBlt 填 0 而不是填年份中位数,是因为年份为 0 在树模型里可以被当作独立分支,而填入一个真实年份会误导模型以为该房屋在某个年份建了车库。

提示:合并 train 和 test 之后再算填充统计量更稳妥。单独拿 train 的中位数去填 test,遇到房价上涨年份或新区块时会留下系统性偏差。代码包里如果只有 train 的统计量,建议自己改成pd.concat([train, test])先合并。

2.3 目标值 log1p 不是可选操作:RMSLE 下的标准动作

这个比赛官方评估指标是 RMSLE,也就是对预测值和真实值都取对数后再算均方根误差。如果你直接用 RMSE 调参,模型会拼命拟合高价房,中低价位的误差完全失控。代码包里通常会写train["SalePrice"] = np.log1p(train["SalePrice"]),然后所有模型的预测目标都变成 log(SalePrice+1)。

为什么是 log1p 而不是 log?因为 SalePrice 有可能出现 0(极少见但存在),log(0) 会得到负无穷,log1p 加 1 之后避免了这个问题。预测阶段要用np.expm1(pred)还原成真实房价,最后提交的 SalePrice 列必须是还原后的价格,而不是对数。很多人漏掉最后一步,导致提交文件所有价格都接近 e 的某次幂,分数直接崩盘。

# 目标变换 train["SalePrice_log"] = np.log1p(train["SalePrice"]) # 建模后用这个函数还原 def invert_log1p(pred): return np.expm1(pred)

3. 跑通最小可复现的 LightGBM 五折基线:RMSLE 怎么算才对得上

3.1 统一预处理管道:把特征合并、编码、建模塞进同一套逻辑

代码包里的特征工程脚本可能写得很长,但核心无非是:数值特征原样保留,类别特征做 One-Hot 或标签编码,最后拼成一个大宽表。我习惯先用一个最小管道跑通基线,再去叠加那些看起来高级的交互特征。基线模型选 LightGBM,理由是训练快、对缺失值天然容忍、不需要做特征归一化。

from sklearn.model_selection import KFold from sklearn.preprocessing import LabelEncoder import lightgbm as lgb features = [c for c in train.columns if c not in ("Id", "SalePrice", "SalePrice_log")] # 标签编码:只对类别列做,避免 One-Hot 把维度撑爆 for col in train[features].select_dtypes(include="object").columns: le = LabelEncoder() # 合并 train/test 防止新类别出现在 test 中 combined = pd.concat([train[col], test[col]], axis=0).astype(str) le.fit(combined) train[col] = le.transform(train[col].astype(str)) test[col] = le.transform(test[col].astype(str)) X = train[features] y = train["SalePrice_log"]

LabelEncoder 的 fit 对象必须是 train 和 test 合并后的全集。不然如果 test 里出现一个 train 中没有的类别值,transform 阶段会直接抛 KeyError,这是复现代码包时很常见的中断点。数值列不用做标准化,因为 LightGBM 是树模型,特征的单调变换不影响分裂点,但如果你后续要叠 Ridge,就必须再做 StandardScaler。

3.2 五折交叉验证与 RMSLE 计算:避免「本地高分、线上翻车」

拟合整个训练集然后看 train_score 是最常见的自欺欺人操作。正确做法是 KFold 五折,每一折留下一个验证集,记录验证集的 RMSLE,最后取五折均值。这个均值才约等于你在 Kaggle 排行榜上会拿到的分数,偏差通常在 ±0.01 以内。

def rmsle(y_true, y_pred): return np.sqrt(np.mean((y_true - y_pred) ** 2)) kf = KFold(n_splits=5, shuffle=True, random_state=42) oof = np.zeros(len(X)) valid_scores = [] for fold, (tr_idx, va_idx) in enumerate(kf.split(X)): X_tr, X_va = X.iloc[tr_idx], X.iloc[va_idx] y_tr, y_va = y.iloc[tr_idx], y.iloc[va_idx] model = lgb.LGBMRegressor( n_estimators=1000, learning_rate=0.05, max_depth=4, num_leaves=15, subsample=0.8, colsample_bytree=0.8, random_state=42 ) model.fit( X_tr, y_tr, eval_set=[(X_va, y_va)], eval_metric="rmse", callbacks=[lgb.early_stopping(100), lgb.log_evaluation(200)] ) pred_va = model.predict(X_va) oof[va_idx] = pred_va fold_score = rmsle(y_va, pred_va) valid_scores.append(fold_score) print(f"fold {fold} rmsle: {fold_score:.5f}") print(f"OOF RMSLE: {rmsle(y, oof):.5f}")

这段代码里eval_metric="rmse"看着和 RMSLE 不一致,但因为 y 已经做过 log1p,对 log 目标算 RMSE 等价于对原目标算 RMSLE,这是代码包里最常让人疑惑的一行。early_stopping 的 patience 设为 100,防止树数目过多导致过拟合;如果你发现 OOF 分数远低于 README 里的分数,先检查是不是random_state不一样导致验证划分不同。KFold 的 shuffle 必须固定种子,这一点在复现别人代码时尤其容易踩坑——不少公开代码包把random_state=42写死,你改成自己的随机数可能分数更好,但那就不是这份 zip 的效果了。

3.3 全量训练与预测:把验证脚本变成提交脚本的最后一段

五折验证结束之后,要用全部训练数据重新训练一次模型,再用同样的模型预测 test。很多人验证和最终训练共用同一个模型,导致提交分数比验证分数差一截。原因很简单:验证时每折只用了 80% 数据,最终模型用 100% 数据训练,理论上分布略有差异,但更关键的是 early_stopping 的轮数不同,树数量也不同,所以必须重新拟合。

final_model = lgb.LGBMRegressor( n_estimators=1000, learning_rate=0.05, max_depth=4, num_leaves=15, subsample=0.8, colsample_bytree=0.8, random_state=42 ) final_model.fit( X, y, eval_set=[(X, y)], # 这里不需要 early stopping eval_metric="rmse", callbacks=[lgb.log_evaluation(200)] ) pred_test = final_model.predict(test[features]) pred_price = np.expm1(pred_test) submission = pd.DataFrame({ "Id": test["Id"], "SalePrice": pred_price }) submission.to_csv("submission_lgb.csv", index=False)

eval_set在最终训练时可以传自己的训练集,但没有任何验证意义,只是让日志能显示进度。预测出的pred_test是对数空间的数值,必须经过np.expm1还原。提交文件的列名和顺序都很讲究,Id必须来自 test 原始文件,SalePrice不能是整数、不能含负数,这两点在后面的避坑章节还会展开。

4. 把分数刷到 Top 档位:三个参数、一次网格搜索、二次模型融合

4.1 先调这三个参数:num_leaves、max_depth、learning_rate

木模型参数调优的顺序比参数本身更重要。我见过很多人在 GridSearch 里一次丢进去五六个参数,跑十几个小时,最后分数的提升还不到 0.003,性价比极低。常见的合理顺序是:固定 learning_rate=0.05,先调 num_leaves 和 max_depth,让模型容量匹配数据量;然后调 subsample 和 colsample_bytree 控制随机性;最后可以尝试降低 learning_rate 并相应增加 n_estimators。

这个数据只有不到 1500 行,num_leaves 超过 31 之后过拟合非常快。我一般从 15 开始,配合 max_depth=4 或 5 搜索。下面是一个小型网格搜索,总训练时间控制在几分钟内:

from sklearn.model_selection import GridSearchCV param_grid = { "num_leaves": [15, 31], "max_depth": [4, 5], "learning_rate": [0.05, 0.08], "n_estimators": [500, 1000] } lgb_model = lgb.LGBMRegressor( subsample=0.8, colsample_bytree=0.8, random_state=42 ) grid = GridSearchCV( lgb_model, param_grid, cv=KFold(n_splits=5, shuffle=True, random_state=42), scoring="neg_root_mean_squared_error", n_jobs=-1, verbose=1 ) grid.fit(X, y) print("best params:", grid.best_params_) print("best cv score:", -grid.best_score_)

这里的 scoring 选 neg_root_mean_squared_error 而不是 rmsle,因为 y 已经 log 过。使用 GridSearchCV 时一定把 cv 参数显式写成一个固定种子的 KFold 对象,否则内部默认的 KFold 不设 shuffle,你复现不了别人的搜索结果。n_jobs=-1 会占满所有 CPU 核心,在笔记本上跑会风扇狂转;如果你还在做别的事,建议改成 n_jobs=4。

4.2 让第二个模型长得不一样:XGBoost 与 Ridge 的互补逻辑

融合的前提是模型之间差异够大。LightGBM 和 XGBoost 在特征处理上高度相似,如果只用这两个模型做简单平均,分数提升很有限,真正能拉开差距的是加入一个线性模型 Ridge。Ridge 对数值型特征的尺度敏感,所以特征管道要单独准备一份标准化后的版本。

from sklearn.preprocessing import StandardScaler from sklearn.linear_model import Ridge from sklearn.pipeline import make_pipeline # 数值特征单独取出来,做标准化 num_cols = X.select_dtypes(include=[np.number]).columns.tolist() ridge_pipeline = make_pipeline( StandardScaler(), Ridge(alpha=15.0) ) ridge_oof = np.zeros(len(X)) for fold, (tr_idx, va_idx) in enumerate(kf.split(X)): X_tr, X_va = X.iloc[tr_idx, :], X.iloc[va_idx, :] y_tr, y_va = y.iloc[tr_idx], y.iloc[va_idx] ridge_pipeline.fit(X_tr, y_tr) ridge_oof[va_idx] = ridge_pipeline.predict(X_va) print("Ridge OOF RMSLE:", rmsle(y, ridge_oof))

Ridge 的 alpha 值在这个比赛里常见范围是 5 到 25。alpha 太大模型过于平滑,太小则容易过拟合噪声。Ridge 和 LightGBM 的融合通常比两个树模型的平均要好,因为线性模型能捕获树模型容易忽略的全局线性趋势。你也可以尝试核岭回归或 ElasticNet,但我试下来在这个数据规模下提升不大。

4.3 Stacking 融合:用 OOF 预测值喂给元模型,而不是简单平均

简单平均是省事,但 Stacking 通常能再挤 0.002 到 0.005 的提升。做法是把 LightGBM 和 Ridge 的五折验证预测值(OOF)作为两个特征,再加一个简单的线性回归作为元模型,用同样的五折划分训练这个元模型。

stack_X = pd.DataFrame({ "lgb_oof": oof, "ridge_oof": ridge_oof, }) stack_y = y meta_model = Ridge(alpha=1.0) stack_kf = KFold(n_splits=5, shuffle=True, random_state=42) stack_pred = np.zeros(len(stack_X)) for fold, (tr_idx, va_idx) in enumerate(stack_kf.split(stack_X)): meta_model.fit(stack_X.iloc[tr_idx], stack_y.iloc[tr_idx]) stack_pred[va_idx] = meta_model.predict(stack_X.iloc[va_idx]) print("Stacking OOF RMSLE:", rmsle(stack_y, stack_pred))

如果 Stacking 之后的分数还没有单模型好,先怀疑你的 OOF 是否发生了泄漏。泄漏的常见来源是:填充缺失值时用了全量数据的统计量,或者 LabelEncoder 的 fit 对象包含了验证折的信息。规避办法是填充统计量先基于 train 部分计算,再应用到验证部分,但这会让代码复杂不少。折中做法是:先接受全量填充带来的微小偏差,因为它在 train 和 test 之间的影响是对称的,然后在 Stacking 层用更强的正则化(Ridge 的 alpha 调大)来抑制过拟合。

注意:千万不要把 test 的预测结果放进 Stacking 的训练特征里。有人图省事,把 test 先跑一遍得到预测,拿这个预测去和 OOF 一起训元模型,这等于让元模型偷看了测试集,线上分数必定翻车。

4.4 提交文件生成:Shape、索引、列名一个都不能错

Kaggle 对提交文件的校验很严格:必须有 Id 和 SalePrice 两列,行数必须与 test.csv 完全一致,且索引顺序要一一对应。常见错误是提交时pd.read_csv("test.csv")["Id"]已经被前面的特征工程改变了顺序,或者reset_index没做导致 Id 错位。

submission = pd.DataFrame({ "Id": test["Id"], "SalePrice": np.expm1(stack_test_pred) }) submission.to_csv("submission_stack.csv", index=False) print(submission.shape) print(submission.head())

stack_test_pred 是对数空间的预测结果,你需要在最终训练用全量数据重新生成 lgb_test_pred 和 ridge_test_pred,再交给元模型预测。这里最容易翻车的点是:全量训练的预测值和 OOF 预测值是两套不同的模型状态,你如果偷懒直接复用验证阶段的模型,test 形状不匹配会直接报错。行数校验可以用一句断言assert submission.shape[0] == 1459,但要记住 train.csv 的行数加上 test.csv 的行数才是这个比赛的全量样本数。

5. 复现这套比赛代码时最容易翻车的 5 个坑:现象、原因、解决

5.1 提交后分数远差于本地 CV,且预测价格出现负值

现象:本地 OOF RMSLE 0.115,线上提交 0.165,打开 submission 发现有一批 SalePrice 是负数。

原因:本地验证时 y 和预测都在对数空间,RMSLE 看起来很好;但提交前没有把预测值做np.expm1还原,或者还原后没有对极值做裁剪。更隐蔽的是,某些特征填充 0 后,树模型会产生低于 log(1) 的预测,还原后就是负数。

解决:提交前对预测值做np.maximum(pred, 0),并打印min(pred)、max(pred)和分布分位数。实际业务中房价不可能为负,这是管线里最后的防线。

pred_price = np.maximum(np.expm1(pred_test), 0)

5.2 尝试统一 fillna(0) 或 fillna("None") 后 CV 反而恶化

现象:某开源代码包对所有缺失列统一fillna("None"),跑出来的分数比你自己精修的填充策略只差 0.003,但如果照搬到另一个数据集直接崩盘。

原因:在这个比赛里,统一填充之所以伤害不大,是因为树模型本身对常数填充有容忍度。但 LotFrontage 和 GarageYrBlt 这类数值列不应该填入字符串,统一填充会改变列类型;某些代码用fillna(0)处理 BsmtQual 这种分类列,等于把「没有地下室」和「地下室质量最差」混为一个分支。

解决:先跑一遍缺失率统计,区分语义缺失和数据缺失,分别写填充逻辑。这也是为什么我不建议直接拿网上的脚本来跑自己的数据——填充策略必须贴着业务语义走。

5.3 GridSearchCV 的cv参数没固定种子,结果不可复现

现象:同一个 zip、同一个参数网格,第一天跑出 best_score 0.120,第二天变成 0.123,你怀疑代码被人改过。

原因:GridSearchCV 默认使用KFold(5),不带 shuffle,所以每次运行的数据划分完全一致。但如果你在 Notebook 里先设置过全局随机种子再运行,某些依赖随机性的预处理步骤会影响划分;更常见的是手动传了cv=5,而 5 在 sklearn 里会被解释成KFold(shuffle=False),一旦数据集顺序不同,划分就不同。

解决:始终显式传cv=KFold(n_splits=5, shuffle=True, random_state=42),并且在脚本开头固定np.random.seed(42)。复现别人的比赛代码时,先检查这一处,比检查模型参数更重要。

5.4 训练用dropna()删行导致形状对不上,提交直接报错

现象:复现脚本时报错ValueError: X has 1459 samples, but y has 1456 samples,或者提交文件行数比 test.csv 多一行。

原因:很多人看到缺失值的第一反应是train.dropna(inplace=True)。这个比赛 test 比 train 多 3 行,两边的缺失模式不同,直接删行会让训练集和测试集列数不一致,等到建模阶段就崩。更常见的情况是特征工程时忘了reset_index(),训练集索引是[0, 1455],测试集索引是[0, 1458],concat 后全是 NaN。

解决:永远不要对行做 dropna,所有列操作都要保留原始行数。在脚本末尾加断言:

assert train.shape[0] == 1460 assert test.shape[0] == 1459

5.5 模型融合后分数没变甚至下降,因为融合的两个模型太像

现象:LightGBM 的 OOF 是 0.114,XGBoost 的 OOF 也是 0.114,取平均后还是 0.114,Stacking 后变成 0.115,你怀疑是代码包作者在吹水。

原因:两个树模型共用同一套特征和相似的目标函数,预测误差高度正相关。平均两个高度相关的模型不会降低方差,反而可能放大某个模型的系统性偏差。

解决:换一个决策路径完全不同的模型族,比如线性模型、KNN 或带 RF 的浅层模型。这个比赛里最有效的互补模型是 Ridge + LightGBM,不是 XGBoost + LightGBM。如果你一定要用 XGBoost,可以调整它的线性树数和 boosting 方式,让它更接近线性模型,但这比直接加 Ridge 麻烦得多。

6. 提交前用预测分布和 SHAP 验证模型:一个我自己保留的检查顺序

分数不是唯一指标,提交前我会花 10 分钟做三项检查,全过之后才会上传。

第一项是预测分布对比。训练集 SalePrice 经 log1p 后的分布接近正态,预测分布也应该接近,且右尾不能比训练集长出太多。画两个直方图重叠就能看出异常:

plt.figure(figsize=(10, 4)) plt.hist(np.log1p(train["SalePrice"]), bins=50, alpha=0.6, label="train") plt.hist(np.log1p(pred_price), bins=50, alpha=0.6, label="pred") plt.legend() plt.show()

如果预测分布的整体形状与训练集明显不同,比如双峰或者长尾,大概率是特征编码出了问题,而不是模型参数问题,先回去查填充策略和类别编码。

第二项是 SHAP 值。树模型的 SHAP 能直接告诉你哪些特征在驱动预测,如果 Top 特征里混进了Id或者你构造的某个计数特征,模型就是在记编号而不是学规律。

import shap explainer = shap.TreeExplainer(final_model) shap_values = explainer.shap_values(X) shap.summary_plot(shap_values, X, max_display=15)

第三项是极端值检查。模拟项目 X 中我吃过一次亏:预测的中位数很合理,但最高房价达到 80 万,比训练集最大值还高一倍。后来发现是GrLivArea在 test 里有个离群值,而我没有对它做裁剪。现在的习惯是提交前打印pred_price.describe(),看到分位数异常就停下来排查。

这套检查顺序不复杂,但它帮我避免过至少三次「本地验证挺好、提交分数被打脸」的经历。代码包里的模型参数可以改,管道结构可以重写,但这个「分布、SHAP、极端值」的检查顺序我从来没有跳过。比赛结束之后,这份检查清单也会跟着我的日常工作走,毕竟线上模型跑起来之后,你同样需要知道自己预测的边界在哪。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 3:19:36

PyTorch图像识别实战:从数据增强到模型训练与部署

搞图像识别这个方向,PyTorch基本是目前绕不开的工具。这篇文章我不打算讲那种“从零入门深度学习”的教科书内容,而是直接进入实战:给定一个图像分类任务,如何用PyTorch把完整流程走通,包括数据集怎么处理、网络怎么搭…

作者头像 李华
网站建设 2026/10/22 3:19:12

RAG实战指南:从原理到系统搭建,避开工程常见坑

最近总有朋友问我:RAG到底是什么?为什么大模型明明很能聊,一问到自家文档里的事实就胡编乱造?实际上,RAG(Retrieval-Augmented Generation,检索增强生成)是目前把大模型接进私有知识…

作者头像 李华
网站建设 2026/10/22 3:19:11

RAG实战指南:从原理到调优,构建大模型知识库问答系统

如果你最近在关注大模型应用开发,肯定绕不开一个词:RAG(Retrieval-Augmented Generation,检索增强生成)。说白了,它就是给大模型装上一个“外挂知识库”——先检索再回答,而不是让模型硬着头皮猜…

作者头像 李华
网站建设 2026/10/22 3:19:02

Spring Boot+Vue前后端分离注销登录实战:JWT黑名单机制与部署踩坑

最近有个项目正好部署上线,测试那边的同事跑过一轮回归后跟我说:“退出登录这个功能,你们是不是没做?我点完退出,再手动敲接口地址,还能拉到用户数据。”我说做了啊,前端点击之后明明跳转到登录…

作者头像 李华
网站建设 2026/10/22 3:18:47

GRE封装原理与实战:不是隧道而是信封

1. 为什么GRE不是“隧道”而是“封装信封”——从快递寄送讲清本质很多人第一次接触GRE时,听到“通用路由封装”这个术语,下意识就把它想象成一条挖在地下的物理隧道:数据包钻进去,穿过去,再钻出来。这种类比看似直观&…

作者头像 李华