随机森林特征重要性(Variable importance)评估方法

文章介绍了随机森林中MDA(MeanDecreaseAccuracy)方法来评估特征重要性,分别详细阐述了分类和回归场景下的计算公式,并提供了C++实现代码。通过实验验证了这种方法在waveform、superconductivity、power-consumption和mnist数据集上的效果,以及与sklearn库的比较,证明了实现的准确性。
摘要由CSDN通过智能技术生成

项目主页:randomforest github, gitee
C++ implementation of random forests classification, regression, proximity and variable importance.
推荐阅读:Random Forests C++实现:细节,使用与实验

算法介绍

  特征重要性评估(Variable importance measure, or Feature importance evaluation)用来计算样本特征的重要性,定量地描述特征对分类或者回归的贡献程度。随机森林可以用来对特征重要性进行评估,从另一个角度来说,特征重要新评估是随机森林的一种自带工具,主要分为两种方法:(1)Mean Decrease Impurity(MDI),通过统计节点分裂时不纯度的下降数值来衡量某个节点的重要性;(2)Mean Decrease Accuracy(MDA), 方法是随机交换袋外数据集 (permuted oob data) 中某个特征的值,然后重新进行预测,通过衡量分类/回归的准确度下降的程度来计算该特征的重要性。这两种方法的思想都是Leo Breiman提出的。
  MDI使用的是训练集数据,在RF训练完成后就能直接获取MDI特征重要性评估值;MDA使用的是oob数据,需要在RF训练完成后独立运行特征重要性评估的程序。由于MDI完全使用训练数据,它可能对评估的准确性造成影响,而且更加倾向于加大high cardinality features的权重。相比之下,MDA的结果更加准确。MDI与MDA的对比可以查看sklearn给出的实验——Permutation Importance vs Random Forest Feature Importance (MDI)

Mean Decrease Accuracy:
The MDA of X(j) is obtained by averaging the difference in out-of-bag error estimation before and after the permutation over all trees.
—— Biau, G., Scornet, E. A random forest guided tour. TEST 25, 197–227 (2016).

本文讨论和实现MDA方法,特征 j j j(样本第 j j j维)的重要性计算方法如下:
(1)分类:

M D A c ( j ) = 1 T ∑ t T [ 1 ∣ D t ∣ ( ∑ X i ∈ D t I ( P ( X i ) = = y i ) − ∑ X i j ∈ D t j I ( P ( X i j ) = = y i ) ) ] MDA_c(j)=\frac {1}{T} \sum_t^T\left [ \frac{1}{|D_t|} \left( \sum_{X_i\in D_t} I(P(X_i)==y_i)- \sum_{X_i^j\in D_t^j} I(P(X_i^j)==y_i) \right) \right] MDAc(j)=T1tT Dt1 XiDtI(P(Xi)==yi)XijDtjI(P(Xij)==yi)

(2)回归:

M D A r ( j ) = 1 T ∑ t T [ 1 ∣ D t ∣ ( ∑ X i j ∈ D t j ∑ k ( R k ( X i j ) − y i k ) 2 − ∑ X i ∈ D t ∑ k ( R k ( X i ) − y i k ) 2 ) ] MDA_r(j)=\frac {1}{T} \sum_t^T\left [ \frac{1}{|D_t|} \left( \sum_{X_i^j\in D_t^j} \sum _k \left(R_k(X_i^j)-y_i^k\right)^2- \sum_{X_i\in D_t} \sum_k \left(R_k(X_i)-y_i^k\right)^2 \right) \right] MDAr(j)=T1tT Dt1 XijDtjk(Rk(Xij)yik)2XiDtk(Rk(Xi)yik)2

以上公式参考自Gérard Biau的经典论文"A Random Forest Guided Tour",解释下公式中的符号:

  1. T T T: RF中随机树的数量
  2. ( X i , y i ) (X_i,y_i) (Xi,yi): 样本,对于分类 y y y为类别,对于回归为输出(可以是多维)
  3. X i j X_i^j Xij: X i X_i Xi的第 j j j维(特征)随机交换后的样本
  4. D t D_t Dt: 随机树 t t t的袋外(oob)样本集, D t j D_t^j Dtj: 第 j j j维交换后形成的样本集
  5. P ( X i ) P(X_i) P(Xi): 样本 X i X_i Xi的预测结果(类别); R ( X i ) R(X_i) R(Xi):样本 X i X_i Xi的预测输出(回归)
  6. I ( P X i = = y i ) I(P_{X_i}==y_i) I(PXi==yi): 指示函数,如果预测结果与真实类别相同,则返回1,否则返回0。
  7. y i k y_i^k yik: 考虑到多目标回归,表示第 k k k维输出

