Здесь я демонстрирую результаты простой многоуровневой модели для распознавания цифр с помощью ансамблевого обучения. Этот проект направлен на определение одиночных цифр в проблеме классификации строковых цифр. В моем случае набор данных был частным, но здесь я разделяю общий подход.

Используя ансамблевое обучение, мы могли бы извлечь выгоду из производительности комбинированной модели, здесь это простой метод усреднения:

Общий подход к подготовке изображений заключается в следующем: измените масштаб каждого изображения до фиксированного размера (28 * n) * 28, где n - количество цифр, содержащихся в строке, а затем предскажите результат с помощью метода скользящего окна:

def prepare_image( img, show = False):
""" prepares the training and testing as well as
the partial images used in partial_img_rec by transforming them
into numpy arrays that the network will be able to process """
# convert to greyscale
img = img.convert("L")
# rescale image to 28 * 28 dimension if neccessary
if img.size != (28,28):
img = img.resize((28,28), PIL.Image.ANTIALIAS)
# transform to vector
img = np.asarray(img, "float32")
img = img / 255.
# threshold eliminates background noise
img[img < 0.4] = 0.
if show:
plt.imshow(img, cmap = "Greys")
img = img.reshape((1, 28, 28, 1))
return img
def predict_result( img, show = False):
""" predicts the number in a picture (vector) """
#assert type(img) == np.ndarray and img.shape == (1, 28, 28, 1)
# 1, 28, 28, 1 is the image shape the input layer demands. 28, 28 are the dimensions and 1 stands for greyscale (channel)
if show:
img = img.reshape((28, 28))
# show the picture
plt.imshow(img, cmap='Greys')
plt.show()
img = img.reshape((1, 28, 28, 1))
# the probabilities
res_probabilities = predict_stacked_model(model, img)
# the value with the hightest probability
res_number = np.argmax(res_probabilities)
return  res_number, res_probabilities
def test_all():
""" evaluates the success rate using all the test data """
scores = model.evaluate(test_img, test_res, verbose=0)
print("Baseline Error: %.2f%%" % (100-scores[1]*100))
def partial_img_rec( image, upper_left, lower_right, results=[], show = True):
""" passes square parts of images to predict_result """
left_x, left_y = upper_left
right_x, right_y = lower_right
print("current test part: ", upper_left, lower_right)
print("results: ", results)
# condition to stop recursion: we've reached the full width of the picture
width, height = image.size
if right_x > width:
return results
partial = image.crop((left_x, left_y, right_x, right_y))
if show:
partial.show()
partial = prepare_image(partial)
# is there a number or operator in this part of the image?
res, prob = predict_result(partial)
print("result: ", res, ". probabilities: ", prob)
# only count this result if the network is at least 40% sure
if prob[0][res] >= 0.4:
step = int(height * 1)
results.append(res)
# step is 80% of the partial image's size (which is equivalent to the original image's height)
print("found valid result")
else:
# if there is no number or operator found we take smaller steps
step = height // 20
#print("step: ", step)
# recursive call with modified positions ( move on step variables )
return partial_img_rec(image, (left_x + step, left_y), (right_x + step, right_y), results = results)
def individual_digits( img):
""" uses partial_img_rec to predict individual digits in square images """
#assert type(img) == PIL.JpegImagePlugin.JpegImageFile or type(img) == PIL.PngImagePlugin.PngImageFile or type(img) == PIL.Image.Image
if img.size[0]  != img.size[1]:
print(img, " has the wrong proportions: ", img.size,". It has to be a square.")
return partial_img_rec(img, (0,0), (img.size[0], img.size[1]), results=[])
def multiple_digits( img):
""" takes as input an image without unnecessary whitespace surrounding the digits """
#assert type(imgName) == str
#img = cuttingImage(imgName)
#img = PIL.Image.open(imgName)
width, height = img.size
# start with the first square part of the image. This can work because there is no unneccessary whitespace.
res_list = partial_img_rec(img, (0,0),(height ,height), results = [])
res_str = ""
for elem in res_list:
res_str += str(elem)
return res_str

Для создания модели я объединил простую сверточную сеть с сетью LSTM, чтобы создать окончательную интегрированную модель:

В этом подходе мы обучаем каждую сеть отдельно, затем сохраняем веса сетей, и на заключительном этапе создания модели с накоплением мы загружаем веса первого этапа, а затем обучаем модель с накоплением:

# load models from file
def load_all_models( n_models):
all_models = list()
for i in range(n_models):
# define filename for this ensemble
filename = 'model_' + str(i + 1) + '.h5'
# load model from file
model = load_model(filename)
# add to list of members
all_models.append(model)
print('>loaded %s' % filename)
return all_models
# define stacked model from multiple member input models
def define_stacked_model( members):
# update all layers in all models to not be trainable
for i in range(len(members)):
model = members[i]
for layer in model.layers:
# make not trainable
layer.trainable = False
# rename to avoid 'unique layer name' issue
layer.name = 'ensemble_' + str(i+1) + '_' + layer.name
# define multi-headed input
ensemble_visible = [model.input for model in members]
# concatenate merge output from each model
ensemble_outputs = [model.output for model in members]
merge = concatenate(ensemble_outputs)
#print(ensemble_visible.shape, ensemble_outputs.shape)
print(merge.shape)
output = Dense(10, activation='softmax')(merge)
#print(hidden.shape)
#output = Dense(10, activation='softmax')(hidden)
model = Model(inputs=ensemble_visible, outputs=output)
# plot graph of ensemble
plot_model(model, show_shapes=True, to_file='model_graph.png')
# compile
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])
return model
# fit a stacked model
def fit_stacked_model( model, inputX, inputy):
# prepare input data
X = [inputX for _ in range(len(model.input))]
# encode output data
#inputy_enc = to_categorical(inputy)
# fit model
model.fit(X, inputy, epochs=10, verbose=1)
# make a prediction with a stacked model
def predict_stacked_model(model, inputX):
# prepare input data
X = [inputX for _ in range(len(model.input))]
# make prediction
return model.predict(X, verbose=0)

После определения необходимых функций, наконец, мы обучаем финальную модель:

from sklearn.metrics import accuracy_score
from keras.models import load_model
from keras.utils import to_categorical
from keras.utils import plot_model
from keras.layers.merge import concatenate
from numpy import argmax
n_members = 2
members = load_all_models(n_members)
print('Loaded %d models' % len(members))
# define ensemble model
model = define_stacked_model(members)
# fit stacked model on test dataset
fit_stacked_model(model, X_train, Y_train)

Результаты для простого использования Convloutional Network:

И результаты использования модели с накоплением следующие:

Как мы видим в этом проекте по распознаванию цифр, в большинстве случаев на 10 процентов производительность возросла, что действительно является хорошим результатом.

Ссылки:

1- https://keras.io/examples/mnist_hierarchical_rnn/

2- https://machinelearningmastery.com/stacking-ensemble-for-deep-learning-neural-networks/

3-https://stackoverflow.com/questions/59617642/reading-multiple-handwriting-digits-with-keras-cnn-model