深度学习大揭秘:轻松解决多分类难题的绝佳方法(代码篇)

510 阅读7分钟

前言

本文主要是对深度学习大揭秘:轻松解决多分类难题的绝佳方法这篇文章的内容补充。

按照下图所示流程,封装成几个函数,仅供参考

训练AI流程图.png

示例

本文用到的库主要有:

  • numpy 1.24.3
  • pandas 1.5.3
  • scikit-learn 1.2.2
  • keras 2.9.0
  • tensorflow-gpu 2.9.0

Python环境是:3.10

数据获取

def get_data():  
"""  
从指定的CSV文件中读取数据,并将其转换为DataFrame对象后返回。

参数:
无

返回:
data:DataFrame对象,包含CSV文件中的数据
"""  
data = pd.read_csv('data.csv', header=0)  
return data  
  

数据预处理

def preprocess_data(data):
"""
对数据进行处理和预处理操作。

参数:
data:DataFrame对象,待处理的数据

返回:
x:numpy array,处理后的特征数据
y:numpy array,处理后的标签数据
label_encoder:LabelEncoder对象,标签的编码器
"""

# 清洗数据(处理缺失值和异常值)  
data = data.dropna() # 删除缺失值  
  
features = data.iloc[:, :-1] # 前面所有列作为特征  
labels = data.iloc[:, -1] # 最后一列作为标签  
  
# 特征工程--特征选择、特征提取(在下文)
# selected_features = feature_selection(features, labels)  
# extracted_scaled_features = feature_extraction(selected_features)  
  
# 标准化数据(使数据在同一尺度上)  
scaler = MinMaxScaler(feature_range=(-1, 1)) # 特征缩放  
scaled_features = scaler.fit_transform(features)  
  
# One-Hot编码标签  
label_encoder = LabelEncoder()  
labels_encoded = label_encoder.fit_transform(labels)  
labels_one_hot = to_categorical(labels_encoded, num_classes=3)  
  
# 构建特征和标签  
#通过滑动窗口将原始的时间序列数据划分为一组固定长度的子序列,然后将每个子序列作为模型的输入(x),对应的标签作为模型的输出(y)。
sequence_length = 64 # 滑动窗口的大小  
x = []  
y = []  
for i in range(len(scaled_features) - sequence_length + 1):  
x.append(scaled_features[i: i + sequence_length])  
y.append(labels_one_hot[i + sequence_length - 1])  
  
x = np.array(x)  
y = np.array(y)  
return x, y, label_encoder  

上述代码中,使用滑动窗口的目的是为了将时间序列数据划分为固定长度的子序列。这样做的好处是可以捕捉到时间序列中的局部模式和趋势,从而更好地训练模型。

滑动窗口的大小(sequence_length)决定了每个子序列的长度,可以根据具体的问题和数据特点进行调整。较长的窗口可以捕捉到更长期的模式和趋势,但可能会导致模型训练和预测的复杂性增加;较短的窗口可以更快地捕捉到短期的模式和趋势,但可能会丢失一些长期的信息。需要根据具体问题和数据权衡滑动窗口大小。

特征工程(选择性使用)

# 特征选择  
def feature_selection(features, labels):  
"""
对特征进行选择,选择与目标变量最相关的特征。

参数:
features:numpy array,特征数据
labels:numpy array,目标变量

返回:
selected_features:numpy array,选择后的特征数据
"""
# 使用评分函数(互信息、方差分析)来度量每个特征与目标变量之间的相关性  
selector = SelectKBest(score_func=mutual_info_classif, k=64) # 选择与目标变量最相关的64个特征  
selected_features = selector.fit_transform(features, labels)  
  
selector = SelectKBest(score_func=f_classif, k=32) # 选择与目标变量最相关的32个特征  
selected_features = selector.fit_transform(selected_features, labels)  

return selected_features  
  
  
# 特征提取  
def feature_extraction(features):  
"""  
使用非线性特征提取方法(核主成分分析)提取特征  
  
参数:  
features: 特征矩阵,形状为 (n_samples, n_features)  
n_components: 提取的主成分数量  
  
返回:  
extracted_features: 提取后的特征矩阵,形状为 (n_samples, n_components)  
"""  
  
# 标准化数据(使数据在同一尺度上)  
scaler = MinMaxScaler(feature_range=(-1, 1)) # 特征缩放  
scaled_features = scaler.fit_transform(features)  

# 使用核主成分分析进行特征提取  
kpca = KernelPCA(n_components=60, kernel='rbf') # 使用高斯核函数(RBF核)  
extracted_features = kpca.fit_transform(scaled_features)  

  
return extracted_features  
  

划分数据集

此处直接使用scikit-learn库的train_test_split函数就行,无需自己封装,后续会放到到main函数中调用

# 划分训练集和测试集,其中80%用作训练集,20%用作测试集
x_train, x_test, y_train, y_test = train_test_split(x, y, train_size=0.8, test_size=0.2, random_state=42)