这个公式反映了,对样本第 j j j维(特征)上的数据进行随机交换后,模型预测(分类)准确度或者(回归)精度下降的程度,使用的是每棵树的袋外数据。如果在随机交换后,准确度下降较多,则认为这个特征比较重要。
上面公式中 M D A c ( j ) 、 M D A r ( j ) MDA_c(j)、MDA_r(j) MDAc(j)MDAr(j)被称为raw score。对于 M D A c ( j ) MDA_c(j) MDAc(j),在数值上它反映的是当特征 j j j 被随机交换后,袋外样本的平均分类正确率的下降值;对于 M D A r ( j ) MDA_r(j) MDAr(j),在数值上它反映的是当特征 j j j 被随机交换后,袋外样本均方误差(MSE)的上升数值。

In every tree grown in the forest, put down the oob cases and count the number of votes cast for the correct class. Now randomly permute the values of variable m in the oob cases and put these cases down the tree. Subtract the number of votes for the correct class in the variable-m-permuted oob data from the number of votes for the correct class in the untouched oob data. The average of this number over all trees in the forest is the raw importance score for variable m. by Leo Breiman

实现

算法流程

随机森林MDA算法的流程如下(以分类为例):

训练随机森林模型(树数量 T T T),训练过程记录每棵树的袋外数据集 D t D_t Dt
对于每棵树 t = 1...... T t=1......T t=1......T
|  计算 D t D_t Dt上的分类正确的样本数 c 0 c_0 c0
|     ∑ D t I ( P ( X i ) = = y i ) \sum_{D_t} I(P(X_i)==y_i) DtI(P(Xi)==yi)
|  对于每个特征 j = 1...... M j=1......M j=1......M
|  |  对 D t D_t Dt样本的特征 j j j进行随机交换得到 D t j D_t^j Dtj
|  |  计算 D t j D_t^j Dtj上的分类正确的样本数 c 1 c_1 c1:
|  |     ∑ D t j I ( P ( X i j ) = = y i ) \sum_{D_t^j} I(P(X_i^j)==y_i) DtjI(P(Xij)==yi)
|  |  计算: m d a ( t , j ) = 1 ∣ D t ∣ ( c 0 − c 1 ) mda(t,j)=\frac{1}{|D_t|}(c_0-c_1) mda(t,j)=Dt1(c0c1)
|  end of j j j
end of t t t
对于每个特征 j = 1...... M j=1......M j=1......M
|  计算特征 j j j的重要性raw socre:
|     M D A c ( j ) = 1 ∣ T ∣ ∑ t m d a ( t , j ) MDA_c(j)=\frac{1}{|T|}\sum_t mda(t,j) MDAc(j)=T1tmda(t,j)
end of j j j

算法的时间复杂度为 O ( M ∗ N ∗ T ) O(M*N*T) O(MNT) M , N , T M,N,T M,N,T分别是特征数、样本数、随机树数量。运行MDA特征重要性评估的耗时要远远大于随机森林训练的耗时。MDA特征重要性评估的C++实现如下:

分类

