2025, Nov 20 03:01
Извлечение признаков из Keras CNN: почему Sequential падает и как помогает Functional API
Показываем, как извлекать признаки из слоя Dense в Keras CNN для аудио без ошибок: почему Sequential выдаёт ValueError и как решить задачу через Functional API.
Извлечение промежуточных признаков из сверточной сети Keras — привычный шаг в задачах аудио и речи, особенно когда нужен переиспользуемый вектор признаков с полносвязного слоя Dense. Однако попытка построить экстрактор признаков поверх Sequential-модели может завершиться ValueError, если вычислительный граф модели определён не полностью. Ниже — короткое объяснение проблемы и надёжный способ решения с помощью Functional API.
Постановка задачи
Сеть CNN обучается на последовательностях MFCC формы (500, 13) и содержит слой Dense(256), предназначенный для извлечения признаков:
from tensorflow.keras import models, layers
def make_trainable_convnet(input_shape, class_count):
net = models.Sequential()
net.add(layers.Input(shape=input_shape))
net.add(layers.Conv1D(64, 3, padding='same', activation='relu'))
net.add(layers.BatchNormalization())
net.add(layers.MaxPooling1D(pool_size=2))
net.add(layers.Dropout(0.05))
net.add(layers.Conv1D(128, 3, padding='same', activation='relu'))
net.add(layers.BatchNormalization())
net.add(layers.MaxPooling1D(pool_size=2))
net.add(layers.Dropout(0.05))
net.add(layers.Conv1D(128, 3, padding='same', activation='relu'))
net.add(layers.BatchNormalization())
net.add(layers.MaxPooling1D(pool_size=2))
net.add(layers.Dropout(0.05))
net.add(layers.Conv1D(128, 3, padding='same', activation='relu'))
net.add(layers.BatchNormalization())
net.add(layers.MaxPooling1D(pool_size=2))
net.add(layers.Dropout(0.05))
net.add(layers.Flatten())
net.add(layers.Dense(256, activation='relu'))
net.add(layers.Dropout(0.05))
net.add(layers.Dense(class_count, activation='softmax'))
return net
Данные загружаются как массивы NumPy с признаками по сегментам:
import numpy as np
mfcc_arr = np.load("/kaggle/working/mfcc_features.npy") # shape: (segments, 500, 13)
logmel_arr = np.load("/kaggle/working/logmel_features.npy") # shape: (segments, 500, 26)
Модель компилируется и обучается на MFCC:
from tensorflow.keras.optimizers import Adam
CLASS_TOTAL = 3
net_mfcc = make_trainable_convnet(input_shape=(500, 13), class_count=CLASS_TOTAL)
net_mfcc.compile(optimizer=Adam(1e-4),
loss='sparse_categorical_crossentropy',
metrics=['accuracy'])
net_mfcc.fit(mfcc_arr, labels, epochs=30, batch_size=32)
Извлечение признаков нацелено на слой Dense(256) (третий с конца):
from tensorflow.keras import models as kmodels
extractor = kmodels.Model(inputs=[net_mfcc.input], outputs=[net_mfcc.layers[-3].output])
extractor.save("cnn_feature_extractor_mfcc.h5")
Попытки заранее построить модель и выполнить фиктивный прямой проход не устраняют ошибку:
net_mfcc.build(input_shape=(None, 500, 13))
_ = net_mfcc(np.zeros((1, 500, 13)))
Почему возникает ошибка
ValueError появляется потому, что внутренний вычислительный граф, включая входные и выходные тензоры слоёв, не материализован достаточно полно, чтобы из промежуточных тензоров можно было собрать новый Model. Вызов модели на фиктивных данных всё ещё не гарантирует, что у графа, создаваемого через Model(...), будет конкретный, отслеживаемый вход. Когда Model создаётся из тензоров, TensorFlow требует чётко определённого входного тензора; иначе граф признаков просто не удаётся связать.
Решение: перейдите на Functional API и извлекайте по имени слоя
Functional API явно объявляет входы и детерминированно связывает граф уже на этапе конструирования модели. Это гарантирует наличие model.input и промежуточных тензоров, необходимых для сборки экстрактора признаков.
from tensorflow.keras import Model, Input
from tensorflow.keras.layers import Conv1D, BatchNormalization, MaxPooling1D, Dropout, Flatten, Dense
def craft_cnn_functional(in_shape, class_num):
x_in = Input(shape=in_shape)
x = Conv1D(64, 3, padding='same', activation='relu')(x_in)
x = BatchNormalization()(x)
x = MaxPooling1D(pool_size=2)(x)
x = Dropout(0.05)(x)
x = Conv1D(128, 3, padding='same', activation='relu')(x)
x = BatchNormalization()(x)
x = MaxPooling1D(pool_size=2)(x)
x = Dropout(0.05)(x)
x = Conv1D(128, 3, padding='same', activation='relu')(x)
x = BatchNormalization()(x)
x = MaxPooling1D(pool_size=2)(x)
x = Dropout(0.05)(x)
x = Conv1D(128, 3, padding='same', activation='relu')(x)
x = BatchNormalization()(x)
x = MaxPooling1D(pool_size=2)(x)
x = Dropout(0.05)(x)
x = Flatten()(x)
dense_vec = Dense(256, activation='relu', name='dense_vec')(x)
x = Dropout(0.05)(dense_vec)
y_out = Dense(class_num, activation='softmax', name='cls_head')(x)
return Model(inputs=x_in, outputs=y_out)
CLASS_TOTAL = 3
net_fn_mfcc = craft_cnn_functional((500, 13), class_num=CLASS_TOTAL)
feature_net = Model(inputs=net_fn_mfcc.input,
outputs=net_fn_mfcc.get_layer('dense_vec').output)
feature_net.save("cnn_feature_extractor_mfcc.h5")
Почему это важно
Извлечение промежуточных признаков опирается на стабильный, полностью определённый вычислительный граф и известный входной тензор. Functional API обеспечивает оба условия уже при создании модели, что позволяет надёжно строить Model, публикующий тензоры из любого внутреннего слоя. Без конкретного входа и трассируемого графа TensorFlow не сможет создать новую модель для извлечения признаков.
Выводы
Если цель — получить эмбеддинги из Dense или любого другого промежуточного слоя, предпочтительнее использовать Functional API. Явно задайте вход, присвойте целевому слою понятное имя и собирайте экстрактор по выходу этого слоя. Такой подход устраняет подводные камни инициализации графа и делает извлечение признаков простым и воспроизводимым.