news 2026/10/14 6:03:57

决策树回归实战:基于sklearn的DecisionTreeRegressor完整指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
决策树回归实战:基于sklearn的DecisionTreeRegressor完整指南

要说机器学习里最友好的入门模型,决策树绝对排得上号。可大多数人一上手就写分类,到回归就有点懵——标签从离散变成连续,预测的东西还能叫“树”吗?参数怎么设?效果怎么评?跟分类完全是两码事。正好,我手上有个模拟项目,核心就是“机器学习算法_DT_决策树模型案例2_基于sklearn接口的回归”,说白了就是用 sklearn 的 DecisionTreeRegressor 做一套完整的回归流程。借着这个案例,我把从数据准备、参数选择、训练评估到调参避坑的完整过程撸一遍,顺手把踩过的坑都放出来。如果你是刚学 sklearn 的新手,或者只会分类、想补上回归这块技能树的同学,这篇可以直接当操作手册用。

1. 回归树到底在做什么,和分类树差在哪

1.1 回归树的预测结果为什么是“阶梯状”的

很多人第一次看回归树都会有同一个疑问:决策树不是用来做分类的吗,怎么还能预测连续数值?

其实回归树和分类树的框架没差多少,核心都是“递归划分”。区别在于,分类树的叶子节点存的是类别标签,做预测时取的是多数投票;而回归树的叶子节点存的是样本目标值的平均值,预测时直接用这个均值作为输出。

举个例子,如果树把样本分成“面积大于 90 平”和“面积小于等于 90 平”两支,分类树会在叶子节点标记“豪宅”或“普通房”,而回归树会在左边叶子节点存下这批样本的平均单价,比如 54000 元/平。新样本落进哪个叶子,就拿那个叶子的均值去预测。

这就带来一个很直观的特点:回归树的预测结果天然是阶梯状的。你画预测值和真实值的散点图时,会看到预测值集中在几个固定水平线上,而不是一条平滑的斜线。原因是树模型只能做分段常数逼近,树的叶子数量决定了输出值能取几种“档位”。叶子越多,台阶越密;叶子少,台阶就粗糙。

这个特性既是优点也是缺点。优点是拟合非线性关系很强,比如“面积对价格的影响在前 60 平时很陡、60 平以后趋缓”这种分段变化,决策树能自动捕捉;缺点是对连续变量的变化不够细腻,而且特别容易过拟合到训练集的细节上。理解了“阶梯状”这个概念,后面看评估指标、过拟合就顺了。

1.2 不自己手写,直接用 sklearn 接口的理由

网上有很多讲决策树原理的教程,动不动就手写一个决策树类。原理确实值得看,但实际干活时,手写树模块基本是重复造轮子。

sklearn 的DecisionTreeRegressor接口提供的价值,不止是“帮你实现了一个树”,更在于它在工业标准上做得很完整。比如:

  • 分裂准则有多种选择,回归场景常用squared_error、friedman_mse、absolute_error,这在手写版本里未必考虑得那么细;
  • 预剪枝参数(深度、叶子节点样本数等)都是现成的,随手就能调;
  • 剪枝路径cost_complexity_pruning_path也内置了,后剪枝做起来非常轻松;
  • 可以无缝接进Pipeline、GridSearchCV、cross_val_score这些工具链,调参和验证非常省事。

我自己早期也做过一个练手项目,自己写分裂逻辑、自己算熵,但加一个min_samples_leaf参数就要改半天,效果还不一定对。后来转到 sklearn 接口,发现一行参数就能控制模型复杂度,根本不用动树结构代码。对新手来说,先用 sklearn 把流程跑通、把评估体系建立起来,再回头去读源码理解机制,反而更高效。

2. 准备数据和参数,先把 DecisionTreeRegressor 吃透

2.1 数据准备好了吗?回归建模前一定要做的 3 件事

用回归树建模之前,数据准备有几个坑是新手必踩的。以我这次模拟项目为例,数据集是某平台的房源成交记录,目标变量是每平方米成交单价,特征有面积、房龄、楼层、朝向、所在商圈等。

