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. Явно задайте вход, присвойте целевому слою понятное имя и собирайте экстрактор по выходу этого слоя. Такой подход устраняет подводные камни инициализации графа и делает извлечение признаков простым и воспроизводимым.