[Tutorial de nível de modelo + código/babá] Usando o Pytorch para realizar o reconhecimento de caracteres chineses manuscritos

prefácio


Artigo de referência:

As duas primeiras referências:
[Pytorch] Reconhecimento de caracteres chineses manuscritos com base na CNN
"Pytorch" A CNN realiza o reconhecimento de caracteres chineses manuscritos (produção de conjunto de dados, construção de rede, treinamento e teste de verificação de todos os códigos)
modelo: conjunto de dados
detalhados da rede EfficientNetV2
(sem necessidade de baixe aqui, você pode dar uma olhada em sua introdução):
CASIA Online and Offline Chinese Handwriting Databases

Tendo em vista que já se passaram 3202 anos e o GPT4 foi lançado, ainda faltam tutoriais práticos sobre a rede neural básica "subjacente" do reconhecimento de caracteres chineses que permitem que os novatos comecem diretamente , então Vou me aventurar a escrever um eu mesmo.

As principais características deste artigo:

  1. Use o modelo EfficientNetV2 para realmente realizar o reconhecimento de caracteres chineses 3755

  1. Projeto de código aberto

  1. Público modelo pré-treinado

  1. Conjuntos de dados pré-fabricados que podem ser usados ​​diretamente sem processamento

conjunto de dados


Usando o conjunto de dados de caracteres chineses manuscritos produzidos pela Academia Chinesa de Ciências, o link vai direto para o site oficial, então não vou apresentar muito aqui, apenas com respeito.

O blog mencionado acima pode exigir que você o pré-processe após baixá-lo você mesmo, mas há muitos amigos que têm problemas neste link. De acordo com o princípio dos tutoriais em nível de babá, passei os dados pré-processados ​​para Beihang Cloud Disk (parece estar danificado, primeiro use o link na área de comentários), a velocidade deve ser mais rápida que Baidu Netdisk, provavelmente...

O modelo de pré-treinamento foi carregado (há um link na parte de trás), mas se você quiser treiná-lo você mesmo, você precisa baixar este conjunto de dados e descompactá-lo na pasta de dados na estrutura do projeto, conforme mostrado abaixo

A pasta de dados e a pasta de log precisam ser criadas por você.

estrutura do projeto


Código-fonte completo: [ código-fonte do projeto ]

Estrutura de Diretórios

Preste atenção à estrutura da pasta de dados, não perca o conjunto de dados ou aninhe mais pastas

├─Chinese_Character_Rec
│ ├─asserts
│ │ ├─*.png
│ ├─char_dict
│ ├─Data.py
│ ├─EfficientNetV2
│ │ ├─demo.py
│ │ ├─EffNetV2.py
│ │ ├─Evaluate.py
│ │ ├─model.py
│ │ └─Train.py
│ ├─Utils.py
│ ├─VGG19
│ │ ├─demo.py
│ │ ├─Evaluate.py
│ │ ├─model.py
│ │ ├─Treine. py
│ │ └─VGG19.py
| └─README.md
├─dados
│ ├─teste
│ │ ├─00000
│ │ ├─00001
│ │ ├─00002
│ │ ├─00003
│ | └─...
│ ├─test.txt
│ ├─trem
│ │ ├─00000
│ │ ├─00001
│ │ ├─00002
│ │ ├─00003
| | └─ ...
│ └─train.txt
├─log
│ ├─log1.pth
│ └─…

modelo de rede neural


Link de parâmetro de modelo pré-treinado (incluindo vgg19 e eficientenetv2)

Renomeie o arquivo .pth para o formato log+number.pth, como log1.pth, e coloque-o na pasta log . Fácil de identificar e retreinar.

VGG19

Dois tipos de redes neurais foram usados ​​aqui sucessivamente. Primeiro tentei com VGG19 para classificar os primeiros 1000 caracteres chineses. O treinamento é um pouco lento, principalmente porque o modelo é um pouco antigo e o número de parâmetros não é pequeno. Além disso, se você quiser mudar para 3755 e usar os parâmetros originais, será difícil convergir. Não sei como ajustar os parâmetros. Estima-se que a escala ficará grande após o ajuste. Portanto, a versão de o modelo VGG19 aqui só pode classificar 1000 tipos, que são os dados.As primeiras 1000 espécies do conjunto (precisão >92%).

EfficientNetV2

Este modelo é muito bom, principalmente porque a camada convolucional é muito eficaz e o número de parâmetros é muito pequeno. Use diretamente a versão pequena para classificar 3755 caracteres chineses e a convergência é de quase meia hora. Portanto, o modelo usado neste artigo para implementar 3755 caracteres chineses é EfficientNetV2 (taxa de precisão > 89%). Os tutoriais a seguir são baseados nisso e VGG19 é ignorado. Se você estiver interessado no código-fonte, leia você mesmo.

O código a seguir não precisa ser escrito por você, o código-fonte completo foi fornecido anteriormente, o tutorial a seguir é apenas uma explicação combinada com o código-fonte.

ambiente operacional


