[Modelo + Código/Tutorial de nivel de niñera] Uso de Pytorch para realizar el reconocimiento de caracteres chinos escritos a mano

prefacio


Artículo de referencia:

Las dos primeras referencias:
[Pytorch] Reconocimiento de caracteres chinos escritos a mano con base en CNN
"Pytorch" CNN realiza el reconocimiento de caracteres chinos escritos a mano (producción de conjuntos de datos, construcción de redes, capacitación y prueba de verificación de todos los códigos)
modelo: conjunto de datos
detallados de la red EfficientNetV2
(sin necesidad de descargar desde aquí, puede echar un vistazo a su introducción):
CASIA Bases de datos de escritura a mano en chino en línea y fuera de línea

En vista del hecho de que han pasado 3202 años y GPT4 ha aparecido, todavía faltan tutoriales prácticos sobre la red neuronal básica "subyacente" del reconocimiento de caracteres chinos que permite a los novatos comenzar directamente , por lo que Me aventuraré a escribir uno yo mismo.

Las principales características de este artículo:

  1. Utilice el modelo EfficientNetV2 para realizar realmente el reconocimiento de 3755 caracteres chinos

  1. proyecto de código abierto

  1. Público modelo pre-entrenado

  1. Conjuntos de datos prefabricados que se pueden usar directamente sin procesamiento

conjunto de datos


Usando el conjunto de datos de caracteres chinos escritos a mano producidos por la Academia de Ciencias de China, el enlace va directamente al sitio web oficial, por lo que no introduciré mucho aquí, solo lleno de respeto.

El blog al que se hace referencia anteriormente puede requerir que lo proceses previamente después de descargarlo tú mismo, pero hay muchos amigos que tienen problemas en este enlace. De acuerdo con el principio de los tutoriales de nivel de niñera, he pasado los datos preprocesados ​​a Beihang Cloud Disk (parece estar dañado, primero use el enlace en el área de comentarios), la velocidad debería ser más rápida que Baidu Netdisk, probablemente...

El modelo de preentrenamiento se cargó (hay un enlace en la parte posterior), pero si desea entrenarlo usted mismo, debe descargar este conjunto de datos y descomprimirlo en la carpeta de datos en la estructura del proyecto, como se muestra a continuación.

La carpeta de datos y la carpeta de registro deben ser creadas por usted mismo.

estructura del proyecto


Código fuente completo: [ Código fuente del proyecto ]

Estructura de directorios

Preste atención a la estructura de la carpeta de datos, no extravíe el conjunto de datos ni anide más carpetas

├─Chinese_Character_Rec
│ ├─asserts
│ │ ├─*.png
│ ├─char_dict
│ ├─Data.py
│ ├─EfficientNetV2
│ │ ├─demo.py
│ │ ├─EffNetV2.py
│ │ ├─Evaluar.py
│ │ ├─model.py
│ │ └─Train.py
│ ├─Utils.py
│ ├─VGG19
│ │ ├─demo.py
│ │ ├─Evaluate.py
│ │ ├─model.py
│ │ ├─Tren. py
│ │ └─VGG19.py
| └─README.md
├─data
│ ├─test
│ │ ├─00000
│ │ ├─00001
│ │ ├─00002
│ │ ├─00003
│ | └─...
│ ├─prueba.txt
│ ├─tren
│ │ ├─00000
│ │ ├─00001
│ │ ├─00002
│ │ ├─00003
| | └─ ...
│ └─tren.txt
├─log
│ ├─log1.pth
│ └─…

modelo de red neuronal


Enlace de parámetro de modelo preentrenado (incluidos vgg19 y eficientenetv2)

Cambie el nombre del archivo .pth al formato log+number.pth, como log1.pth, y colóquelo en la carpeta de registro. Fácil de identificar y volver a entrenar.

VGG19

Aquí se han utilizado sucesivamente dos tipos de redes neuronales. Primero lo probé con VGG19 para clasificar los primeros 1000 caracteres chinos. El entrenamiento es un poco lento, principalmente porque el modelo es un poco antiguo y la cantidad de parámetros no es pequeña. Además, si desea cambiar a 3755 y usar los parámetros originales, será difícil converger. No sé cómo ajustar los parámetros. Se estima que la escala será grande después del ajuste. Por lo tanto, la versión de el modelo VGG19 aquí solo puede clasificar 1000 tipos, que son los datos, las primeras 1000 especies del conjunto (precisión> 92%).

EfficientNetV2