第一件事:特征里不能有缺失值。回归树虽然不像线性回归那样对缺失值“零容忍”,但 sklearn 的DecisionTreeRegressor目前并不会自动处理缺失值,出现NaN就会报错。最简单的办法是用均值或中位数填充,比如房龄缺失就用中位数填。对树模型来说,填充方式的影响相对小,因为它本质是按阈值切分,不太依赖特征的精确数值。

第二件事:类别特征要么编码,要么直接用数值特征。树模型可以做特征组合,但对中文文本类取值(比如“朝南”“朝北”)是没法直接算阈值的。我会把“朝向”这种低基数类别用独热编码,但如果类别特别多(比如商圈有 60 个),独热编码会把特征维度拉得很高,这时候可以考虑标签编码,或者干脆把高频类单独抽出来变成新特征。这个后面在常见问题里再展开。

第三件事:检查一下目标变量分布。回归树在预测时输出的是叶子节点均值,如果目标变量本身是重尾分布(比如少数豪宅价格极高),树的叶子均值会被极端值拉得很偏。我一般会先看一眼目标变量的直方图,如果偏得厉害,可以对目标变量取对数,预测完再转回来。这个技巧在房价、销售额这类场景里非常实用。

2.2 DecisionTreeRegressor 核心参数速查与选择逻辑

很多教程把每个参数都讲一遍,让人看完更晕。其实回归树里常用的参数就几个,我平时会给新手一张速查表,照着选就行。

参数默认值核心作用我的常用设置
criterionsquared_error分裂时用什么指标衡量不纯度默认即可,噪声大时可试 friedman_mse
max_depthNone树最大深度,限制模型复杂度3~8,看数据量,先固定住
min_samples_split2内部节点继续分裂所需最少样本数10~20,数据量越大可以设越高
min_samples_leaf1叶子节点最少样本数,防过拟合最有效5~10
max_featuresNone(全部)每次分裂时最多考虑的特征数回归里可设 1.0 或 sqrt
max_leaf_nodesNone最大叶子节点数,等价于另一种深度约束和 max_depth 二选一设即可
ccp_alpha0后剪枝复杂度惩罚系数配合剪枝路径找拐点

先看criterion。回归树默认用squared_error,也就是最小化均方误差。friedman_mse是在此基础上加了一个改进的权重修正,对某些有噪声的数据集更稳一点。absolute_error则用绝对误差,对异常值不那么敏感。我自己的习惯是:先默认squared_error跑一轮,如果发现预测被极端值带偏,再换掉。

再看max_depth和min_samples_leaf。这两个是控制“树长多深、叶子多小”的关键。max_depth限制的是整体深度,min_samples_leaf则保证每个叶子都有足够样本支撑。后者的解释性更强:如果叶子节点只有 1 个样本,那这个节点就是纯粹背答案,泛化肯定差。我通常先设min_samples_leaf=5,再逐步减小去感受训练误差和测试误差的变化。

至于random_state,很多人忽略。回归树在特征选择上默认是“所有特征都参与”,不存在随机抽样,所以理论上不设随机种子也能复现结果。但如果你设置了max_features或者用了类似的操作,结果就会随随机状态变化。我的建议是固定random_state=42,少一个变量,后面调参分析才靠谱。

3. 从训练到预测,撸一套可复用的回归代码

3.1 训练与预测:最小可运行版本

直接上代码。这是我模拟项目里的核心骨架,去掉业务细节以后非常干净。

import pandas as pd from sklearn.model_selection import train_test_split from sklearn.tree import DecisionTreeRegressor from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score # 假设 df 已经做好特征工程,目标列是 unit_price X = df.drop("unit_price", axis=1) y = df["unit_price"] X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42 ) model = DecisionTreeRegressor( max_depth=5, min_samples_leaf=5, random_state=42, ) model.fit(X_train, y_train) y_pred = model.predict(X_test) mse = mean_squared_error(y_test, y_pred) mae = mean_absolute_error(y_test, y_pred) r2 = r2_score(y_test, y_pred) print(f"MSE: {mse:.2f}") print(f"MAE: {mae:.2f}") print(f"R² : {r2:.4f}")

这段代码第一次跑通,整个决策树回归流程就走完了。fit做的事情本质上是递归地把特征空间切开:每次找一个特征和一个阈值,把当前节点的样本分成两组,目标是让分裂后的两组均方误差之和最小。这个过程会一直持续到触发停止条件(深度达到上限、样本数小于阈值、误差不再下降等)。