Memória de vídeo >=4G (em relação a batchSize, quando batchSize=512, a memória de vídeo ocupa 4,8G; se for 256 ou 128, deve ser menor que 4G, embora leve a um treinamento mais lento)

Memória >=16G (Não ocupa muita memória durante o treinamento, mas ocupará repentinamente quando começar a carregar. Se for menor que 16G, ainda tem medo de estourar)

Se você não instalou o Pytorch, ah, não sei como fazer, ou você pode ler o tutorial de instalação do Pytorch. (As etapas gerais são, se você tiver um cartão N não muito antigo, primeiro vá para o driver para verificar a versão do cuda, instale o CUDA apropriado e, em seguida, vá para pytorch.org para encontrar as instruções de instalação apropriadas de acordo com a versão do CUDA , e, em seguida, pip instalar localmente)

O seguinte é o ambiente operacional do projeto, sou 3060 6G, CUDA versão 11.6

Não se preocupe com o sinal de igual, você pode instalar a versão mais recente, de qualquer forma, não devo usar nenhuma API especial aqui

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

Preparação do conjunto de dados


Primeiro defina o método classes_txt em Utils.py (não escrito por mim, mas dos dois blogs da CSDN, MyDataset é o mesmo):

Gere o caminho de cada imagem e armazene-o em train.txt ou test.txt. Dados fáceis de ler durante o treinamento ou avaliação

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 a classe Dataset, que é usada para fazer um conjunto de dados, e adicione um rótulo correspondente a cada imagem, ou seja, o nome do código da pasta onde a imagem está localizada

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


Eu coloquei todos os tipos de super parâmetros em args para facilitar a modificação, por favor, ajuste de acordo com a situação real. Este conjunto de padrões são os hiperparâmetros que usei ao treinar este modelo. O tamanho de imagem padrão é 32 porque minha memória de vídeo é muito pequena! ! No entanto, o tamanho das imagens fornecidas pelo conjunto de dados geralmente não excede 64. Se você deseja treinar com mais precisão, pode tentar o tamanho de 64*64.

Se você estourar a memória durante o treinamento, reduza o batch_size, tente 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')

trem


Com base no blog CSDN anterior, lr_scheduler é adicionado para ajustar a taxa de aprendizado por si só (se não houver melhora em 2 épocas consecutivas, reduza lr à metade) e a função de treinamento contínuo é adicionada:

Primeiro verifique se existe um arquivo de parâmetro na pasta log. Caso não, será considerado como o treinamento inicial; se houver, encontre o log.pth com o maior número de sufixo, continue treinando com base nisso e salve-o a cada hora em que uma época é concluída. O último log.pth, nome de código é +1 desde a última vez. Dessa forma, vários treinamentos podem ser realizados para evitar erros durante o processo de treinamento e danos ao arquivo de parâmetros.

Entre eles, has_log_file e find_max_log são definidos em 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')

Avalie


Não há nada a dizer, apenas execute o conjunto de teste e calcule a precisão geral. Mas há uma imperfeição, ou seja, a taxa de precisão específica de cada classe não pode ser vista. Na verdade, meu modelo pré-treinado sente que várias categorias estão superajustadas, mas estou com preguiça de ajustá-lo.

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, '%')

raciocínio


Insira uma imagem de texto e gere o resultado do reconhecimento:

Entre eles, char_dict é o código gb2312 correspondente ao nome de código de cada caractere chinês no conjunto de dados. O resultado de saída desse modelo é seu nome de código no conjunto de dados, então você precisa verificar este char_dict para obter seu caractere chinês correspondente .

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 exemplo, a imagem de entrada é:

Resultado da execução do programa:

outras instruções


Estou tentando portar este modelo para um aplicativo Android, porque o Pytorch tem um conjunto de Pytorch para Android, mas agora me deparo com um problema, a implementação interna de sua função bitmap2Tensor é diferente do toTensor()+Normalize() do Pytorch, resultando em a mesma imagem de entrada, os tensores convertidos são diferentes. Por exemplo, a imagem que eu insiro tem caracteres pretos em um fundo branco e a saída do fundo branco é a mesma, mas o valor da parte preta é deslocado. Eu uso o mesmo conjunto de parâmetros de normalização. Não sei por quê. Então a diferença neste tensor fez com que o desempenho do lado do Android fosse muito ruim. Estou procurando uma solução. Processamento em escala de cinza pode ser a saída?

Além disso, a precisão desse modelo não parece ser muito boa para fontes muito finas e muito escuras, e ainda pode ser um pouco overfitting. Recomenda-se que a imagem de entrada esteja próxima ao estilo do conjunto de dados, o preto deve ser o mais claro possível e as linhas não devem ser muito finas.

Se você ainda tiver alguma dúvida, pode postá-la na área de comentários. Se você não as vir, por favor, me detenha na estação b ( https://www.bilibili.com/read/cv22530702 ). É isso, vamos parar com o kung fu tradicional, obrigado a todos.

Acho que você gosta

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