Este modelo es muy bueno, principalmente porque la capa convolucional es muy efectiva y el número de parámetros es muy pequeño. Use directamente la versión pequeña para clasificar 3755 caracteres chinos, y la convergencia es de casi media hora. Por lo tanto, el modelo utilizado en este artículo para implementar 3755 caracteres chinos es EfficientNetV2 (tasa de precisión > 89 %). Los siguientes tutoriales se basan en esto y se ignora VGG19. Si está interesado en el código fuente, léalo usted mismo.

El siguiente código no necesita ser escrito por usted mismo, el código fuente completo se proporcionó anteriormente, el siguiente tutorial es solo una explicación combinada con el código fuente.

entorno operativo


Memoria de video >=4G (relacionado con batchSize, cuando batchSize=512, la memoria de video ocupa 4.8G; si es 256 o 128, debe ser inferior a 4G, aunque conducirá a un entrenamiento más lento)

Memoria> = 16G (No ocupa mucha memoria durante el entrenamiento, pero la ocupará repentinamente cuando comience a cargarse. Si es menos de 16G, todavía tiene miedo de explotar)

Si no ha instalado Pytorch, ah, no sé cómo hacerlo, o puede leer el tutorial sobre cómo instalar Pytorch. (Los pasos generales son, si tiene una tarjeta N no demasiado antigua, primero vaya al controlador para verificar la versión de cuda, instale el CUDA apropiado, luego vaya a pytorch.org para encontrar las instrucciones de instalación apropiadas de acuerdo con la versión de CUDA , y luego instalar pip localmente)

El siguiente es el entorno operativo del proyecto, soy 3060 6G, CUDA versión 11.6

No se preocupe por el signo igual, puede instalar la última versión, de todos modos, no debería usar ninguna API especial aquí

torch~=1.12.1+cu116
torchvision~=0.13.1+cu116
Pillow~=9.3.0

Preparación del conjunto de datos


Primero defina el método classes_txt en Utils.py (no escrito por mí, pero de los dos blogs de CSDN, MyDataset es el mismo):

Genere la ruta de cada imagen y guárdela en train.txt o test.txt. Datos fáciles de leer durante el entrenamiento o la evaluación

def classes_txt(root, out_path, num_class=None):
    dirs = os.listdir(root)
    if not num_class:
        num_class = len(dirs)

    with open(out_path, 'w') as f:
        end = 0
        if end < num_class - 1:
            dirs.sort()
            dirs = dirs[end:num_class]
            for dir1 in dirs:
                files = os.listdir(os.path.join(root, dir1))
                for file in files:
                    f.write(os.path.join(root, dir1, file) + '\n')

Defina la clase Dataset, que se utiliza para crear un conjunto de datos, y agregue una etiqueta correspondiente a cada imagen, es decir, el nombre de código de la carpeta donde se encuentra la imagen.

class MyDataset(Dataset):
    def __init__(self, txt_path, num_class, transforms=None):
        super(MyDataset, self).__init__()
        images = []
        labels = []
        with open(txt_path, 'r') as f:
            for line in f:
                if int(line.split('\\')[1]) >= num_class: # 超出规定的类,就不添加,例如VGG19只添加了1000类
                    break
                line = line.strip('\n')
                images.append(line)
                labels.append(int(line.split('\\')[1]))
        self.images = images
        self.labels = labels
        self.transforms = transforms

    def __getitem__(self, index):
        image = Image.open(self.images[index]).convert('RGB')
        label = self.labels[index]
        if self.transforms is not None:
            image = self.transforms(image)
        return image, label

    def __len__(self):
        return len(self.labels)

Entrada


Puse todo tipo de superparámetros en argumentos para facilitar la modificación, ajústelos de acuerdo con la situación real. Este conjunto de valores predeterminados son los hiperparámetros que utilicé al entrenar este modelo. ¡El tamaño de imagen predeterminado es 32 porque mi memoria de video es demasiado pequeña! ! Sin embargo, el tamaño de las imágenes proporcionadas por el conjunto de datos generalmente no excede 64. Si desea entrenar con mayor precisión, puede probar el tamaño de 64 * 64.

Si revienta la memoria durante el entrenamiento, reduzca el tamaño del lote, intente con 256, 128, 64, 32