int RawVariableImportanceScore(float** data, int* label, LoquatCForest* loquatForest, int nType, float* varImportance, bool bNormalize, char* filename)
{
	int var, tr, i, j, oobnum, index;
	const int variables_num = loquatForest->RFinfo.datainfo.variables_num;
	const int Ntrees = loquatForest->RFinfo.ntrees;
	struct LoquatCTreeStruct* pTree = NULL;
	int* pIndex = NULL, predicted_class_index;
	float confidence;
	int correct_num = 0, correct_num_premute = 0;
	float* tmp_data = new float[variables_num];

	int** DeltMatrix = new int* [Ntrees];
	for (i = 0; i < Ntrees; i++)
	{
		DeltMatrix[i] = new int[variables_num];
		memset(DeltMatrix[i], 0, sizeof(int) * variables_num);
	}

	float* mean_var = new float[variables_num];
	memset(mean_var, 0, sizeof(float) * variables_num);
	float* std2_var = new float[variables_num];
	memset(std2_var, 0, sizeof(float) * variables_num);	
	int *oobOfTrees = new int[Ntrees];
	
	for (tr = 0; tr < Ntrees; tr++)
	{
		pTree = loquatForest->loquatTrees[tr];
		pIndex = pTree->outofbag_samples_index;
		oobnum = pTree->outofbag_samples_num;
		oobOfTrees[tr] = oobnum;
		int* permuted_order = new int[oobnum];
		for (correct_num = 0, i = 0; i < oobnum; i++)
		{
			index = pIndex[i];
			PredictAnTestSampleOnOneTree(data[index], variables_num, pTree, predicted_class_index, confidence);
			if (predicted_class_index == label[index])
				correct_num++;
		}

		for (var = 0; var < variables_num; var++)
		{
			// permuting (var)th variables
			// permute [0, oobnum-1]
			permute(oobnum, permuted_order);

			for (correct_num_premute = 0, i = 0; i < oobnum; i++)
			{
				index = pIndex[permuted_order[i]];
				memcpy(tmp_data, data[pIndex[i]], variables_num * sizeof(float));
				tmp_data[var] = data[index][var];
				PredictAnTestSampleOnOneTree(tmp_data, variables_num, pTree, predicted_class_index, confidence);
				if (predicted_class_index == label[pIndex[i]])
					correct_num_premute++;
			}

			DeltMatrix[tr][var] = correct_num - correct_num_premute;
			mean_var[var] += (correct_num - correct_num_premute)/(float)oobOfTrees[tr];
		}

		delete[]permuted_order;
	}

	for (i = 0; i < variables_num; i++)
		mean_var[i] = mean_var[i] / Ntrees;

	for (i = 0; i < variables_num; i++)
	{
		for (j = 0; j < Ntrees; j++)
			std2_var[i] += (DeltMatrix[j][i]/(float)oobOfTrees[j] - mean_var[i]) * (DeltMatrix[j][i]/(float)oobOfTrees[j] - mean_var[i]);
		std2_var[i] /= Ntrees;
	}

	float* raw_score = new float[variables_num];
	float* z_score = new float[variables_num];
	memset(raw_score, 0, sizeof(float) * variables_num);
	memset(z_score, 0, sizeof(float) * variables_num);

	float fsum = 0.f;
	// raw score
	for (i = 0; i < variables_num; i++)
	{
		raw_score[i] = mean_var[i];
		fsum += raw_score[i];
	}

	// Normalization
	if (bNormalize) {
		for (i = 0; i < variables_num; i++)
			raw_score[i] /= fsum;
	}

	// z-score
	fsum = 0.f;
	for (i = 0; i < variables_num; i++)
	{
		z_score[i] = mean_var[i] / (sqrtf(std2_var[i]) + FLT_EPSILON); //0530
		fsum += z_score[i];
	}

	// Normalization
	if (bNormalize) {
		for (i = 0; i < variables_num; i++)
			z_score[i] /= fsum;
	}

	if (nType == 0) // raw_score
		memcpy(varImportance, raw_score, sizeof(float) * variables_num);
	else  // z-score
		memcpy(varImportance, z_score, sizeof(float) * variables_num);

	if (filename != NULL)
	{
		fstream vieFile;
		vieFile.open(filename, ios_base::out);
		if (vieFile.is_open())
		{
			vieFile << "variable index" << "\t" << "raw score" << "\t\t" << "z-score" << endl;
			for (i = 0; i < variables_num; i++)
				vieFile << i << "\t\t" << raw_score[i] << "\t\t" << z_score[i] << endl;
			vieFile.close();
		}
	}

	delete[] tmp_data;

	for (i = 0; i < Ntrees; i++)
		delete[] DeltMatrix[i];
	delete[] DeltMatrix;
	delete[] mean_var;
	delete[] std2_var;
	delete[] oobOfTrees;
	delete[] raw_score;
	delete[] z_score;

	return 0;
}

回归

回归森林中的实现比分类森林中复杂一些,主要是要兼顾多目标回归的情况。