这里有个值得说的细节:fit用的数据是训练集还是全量数据。很多人图省事,直接把全部数据灌进去训练,再用同一批数据评估,结果一看 R² 高得离谱,上线以后崩得厉害。项目里我严格切了 20% 作为测试集,测试集在训练阶段完全不参与建模。后面看训练集分数和测试集分数的差距,才能真正判断过拟合。

3.2 把树的决策过程画出来看

回归树跟神经网络不一样,它天然自带解释性。训练完之后,我强烈建议画一下树结构,哪怕是生成一个简化的文本版。

from sklearn.tree import export_text, plot_tree # 文本版,适合快速看拆分逻辑 print(export_text(model, feature_names=list(X.columns), max_depth=3)) # 图形版,适合汇报和自查 plot_tree(model, feature_names=list(X.columns), filled=True)

图形版里每个节点会显示分裂条件、当前样本数量、该节点的均方误差和预测值。能特别直观看出来的问题是:如果一个叶子节点的样本数只有个位数,或者树在某个特征上反复横跳,就说明模型可能学到了噪声。

我在项目里画完图后就发现,树前几层分裂几乎全集中在“面积”和“房龄”两个特征上,其他特征要到很深的层才出现。这说明模型的主要信息来自这两个变量。这种结论在给业务方解释模型时特别有用——不用讲复杂的特征重要性理论,直接指图就行。

3.3 分类案例换成回归,只改这几个地方

如果你之前写过 sklearn 决策树分类,现在换回归,其实不用否定重来。差异点就四个,记清楚就够了。

  • 模型类:DecisionTreeClassifier换成DecisionTreeRegressor;
  • 分裂指标:分类的gini/entropy换成回归的squared_error等;
  • 预测输出:predict_proba不能用了,回归树只有直接的数值预测;
  • 评估指标:准确率、F1 换成 MSE、MAE、R²。

模型原理、训练流程、可视化方式基本复用。所以我常说,决策树是学习 sklearn 接口最好的“一站体验”,分类和回归打通以后,后面学随机森林、梯度提升树都会顺畅很多。

4. 回归效果怎么看:MSE、MAE、R² 与误差分布

4.1 三个指标刻度和含义完全不一样

回归任务的评估指标比分类要多,而且新手特别容易只盯着一个数字看。我把项目里用到的三个核心指标逐个拆开说。

MSE(均方误差)MSE = mean((y_test - y_pred)^2)

因为误差要平方,它会给大误差更大的惩罚。一个样本差 100 对 MSE 的贡献,是四个样本各差 50 的四倍。所以 MSE 对异常值特别敏感。在房价这种有极端高价样本的数据里,MSE 可能很大,并且主要被少数极端样本“带节奏”。

MAE(平均绝对误差)MAE = mean(abs(y_test - y_pred))

这个指标更符合人的直觉,直接说“平均每个样本差多少钱/多少单位”。它对异常值的敏感度比 MSE 低得多。如果你更关心普通样本的预测误差,MAE 是更实用的业务指标。

R²(决定系数)R² = 1 - SS_res / SS_tot

R² 衡量的是模型相比“直接用平均值预测”好了多少。R² = 1 表示完美预测,R² = 0 表示模型效果等同于基准均值,负数表示比均值预测还差。它的优势是归一化,可以跨数据集做粗略比较,但 R² 不是唯一标准,优化 R² 不等于优化 MAE。

在模拟项目里,印象很深刻:第一次跑出来的 MSE 看起来三万多,感觉很大,换成 MAE 大概是 4000,解读起来就很有业务含义了。所以我的习惯是三个指标全打印出来一起看,MSE 看异常值影响,MAE 看业务可解释误差,R² 看整体模型水平。

4.2 误差分布能告诉你模型哪里不对

光有平均数值还不够,误差分布才是关键。我每次跑完回归,都会把残差(y_test - y_pred)画个直方图或者散点图,看残差随预测值的变化。

在这个项目里,我发现一个典型现象:当预测价在 35000 元/平以下时,残差很小;但预测价超过 45000 元/平后,残差方向几乎一致——都是真实值远大于预测值。这说明什么问题?树模型在高价位区间的拟合力不足,很可能是因为高价位样本太少,树的叶子节点没法细分这部分空间。