构建模型

通过构建LSTM和Transformer两个模型举例说明如何解决时间序列的三分类问题

LSTM (RNN的一种)

采用Sequential模型类,构建线性堆叠的神经网络模型。

def build_lstm_model(input_shape, num_classes):  
"""  
构建一个基于LSTM的分类模型。 
  
参数:  
- input_shape: 输入数据的形状
- num_classes: 分类的类别数量,这里是3  
  
返回:  
- model: 构建好的Keras模型  
"""  
model = Sequential()  
model.add(LSTM(units=128, return_sequences=True, input_shape=input_shape))  
model.add(Dropout(0.3))  
model.add(BatchNormalization())  
model.add(LeakyReLU(0.01))  
  
model.add(LSTM(units=128, return_sequences=True, kernel_regularizer=regularizers.l2(l2=0.01)))  
model.add(Dropout(0.3))  
model.add(BatchNormalization())  
model.add(LeakyReLU(0.01))  
  
model.add(LSTM(units=512, kernel_regularizer=regularizers.l2(l2=0.01)))  
model.add(Dropout(0.4))  
model.add(BatchNormalization())  
model.add(LeakyReLU(0.01))  
  
model.add(Dense(units=num_classes, activation='softmax')) # 使用softmax函数进行三分类预测  
  
# optimizer = RMSprop(learning_rate=0.005, centered=True)  
optimizer = Adam(learning_rate=0.003, amsgrad=True)  
model.compile(optimizer=optimizer, loss='categorical_crossentropy', metrics=['accuracy'])  
return model  

Transformer

完整的Transformer模型有很多关键的层,此处仅引入了注意力机制。

def build_transformer_model(input_shape, num_classes):  
"""  
构建一个基于Transformer的三分类模型。  
  
参数:  
- input_shape: 输入数据的形状
- num_classes: 分类的类别数量,这里是3  
  
返回:  
- model: 构建好的Keras模型  
"""  
# 定义输入层
inputs = Input(shape=input_shape)  
  
# 添加卷积层,用于提取局部特征,,输出通道数64,核大小3  
conv_out = Conv1D(64, 3, activation='relu', padding='same')(inputs)  
conv_out = BatchNormalization()(conv_out)  
conv_out = Dropout(0.2)(conv_out)  

# 添加transformer层,引入多头注意力机制,头数设为3,键向量维度为64
attention_out = MultiHeadAttention(num_heads=3, key_dim=64)(conv_out, conv_out, conv_out)  
attention_out = LayerNormalization(epsilon=1e-6)(attention_out)  
attention_out = Dropout(0.2)(attention_out)  
  
# 将Transformer层的输出展平
flatten_out = Flatten()(attention_out)  
  
dense_out = Dense(256, activation='relu', kernel_regularizer=regularizers.l1(l1=0.01))(flatten_out)  
dense_out = BatchNormalization()(dense_out)  
dense_out = Dropout(0.4)(dense_out)  
  
# 添加输出层 
outputs = Dense(num_classes, activation='softmax')(dense_out)  

# 构建模型  
model = Model(inputs=inputs, outputs=outputs)  
  
optimizer = RMSprop(learning_rate=0.005, centered=True)  
# optimizer = Adam(learning_rate=0.005, amsgrad=True)  
  
model.compile(optimizer=optimizer, loss='categorical_crossentropy', metrics=['accuracy'])  
# 返回构建好的模型  
return model  

训练模型

def train_model(model, x_train, y_train, class_weights, epochs=200, batch_size=64): 
""" 
训练模型并返回训练好的模型。

参数:
- model: 要训练的模型对象。
- x_train: 训练数据集的特征。
- y_train: 训练数据集的标签。
- class_weights: 类别权重,用于处理类别不平衡问题。
- epochs: 训练的轮数,默认为200。
- batch_size: 每个批次的样本数,默认为64。

返回:
训练好的模型对象。
"""
# 学习率衰减
reduce_lr = ReduceLROnPlateau(monitor='val_loss', patience=30, min_lr=0.0001)
# 早停
early_stopping = EarlyStopping(monitor='val_accuracy', patience=30, restore_best_weights=True)

callbacks = [early_stopping, reduce_lr]  
model.fit(x_train, y_train, epochs=epochs, batch_size=batch_size, validation_split=0.2, callbacks=callbacks,  
class_weight=class_weights)  
  
return model  

模型验证与评估

def evaluate_model(model, x_test, y_test, label_encoder):  
"""
验证和评估模型。

参数:
- model: 待评估的模型对象。
- x_test: 测试数据集的特征。
- y_test: 测试数据集的标签。
- label_encoder: 标签编码器,用于反向编码标签。

"""
# 参数检查  
assert isinstance(x_test, np.ndarray), "X_test应为NumPy数组"  
assert isinstance(y_test, np.ndarray), "y_test应为NumPy数组"  
  