int RawVariableImportanceScore(float** data, float* target, LoquatRForest* loquatForest, int nType, float* varImportance, bool bNormalize, char* filename)
{
	int var, tr, i, j, oobnum, index;
	const int variables_num = loquatForest->RFinfo.datainfo.variables_num_x;
	const int target_num = loquatForest->RFinfo.datainfo.variables_num_y;
	const int Ntrees = loquatForest->RFinfo.ntrees;
	int* pIndex = NULL;
	float confidence,  predicted=0.f;
	float *mse = new float[target_num];
	float *mse_premute = new float[target_num];
	float *target_predicted = NULL;
	float* tmp_data = new float[variables_num];

	float** DeltMatrix = new float * [Ntrees];
	for (i = 0; i < Ntrees; i++)
	{
		DeltMatrix[i] = new float[variables_num];
		memset(DeltMatrix[i], 0, sizeof(float) * variables_num);
	}

	float* mean_var = new float[variables_num];
	memset(mean_var, 0, sizeof(float) * variables_num);
	float* std2_var = new float[variables_num];
	memset(std2_var, 0, sizeof(float) * variables_num);	
	int *oobOfTrees = new int[Ntrees];
	
	for (tr = 0; tr < Ntrees; tr++)
	{
		pIndex = loquatForest->loquatTrees[tr]->outofbag_samples_index;
		oobnum = loquatForest->loquatTrees[tr]->outofbag_samples_num;
		oobOfTrees[tr] = oobnum;
		int* permuted_order = new int[oobnum];
		memset(mse, 0, sizeof(float)*target_num);
		for (i = 0; i < oobnum; i++)
		{
			index = pIndex[i];
			target_predicted = GetArrivedLeafNode(loquatForest, tr,  data[index])->pLeafNodeInfo->MeanOfArrived;
			
			for(int k=0; k<target_num; k++)
			{
				predicted = loquatForest->bTargetNormalize == false ? target_predicted[k] : (target_predicted[k] - loquatForest->offset[k])/ loquatForest->scale[k];
				mse[k] += (target[index*target_num+k]-predicted) * (target[index*target_num+k]-predicted);
			}	
		}

		for (var = 0; var < variables_num; var++)
		{
			// permuting (var)th variables
			// permute [0, oobnum-1]
			permute(oobnum, permuted_order);
			memset(mse_premute, 0, sizeof(float)*target_num);
			for (i = 0; i < oobnum; i++)
			{
				index = pIndex[permuted_order[i]];
				memcpy(tmp_data, data[pIndex[i]], variables_num * sizeof(float));
				tmp_data[var] = data[index][var];
				target_predicted = GetArrivedLeafNode(loquatForest, tr,  tmp_data)->pLeafNodeInfo->MeanOfArrived;

				for(int k=0; k<target_num; k++)
				{
					predicted = loquatForest->bTargetNormalize == false ? target_predicted[k]  : (target_predicted[k] -loquatForest->offset[k])/loquatForest->scale[k];
					mse_premute[k] +=(target[pIndex[i]*target_num+k] - predicted)*(target[pIndex[i]*target_num+k] - predicted);
				}
		
			}

			for (int k=0; k<target_num; k++)
				DeltMatrix[tr][var] += (mse_premute[k] - mse[k]);
			mean_var[var] += DeltMatrix[tr][var]/(float)oobOfTrees[tr];
		}

		delete[]permuted_order;
	}

	for (i = 0; i < variables_num; i++)
		mean_var[i] = mean_var[i] / Ntrees;

	for (i = 0; i < variables_num; i++)
	{
		for (j = 0; j < Ntrees; j++)
			std2_var[i] += (DeltMatrix[j][i]/(float)oobOfTrees[j] - mean_var[i]) * (DeltMatrix[j][i]/(float)oobOfTrees[j] - mean_var[i]);
		std2_var[i] /= Ntrees;
	}

	float* raw_score = new float[variables_num];
	float* z_score = new float[variables_num];
	memset(raw_score, 0, sizeof(float) * variables_num);
	memset(z_score, 0, sizeof(float) * variables_num);

	float fsum = 0.f;
	// raw score
	for (i = 0; i < variables_num; i++)
	{
		raw_score[i] = mean_var[i];
		fsum += raw_score[i];
	}

	// Normalization
	if (bNormalize) {
		for (i = 0; i < variables_num; i++)
			raw_score[i] /= fsum;
	}

	// z-score
	fsum = 0.f;
	for (i = 0; i < variables_num; i++)
	{
		z_score[i] = mean_var[i] / (sqrtf(std2_var[i]) + FLT_EPSILON); //0530
		fsum += z_score[i];
	}

	// Normalization
	if (bNormalize) {
		for (i = 0; i < variables_num; i++)
			z_score[i] /= fsum;
	}

	if (nType == 0) // raw_score
		memcpy(varImportance, raw_score, sizeof(float) * variables_num);
	else  // z-score
		memcpy(varImportance, z_score, sizeof(float) * variables_num);

	if (filename != NULL)
	{
		fstream vieFile;
		vieFile.open(filename, ios_base::out);
		if (vieFile.is_open())
		{
			vieFile << "variable index" << "\t" << "raw score" << "\t\t" << "z-score" << endl;
			for (i = 0; i < variables_num; i++)
				vieFile << i << "\t\t" << raw_score[i] << "\t\t" << z_score[i] << endl;
			vieFile.close();
		}
	}

	delete[] tmp_data;
	delete[] mse;
	delete[] mse_premute;
	for (i = 0; i < Ntrees; i++)
	{
		delete[] DeltMatrix[i];
		DeltMatrix[i] = NULL;
	}
	delete[] DeltMatrix;
	delete[] mean_var;
	delete[] std2_var;
	delete[] oobOfTrees;
	delete[] raw_score;
	delete[] z_score;

	return 1;
}