这种观察是三个平均指标给不出来的。指标告诉你“模型不准”,误差分布告诉你“模型在哪一段不准”。所以评估回归模型时,我强烈建议把“指标 + 残差图”绑定使用,缺一不可。

4.3 用训练/测试分数差判断过拟合

回归模型最容易犯的毛病就是过拟合,判断方法比调参还重要。我这套流程里,最简单的判断方式是分别算训练集和测试集上的 R² 或 MSE。

train_r2 = r2_score(y_train, model.predict(X_train)) test_r2 = r2_score(y_test, y_pred) print(f"train R²: {train_r2:.4f}") print(f"test R²: {test_r2:.4f}")

如果训练 R² 是 0.98,测试 R² 是 0.62,中间差了一大截,不用怀疑,这就是典型的过拟合。第一次用未限制深度的默认树跑时,我这边训练 R² 高达 0.96,测试 R² 只有 0.53。这说明树已经快把训练数据背下来了,但因为数据本身有噪声和分布差异,一到新数据就露馅。

反过来,如果训练和测试 R² 都很低(比如都不到 0.3),那是欠拟合,说明模型太弱或者特征没选对。达到过拟合还是欠拟合的判断之后,下一步调参才不是瞎试。

5. 调参与防过拟合:我实际采用的调参顺序

5.1 单棵决策树为什么天生容易过拟合

决策树的本质是“递归切割”,没有限制的话,它可以一直切到每个叶子只有一个样本。这时候模型把训练集的每一个点都记住了,跟背书一样,自然换数据就失灵。

更麻烦的是,决策树对特征尺度不敏感,对特征与目标之间的偶然相关性极其敏感。数据量一少,某些无关特征刚好跟目标在样本上巧合相关,树就会拿它做高层的分裂,导致模型学了一堆不可复现的规律。这也是为什么单棵回归树容易过拟合,而随机森林通过多棵树平均能缓解这个问题。

过拟合的外部表现,除了训练/测试分数差距大之外,还有一个肉眼可见的信号:树的结构特别深,叶子数特别多,画出来的图密密麻麻。这时候不是先加数据,而是先给树套上复杂度限制。

5.2 手动调参的顺序和判断标准

很多人调参拿到参数列表就蒙了,不知道先动哪个。我的实际经验是:按这种顺序来,既快又好解释。

第一步,先固定max_depth。一个比较稳的起点是 3 到 5,看数据量大小。深度固定以后,树的整体复杂度就有上界了,后面调参都在这个框架里。

第二步,再调min_samples_leaf。把叶子节点的最小样本数从 1 提到 5、10、20,观察测试集 R² 的变化。这一步是性价比最高的防过拟合手段,改动一个参数就能明显看到分数差距缩小。

第三步,调整max_features。做回归时,如果特征数量不多(比如 10 个以内),默认全部参与分裂即可;如果特征有几十个,可以试max_features=0.5或max_features='sqrt',引入一些随机性,降低单一强特征主导分裂的问题。

第四步,配合后剪枝。用ccp_alpha做剪枝,通常能再压一点过拟合。这个我单独详细说。

以我项目为例,初始默认树在测试集上的 R² 只有 0.53。固定max_depth=5后,测试 R² 提到 0.68;再把min_samples_leaf=5,升到 0.71。虽然后面提升幅度变小,但每一步改动都是可解释的,而不是靠蒙。

5.3 网格搜索和剪枝参数配合使用

手动调完一轮之后,如果想更系统地搜索参数组合,直接用GridSearchCV就行。不过网格搜索不是参数越多越好,搜索空间太大反而容易过拟合到验证集上。我通常只对两三个关键参数做网格:

from sklearn.model_selection import GridSearchCV param_grid = { "max_depth": [3, 5, 7], "min_samples_leaf": [3, 5, 10], "ccp_alpha": [0.0, 0.001, 0.005, 0.01], } grid = GridSearchCV( DecisionTreeRegressor(random_state=42), param_grid=param_grid, cv=5, scoring="r2", n_jobs=-1, ) grid.fit(X_train, y_train) print(grid.best_params_) print(grid.best_score_)