parser = argparse.ArgumentParser(description='EfficientNetV2 arguments')
parser.add_argument('--mode', dest='mode', type=str, default='demo', help='Mode of net')
parser.add_argument('--epoch', dest='epoch', type=int, default=50, help='Epoch number of training')
parser.add_argument('--batch_size', dest='batch_size', type=int, default=512, help='Value of batch size')
parser.add_argument('--lr', dest='lr', type=float, default=0.0001, help='Value of lr')
parser.add_argument('--img_size', dest='img_size', type=int, default=32, help='reSize of input image')
parser.add_argument('--data_root', dest='data_root', type=str, default='../../data/', help='Path to data')
parser.add_argument('--log_root', dest='log_root', type=str, default='../../log/', help='Path to model.pth')
parser.add_argument('--num_classes', dest='num_classes', type=int, default=3755, help='Classes of character')
parser.add_argument('--demo_img', dest='demo_img', type=str, default='../asserts/fo2.png', help='Path to demo image')
args = parser.parse_args()


if __name__ == '__main__':
    if not os.path.exists(args.data_root + 'train.txt'): # 只生成一次
        classes_txt(args.data_root + 'train', args.data_root + 'train.txt', args.num_classes)
    if not os.path.exists(args.data_root + 'test.txt'): # 只生成一次
        classes_txt(args.data_root + 'test', args.data_root + 'test.txt', args.num_classes)

    if args.mode == 'train':
        train(args)
    elif args.mode == 'evaluate':
        evaluate(args)
    elif args.mode == 'demo':
        demo(args)
    else:
        print('Unknown mode')

tren


Sobre la base del blog anterior de CSDN, se agrega lr_scheduler para ajustar la tasa de aprendizaje por sí mismo (si no hay mejora en 2 épocas consecutivas, reducir lr a la mitad), y se agrega la función de capacitación continua:

Primero verifique si hay un archivo de parámetros en la carpeta de registro, si no, se considerará como el entrenamiento inicial, si lo hay, busque el log.pth con el número de sufijo más grande, continúe entrenando sobre esta base y guárdelo cada vez que se completa una época El último log.pth, nombre de código es +1 desde la última vez. De esta forma, se pueden realizar varios entrenamientos para evitar errores durante el proceso de entrenamiento y daños en el archivo de parámetros.

Entre ellos, has_log_file y find_max_log se definen en Utils.py.

def train(args):
    print("===Train EffNetV2===")
    # 归一化处理,不一定要这样做,看自己的需求,只是预训练模型的训练是这样设置的
    transform = transforms.Compose(
        [transforms.Resize((args.img_size, args.img_size)), transforms.ToTensor(),
         transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
         transforms.ColorJitter()])  

    train_set = MyDataset(args.data_root + 'train.txt', num_class=args.num_classes, transforms=transform)
    train_loader = DataLoader(train_set, batch_size=args.batch_size, shuffle=True)
    device = torch.device('cuda:0')
    # 加载模型
    model = efficientnetv2_s(num_classes=args.num_classes)
    model.to(device)
    model.train()
    criterion = nn.CrossEntropyLoss()
    optimizer = optim.Adam(model.parameters(), lr=args.lr)
    # 学习率调整函数,不一定要这样做,可以自定义
    scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', patience=2, factor=0.5)
    print("load model...")
    
    # 加载最近保存了的参数
    if has_log_file(args.log_root):
        max_log = find_max_log(args.log_root)
        print("continue training with " + max_log + "...")
        checkpoint = torch.load(max_log)
        model.load_state_dict(checkpoint['model_state_dict'])
        optimizer.load_state_dict(checkpoint['optimizer_state_dict'])
        loss = checkpoint['loss']
        epoch = checkpoint['epoch'] + 1
    else:
        print("train for the first time...")
        loss = 0.0
        epoch = 0

    while epoch < args.epoch:
        running_loss = 0.0
        for i, data in enumerate(train_loader):
            inputs, labels = data[0].to(device), data[1].to(device)
            optimizer.zero_grad()
            outs = model(inputs)
            loss = criterion(outs, labels)
            loss.backward()
            optimizer.step()
            running_loss += loss.item()
            if i % 200 == 199:
                print('epoch %5d: batch: %5d, loss: %8f, lr: %f' % (
                    epoch + 1, i + 1, running_loss / 200, optimizer.state_dict()['param_groups'][0]['lr']))
                running_loss = 0.0

        scheduler.step(loss)
        # 每个epoch结束后就保存最新的参数
        print('Save checkpoint...')
        torch.save({'epoch': epoch,
                    'model_state_dict': model.state_dict(),
                    'optimizer_state_dict': optimizer.state_dict(),
                    'loss': loss},
                   args.log_root + 'log' + str(epoch) + '.pth')
        print('Saved')
        epoch += 1

    print('Finish training')

Evaluar