上述代码随机随机森林实现的一部分,建议到项目主页randomforest上查看。

实验

  本节通过实验展示了随机森林特征重要性评估的效果,选取了分类和回归中典型的数据集,特征数量为11、40、81、780、4096。并根据数据集的特点给出可视化结果,以此验证本项目实现的准确性。

实验1:waveform数据集(分类)

  使用waveform数据集,训练样本数5000,特征40维,类别数3。RF参数为随机树200棵,分类候选特征数 40 \sqrt{40} 40 。运行5次得到以下特征重要性的柱状图,显示每个特征重要性的平均值及标准差。这个数据是经典的用于特征重要性评估验证的数据集,它的后19维特征是随机噪声,计算结果符合上述情况。
在这里插入图片描述

实验2:superconductivity数据集(回归)

  数据集superconductivity的特征81维,训练样本数21263,采集和加工自超导体临界温度的实验。随机回归森林的参数为:随机树 T = 200 T=200 T=200,分类候选特征数 81 3 \frac{81}{3} 381,节点最小样本数5,最大树深度40,都是默认参数。运行5次得到以下特征重要性的柱状图,显示每个特征重要性的平均值及标准差。

在这里插入图片描述

实验3:power-consumption数据集(回归)

  使用Tetuan-City-power-consumption数据集来进行试验,原始数据集是通过时间、温度、湿度、风速等6个变量来预测城市3个配电网的能源消耗,即输入6维,输出3维。由于“时间”变量难以使用,所以分解为[minute,hour,day,month,weekday,weekofyear] 6个变量,加上原始的5个气象变量 (temperature, light, sound, CO2 and digital passive infrared (PIR)),形成新的11维输入。随机回归森林的参数为:随机树 T = 200 T=200 T=200,分类候选特征数 4 = 11 3 4=\frac{11}{3} 4=311,节点最小样本数5,最大树深度40,都是默认参数。可以看到hour,month,weekofyear,气温这四个特征(变量)的重要性排名前列,符合常理。
在这里插入图片描述

实验4:mnist数据集(可视化实验)

  使用mnist手写字符识别数据集训练随机森林分类器,样本数60000,特征数780。RF参数为:随机树 T = 200 T=200 T=200,分类候选特征数 780 \sqrt{780} 780 ,节点最小样本数5,最大树深度40。将特征重要性的输出值转换为相同分辨率的图像(28x28,780+4,在最后增补了4个0),如下图。可以看到重要特征集中在图像中部,周边的特征重要性非常低,这也符合手写字符训练图像周边像素大多为0的情况。

在这里插入图片描述