网格搜索的结果要小心看两个坑。第一个是scoring选什么指标,如果业务更关心 MAE,可以把scoring换成'neg_mean_absolute_error';第二个是样本量小的时候,cv=5 的验证分数波动很大,最好确认最优参数在多次重复下不是偶然。

ccp_alpha是后剪枝的核心参数。sklearn 提供了一条剪枝路径,可以帮你算出所有有效剪枝点对应的ccp_alpha和树的不纯度。实际操作我一般先画出“alpha 越大,训练分数越低”的曲线,选一个在测试分数下降不明显但树的复杂度明显降低的拐点值。这个拐点能用cost_complexity_pruning_path方法取到:

path = model.cost_complexity_pruning_path(X_train, y_train) print(path.ccp_alphas)

剪枝的意义在于:砍掉那些只对少数样本有效的分支,让树更“粗线条”但更稳。网格搜索里加ccp_alpha,本质上就是把后剪枝和预剪枝放在一起选,效果通常比只调深度好一些。

6. 常见问题与排查技巧实录

6.1 问题速查表

整理了几个我在决策树回归中踩过或见过的高频问题,放在一张表里,方便直接对号入座。

现象可能原因排查方向
训练 R² 很高、测试 R² 低过拟合限制 depth,提高 min_samples_leaf,加剪枝
预测值集中在少数几个档位树太浅或叶子太大增大 max_depth,减小 min_samples_leaf
输入有 NaN 报错数据未清洗补缺失值或删除缺失行
预测结果全是同一个值目标变量处理异常或特征全无效检查 y 是否被误当作常数,看特征分布
MSE 巨大且不稳定异常值占比高试试 MAE,做目标变量截断或对数变换
中文字符串特征无法输入类别特征未编码独热编码或标签编码
拿分类模型接口去跑回归模型选错换 DecisionTreeRegressor

这张表是我每次带新人时必发的心得。其实大部分问题不在算法理论,而在工程细节。

6.2 预测值不连续怎么办

预测值呈阶梯状是决策树本身的特性,不是 bug。但如果你发现阶梯太少、预测值看起来“很假”,那可能是树深度太浅,或者叶子节点样本数设得太大。这时候调两个参数:加大max_depth,或者减小min_samples_leaf。比如我把min_samples_leaf从 20 降到 5,预测值的“档位”明显变多,散点图也更好看。

当然,并不是档位越多越好。档位过多往往意味着叶子样本数太少,模型在训练集上过于精细,泛化能力反而下降。这需要回到评估指标做取舍。我一般会把测试集 R² 作为主要约束,在不显著掉分的前提下,尽量让叶子细一些,这样业务方的观感会好一些。

6.3 特征里有中文文本怎么处理

房源数据里像“板块”“小区名”这类特征,回归树是读不进去的。常见的处理方式有两个层面。一是把高价值的类别拆出来,做标签编码;二是低基数类别做独热编码。但要特别注意树模型的特性:独热编码会让特征维度膨胀,而树每次分裂只挑一个特征,所以即使有几十个编码列,每次分裂也只用到其中一个。这不会像线性回归那样引入严重的共线性问题,但会让树的结构不稳定,因为类别少的分支容易被浅层选中。

一个替代方案是:先用树模型跑一遍全部数值特征,再单独把类别特征标签编码后加入模型,对比 R² 变化。如果提升不大,说明类别特征信息冗余,可以直接不参与建模。我项目里一开始把 60 多个商圈都做了独热编码,模型没怎么变好,反而训练时间长了。后面改成只保留排名前 5 的高频商圈作为二值特征,效果反而稳定。

6.4 回归场景下的随机种子陷阱

分类任务里大家已经习惯设random_state,但回归树里很多人会忽略它。特别是当你使用GridSearchCV或者交叉验证时,如果没有固定种子,每次跑出来的训练/测试分数都不同,这会严重影响你判断调参是否有效。

我在项目里遇到过一个问题:同一组参数,第一次跑测试 R² 是 0.71,第二次变成 0.68,当时还以为是代码改坏了,排查了一圈发现只是train_test_split没设置随机种子。从那以后,我的所有建模代码,全统一random_state=42。固定种子会让实验可复现,对比参数效果时才能确定差异来自参数而不是数据切分。