No hay nada que decir, solo ejecute el conjunto de prueba y calcule la precisión general. Pero hay una imperfección, es decir, no se puede ver la tasa de precisión específica de cada clase. Mi modelo preentrenado en realidad siente que varias categorías se ajustan demasiado, pero soy demasiado perezoso para ajustarlo.

def evaluate(args):
    print("===Evaluate EffNetV2===")
    # 这个地方要和train一致,不过colorJitter可有可无
    transform = transforms.Compose(
        [transforms.Resize((args.img_size, args.img_size)), transforms.ToTensor(),
         transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
         transforms.ColorJitter()])

    model = efficientnetv2_s(num_classes=args.num_classes)
    model.eval()
    if has_log_file(args.log_root):
        file = find_max_log(args.log_root)
        print("Using log file: ", file)
        checkpoint = torch.load(file)
        model.load_state_dict(checkpoint['model_state_dict'])
    else:
        print("Warning: No log file")

    model.to(torch.device('cuda:0'))
    test_loader = DataLoader(MyDataset(args.data_root + 'test.txt', num_class=args.num_classes, transforms=transform),batch_size=args.batch_size, shuffle=False)
    total = 0.0
    correct = 0.0
    print("Evaluating...")
    with torch.no_grad():
        for i, data in enumerate(test_loader):
            inputs, labels = data[0].cuda(), data[1].cuda()
            outputs = model(inputs)
            _, predict = torch.max(outputs.data, 1)
            total += labels.size(0)
            correct += (predict == labels).sum().item()
    acc = correct / total * 100
    print('Accuracy'': ', acc, '%')

razonamiento


Ingrese una imagen de texto y emita el resultado del reconocimiento:

Entre ellos, char_dict es el código gb2312 correspondiente al nombre de código de cada carácter chino en el conjunto de datos.El resultado de salida de este modelo es su nombre de código en el conjunto de datos, por lo que debe verificar este char_dict para obtener su carácter chino correspondiente. .

def demo(args):
    print('==Demo EfficientNetV2===')
    print('Input Image: ', args.demo_img)
    # 这个地方要和train一致,不过colorJitter可有可无
    transform = transforms.Compose(
        [transforms.Resize((args.img_size, args.img_size)), transforms.ToTensor(),
         transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])])
    img = Image.open(args.demo_img)
    img = transform(img)
    img = img.unsqueeze(0) # 增维
    model = efficientnetv2_s(num_classes=args.num_classes)
    model.eval()
    if has_log_file(args.log_root):
        file = find_max_log(args.log_root)
        print("Using log file: ", file)
        checkpoint = torch.load(file)
        model.load_state_dict(checkpoint['model_state_dict'])
    else:
        print("Warning: No log file")

    with torch.no_grad():
        output = model(img)
    _, pred = torch.max(output.data, 1)
    f = open('../char_dict', 'rb')
    dic = pickle.load(f)
    for cha in dic:
        if dic[cha] == int(pred):
            print('predict: ', cha)
    f.close()

Por ejemplo, la imagen de entrada es:

Resultado de la ejecución del programa:

otras instrucciones


Estoy tratando de portar este modelo a una aplicación de Android, porque Pytorch tiene un conjunto de Pytorch para Android, pero ahora encuentro un problema, la implementación interna de su función bitmap2Tensor es diferente de toTensor()+Normalize() de Pytorch, lo que da como resultado la misma imagen de entrada, los tensores convertidos son diferentes. Por ejemplo, la imagen que ingresé tiene caracteres negros sobre un fondo blanco, y la salida del fondo blanco es la misma, pero el valor de la parte negra está compensado. Uso el mismo conjunto de parámetros de normalización. No sé por qué. Entonces, la diferencia en este tensor hizo que el rendimiento del lado de Android fuera muy pobre. Actualmente estoy buscando una solución. ¿El procesamiento en escala de grises puede ser la salida?

Además, la precisión de este modelo no parece ser muy buena para las fuentes que son demasiado delgadas y demasiado oscuras, y aún puede estar un poco sobreajustada. Se recomienda que la imagen de entrada se acerque al estilo del conjunto de datos, el negro debe ser lo más claro posible y las líneas no deben ser demasiado delgadas.

Si aún tiene alguna pregunta, puede publicarla en el área de comentarios. Si no la ve, deténgame en la estación b ( https://www.bilibili.com/read/cv22530702 ). Eso es todo, detengámonos con el kung fu tradicional, gracias a todos.

Supongo que te gusta

Origin blog.csdn.net/Katock_Cricket/article/details/129674463
Recomendado
Clasificación