scores = model.evaluate(x_test, y_test)  
loss = scores[0] # 获取整体损失值  
accuracy = scores[1] # 获取整体准确率  
print(f"Loss: {loss}")  
print(f"Accuracy: {accuracy}")  
  
# 使用模型进行预测  
predictions = model.predict(x_test)  
  
# 反向编码标签  
pred_labels = label_encoder.inverse_transform(np.argmax(predictions, axis=1))  
y_test_labels = label_encoder.inverse_transform(np.argmax(y_test, axis=1))  
  
report = classification_report(y_test_labels, pred_labels)
print(report)  

如果想要获取classification_report中某一指标的值,可以参照:

report = classification_report(y_test_labels, pred_labels, output_dict=True ) 
  
macro_avg = report['macro avg']['recall']  
weighted_avg = report['weighted avg']['precision']  

模型保存

可以结合模型验证与评估classification_report中的指标,设置一定条件,满足条件后,调用此函数,保存模型的权重文件

# 保存模型的权重。  
def save_model_weights(model, filepath):
"""
保存模型的权重。  
  
参数:  
- model: 待保存权重的模型
- filepath: 权重文件的保存路径
"""
checkpoint = ModelCheckpoint(filepath=filepath, save_weights_only=True, save_best_only=True)  
model.save_weights(filepath, checkpoint)  

主函数调用


def main():  
# 获取数据  
data = get_data()  

# 数据处理和预处理
x, y, label_encoder = preprocess_data(data)

# 划分训练集和测试集
x_train, x_test, y_train, y_test = train_test_split(x, y, train_size=0.8, test_size=0.2, random_state=42)

x_train = np.array(x_train)
x_test = np.array(x_test)
y_train = np.array(y_train)
y_test = np.array(y_test)

# 构建模型
input_shape = (x_train.shape[1], x_train.shape[2])
num_classes = y_train.shape[1]

# 类别标签在 y 中
class_weights = compute_class_weights(y, y_train)

# 训练LSTM模型
lstm_model = build_lstm_model(input_shape, num_classes)  
lstm_model = train_model(lstm_model, x_train, y_train, class_weights=class_weights)  
evaluate_model(lstm_model, x_test, y_test, label_encoder)  
    
if __name__ == '__main__':  
main()

这里有一个容易出错的点:使用One-Hot编码标签时,传递的是label_encoder对象。

改进(选择性使用)

动态调整权重

前文代码已经调用了此函数,可用于处理类别不平衡问题(可能体现为classification_report中某一类的评价指标值全为0)。根据类别的频率或者重要性来设置,对于少数类别样本,可以给予更高的权重,以便模型更加关注这些样本;对于多数类别样本,可以给予较低的权重,以平衡样本分布。

def compute_class_weights(class_labels, y):  
"""  
计算类别权重。  
  
参数:  
- class_labels: numpy array,所有类别标签  
- y: numpy array,训练样本标签  
  
返回:  
class_weights: dict, 每个类别的权重。  
"""  
# 使用 'balanced' 方法计算类别权重  
class_weights = class_weight.compute_class_weight(class_weight='balanced',  
classes=np.unique(np.argmax(class_labels, axis=1)),  
y=np.argmax(y, axis=1)) # 根据 One-Hot 编码的标签获取原始类别标签  
return dict(enumerate(class_weights))  

动态权重损失函数

动态权重损失函数同样是一种处理样本不平衡问题的技术,通过调整样本的权重来平衡不同类别之间的影响,从而提高模型的性能。

class DynamicWeightedLoss(tf.keras.losses.Loss):
def __init__(self, reduction=tf.keras.losses.Reduction.AUTO, name='dynamic_weighted_loss', smoothing_factor=0.1):
super(DynamicWeightedLoss, self).__init__(reduction=reduction, name=name)
self.smoothing_factor = smoothing_factor
  
def call(self, y_true, y_pred):

# 将one-hot编码的标签转换为类别标签,并将数据类型转换为int32  
labels = tf.cast(tf.argmax(y_true, axis=-1), tf.int32)
  
# 计算每个类别的数量  
class_counts = tf.math.bincount(labels)
  
# 计算每个类别的权重,权重与类别的数量成反比,并使用平滑因子防止权重过大  
# 注意我们现在在这里将 class_counts 转换为浮点数  
class_weights = self.smoothing_factor / (tf.cast(class_counts, tf.float32) + 1e-7)
  
# 获取每个样本对应的权重  
weights = tf.gather(class_weights, labels)
  
# 计算损失  
loss = tf.nn.sparse_softmax_cross_entropy_with_logits(labels=labels, logits=y_pred)
  
# 计算加权损失  
weighted_loss = tf.reduce_mean(loss * weights)

return weighted_loss

如果使用上述损失函数,那么调用时可参考:

model.compile(optimizer=optimizer, loss=DynamicWeightedLoss(), metrics=['accuracy'])

小结

希望这篇文章可以帮助到有需要的人,欢迎各位朋友在评论区留言。