实验5:与sklearn的验证

  Scikit-learn中实现了随机森林MDI方法,在训练结束后可以获取特征重要性的权重值。官网文章“Pixel importances with a parallel forest of trees”通过olivetti_faces人脸识别数据集对像素级特征的重要性进行了可视化。
  将本项目输出的olivetti_faces人脸像素特征重要性与sklearn结果进行对比,以验证本项目实现的随机森林在特征重要性评估中的有效性。使用olivetti_faces人脸识别数据集进行验证,选取样本数50(选取了前5类样本,与sklearn官网的实验保持一致),特征数4096(64x64图像),每个样本是一张人脸图像。RF参数为:随机树 T = 750 T=750 T=750,分类候选特征数 4096 \sqrt{4096} 4096 ,节点最小样本数1,最大树深度40。这个实验是为了与sklearn的的结果进行比较,实验样本完全相同,参数尽量接近。从可视化结果看,本项目的输出(上图)与sklearn的MDI类似,这验证了本项目实现的特征重要性算法的正确性。
本项目MDA

本项目MDA特征重要新

sklearn-MDI

sklearn的MDI特征重要性

附录

使用我实现的randomforest算法进行RF训练+特征重要性评估的代码如下(以分类森林为例):

int main()
{
	// read training samples if necessary
	char filename[500] = "/to/direction/dataset/train-data.txt" 
	float** data = NULL;
	int* label = NULL;
	Dataset_info_C datainfo;
	int rv = InitalClassificationDataMatrixFormFile2(filename, data/*OUT*/, label/*OUT*/, datainfo/*OUT*/);
	// check the return value
	// 	... ...

	// setting random forests parameters
	RandomCForests_info rfinfo;
	rfinfo.datainfo = datainfo;
	rfinfo.maxdepth = 40;
	rfinfo.ntrees = 200;
	rfinfo.mvariables = (int)sqrtf(datainfo.variables_num);
	rfinfo.minsamplessplit = 5;
	rfinfo.randomness = 1;
	// train forest
	LoquatCForest* loquatCForest = NULL;
	rv = TrainRandomForestClassifier(data, label, rfinfo, loquatCForest /*OUT*/, 20);
	// check the return value
	// 	... ...

	// variable importance measure
	float *vim = new float [datainfo.variables_num];
	RawVariableImportanceScore2(data, label, loquatCForest, 0/*raw score*/, vim, true, "file_to_store.txt");
	delete [] vim;
	
	// clear the memory allocated for the entire forest
	ReleaseClassificationForest(&loquatCForest);
	// release money: data, label
	for (int i = 0; i < datainfo.samples_num; i++)
		delete[] data[i];
	delete[] data;
	delete[] label;
	return 0;
}
  • 12
    点赞
  • 85
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
随机森林评估特征重要性方法有多种。一种常用的方法是通过计算特征的平均-减少-不纯重要性来衡量。这种方法通过测量特征在减少不确定性(分类器)或方差(回归器)方面的有效性来计算特征重要性。然而,这种方法并不总是准确反映特征重要性,特别是在特征的度量尺度或类别数量有所不同的情况下。 另一种更可靠的方法是使用互换重要性法。该方法通过将验证集或袋外(OOB)样本导入随机森林,并记录基线准确性(分类器)或 R2 得分(回归器)。然后,对于每个特征,对单一预测特征的列值进行修正,并重新计算准确率或者 R2。特征重要性可以通过基线和因排列组合而造成的总体准确率或 R2 下降之间的差异来衡量。 虽然互换重要性法的计算成本较高,但结果更可靠。它能够更准确地评估特征重要性。 在构建随机森林模型时,可以使用随机森林库中提供的函数来评估特征重要性。例如,在Python中,可以使用scikit-learn库中的RandomForestClassifier类或RandomForestRegressor类来构建随机森林模型,并使用其中的feature_importances_属性来获取特征重要性。可以使用permutation_importance()函数来计算互换重要性。另外,可以使用plot_importances()函数来绘制特征重要性的热图。 综上所述,随机森林评估特征重要性方法有多种,包括平均-减少-不纯重要性和互换重要性。互换重要性法更可靠且准确,但计算成本较高。在实际应用中,可以使用相应的函数和方法来计算和可视化特征重要性。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [机器学习笔记 十五:随机森林Random Forest评估机器学习模型的特征重要性](https://blog.csdn.net/amyniez/article/details/127618854)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值