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:
Use o modelo EfficientNetV2 para realmente realizar o reconhecimento de caracteres chineses 3755
Projeto de código aberto
Público modelo pré-treinado
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 ]
![](https://img-blog.csdnimg.cn/img_convert/9cb7b61af030d97b8cd2db0435ceb5c0.png)
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 é:
![](https://img-blog.csdnimg.cn/img_convert/74d09dfb71edaa765daa08dacfef5d4c.png)
Resultado da execução do programa:
![](https://img-blog.csdnimg.cn/img_convert/19fe88e15e55e60db768aeb9956758fc.png)
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.