6.5 数据量级差异大,MSE 指标异常要怎么看

回归任务不像分类有准确率这种直观刻度,MSE 的量级完全取决于目标变量本身。如果你的目标变量是“价格”,单位是元/平,MSE 动辄几百万都是正常的。这不是模型烂,是指标的天然尺度。

遇到这种问题,我会做两件事:一是同时打印 MAE,让它作为人类可读的业务指标;二是观察 R²,判断相对水平。比如 MSE 大但 R² = 0.8,说明虽然绝对误差大,但模型相对均值基准已经好很多。还可以对目标变量做标准化或对数变换后再建模,能缓解 MSE 尺度过大的干扰,也能让树的分裂更平稳。

写在最后的一点实操经验

这轮用 sklearn 接口做决策树回归,最大的体会是:模型本身很简单,复杂的是评估体系和防过拟合的取舍。决策树回归的代码量很小,但要让它在真实场景里稳定可用,功夫都在数据准备、参数约束和指标解读上。

如果你手头也有回归需求,建议按这个套路走:先切好训练集测试集,用默认参数的树跑一次,打印 MSE、MAE、R²;接着画残差图,看模型在哪一段误差最大;再按深度、叶子样本数、剪枝的顺序逐步收窄模型复杂度。整个过程不需要懂太多数学,但每一步都要有判断依据,不要靠蒙。

另外一个给自己的提醒:单棵决策树通常只是回归任务的起点,不是终点。真正上线时,我一般会再往随机森林或者梯度提升树扩展,但决策树的价值在于,它能帮你快速摸清数据规律和特征信号。把这个案例 2 跑透,后面再上集成模型,就不会觉得跨度大了。

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

国庆七天AI技术演进实录:模型量化、推理优化与云计费变革

1. 项目概述:一场没有硝烟的七日技术演进实录“国庆七天,AI圈没一天消停”——这句话不是夸张修辞,而是过去七年中每一年国庆假期的真实切片。我从2017年开始跟踪大模型与生成式AI的技术脉络,每年十月一号到七号,都会把…

作者头像 李华
网站建设 2026/10/14 6:03:04

新能源电池Pack线PLC程序全解析:S7-1500架构到调试实战

第一次打开一套完整的新能源电池pack线程序时,说实话我是有点头皮发麻的。几千个DB块、上百个FB/FC、密密麻麻的OB,随便点开一个程序段就是七八页的梯形图,根本不知道从哪看起。那会儿我连“S1500”这个叫法都还没搞清楚——后来才明白&#…

作者头像 李华
网站建设 2026/10/14 6:02:54

汽车零部件设计仿真与优化:从离合器到活塞连杆组实战指南

汽车零部件设计仿真与优化实现指南:从离合器到活塞连杆组在汽车行业摸爬滚打这些年,我最大的感触就是:零部件设计早就过了“画图-试制-试验-改图”这种纯靠经验和烧钱堆出来的阶段。如今任何一个成熟的零部件开发项目,都离不开设计…

作者头像 李华
网站建设 2026/10/14 6:02:39

C++教学闭环:结构化知识资产与本地可执行学习路径

简介:本资源是面向C初学者及期末备考学生的系统性学习套件,聚焦清华大学郑莉教授《C程序设计》课程核心内容,助力夯实语法基础、掌握面向对象编程、STL应用与异常处理等关键能力。压缩包共46个文件,含24个CPP源码(覆盖…

作者头像 李华
网站建设 2026/10/14 6:02:05

源荷随机特征下热电联供微网优化调度与Matlab实现

项目标题里最核心的四个词,“源荷随机特征”“热电联供微网”“优化”“Matlab实现”,分开看都不算冷门,但组合在一起就是典型的综合能源系统方向的研究课题了。这两年做微电网优化的朋友应该都能感觉到,纯电微网的套路已经非常成…

作者头像 李华
网站建设 2026/10/14 6:02:02

肉毒素注射和磁波刀治疗头部震颤,哪种方式更合适?

家庭聚会拍合影时,其他人都在看镜头,只有一位老人的头在轻轻晃动,照片拍出来总是模糊的。旁边的人问了一句“是不是紧张”,老人笑了笑,没有解释,只是下意识地把头低下去,之后越来越少参加需要拍…

作者头像 李华