Fine-Tuning GLM-OCR


Fine-Tuning GLM-OCR

In the last article, we covered an introduction to GLM-OCR. The discussion revolved around the architecture and inference for various tasks. With specific prompts, along with text recognition, GLM-OCR can also carry out formula recognition. However, it falters in complex mathematical formulas. In this article, we will be fine-tuning GLM-OCR and observe to what extent we can improve the performance of the model on a task-specific dataset.

Fine-tuned GLM-OCR in action - recognizing mathematical equations.
Figure 1. Fine-tuned GLM-OCR in action – recognizing mathematical equations.

We will use a mathematical formula dataset for fine-tuning GLM-OCR here. It performs well on such samples out of the box. However, there are instances where the final OCR result is wrong, either because the formula is too complex or there are too many notations cluttered together. Our aim is to check whether fine-tuning on the entire dataset improves the results on such samples or not.

We will cover the following while training GLM-OCR:

  • Understanding the mathematical formula dataset
  • Setting up LLama-Factory for the training process
  • Creating the dataset in the desired structure
  • Fine-tuning the GLM-OCR model
  • Creating a simple Gradio application for running inference

The Maths Equation Dataset

We will fine-tune the GLM-OCR model on the 25k_math_equation dataset from Kaggle.

The dataset contains images of 25000 mathematical equations along with their LaTeX equations in a text file.

We get the following directory structure after downloading and extracting the dataset.

├── images  [25000 entries exceeds filelimit, not opening dir]
└── labels.txt

The images directory contains the 25000 equation images and labels.txt file contains the LaTeX equations.

To understand the dataset better, let’s take a look at a few lines from the labels.txt file.

eq_00000.png	\int_0^{4} \left(y_{40}\right)^{1} dx
eq_00001.png	\sin\left(\frac{\ln\left(\frac{852}{479} \cdot P_{27}\right)}{\frac{\frac{\cos\left(M_{40}\right)}{\phi_{36}^{1} \cdot 459}}{\frac{\frac{u_{35}}{j^{7}}}{36}}}\right)
eq_00002.png	\cos\left(\frac{\frac{\left(Q^{1}\right)^{3}}{725 \cdot 12}}{\omega \cdot X - i^{10}} - \left(s_{14}\right) + \ln\left(z_{36}\right) - \left(l_{18} \cdot 695\right)^{5}\right)

Each new line contains two elements separated by a tab space. The first element is the image file name, and the second one is the LaTeX equation.

The following are a few image samples and their raw equations.

Image samples and their corresponding LaTeX equations from the Math Eqs dataset.
Figure 2. Image samples and their corresponding LaTeX equations from the Math Eqs dataset.

We can see that the dataset contains equations of varying complexity.

The Project Directory Structure

Let’s take a look at the project directory structure before moving into the codebase.

├── glm_ocr_app
│   ├── glm_ocr_lora_sft_math_eqs_data
│   │   ├── checkpoint-1000
│   │   │   ├── adapter_config.json
│   │   │   ├── adapter_model.safetensors
│   │   │   ├── chat_template.jinja
│   │   │   ├── optimizer.pt
│   │   │   ├── processor_config.json
│   │   │   ├── README.md
│   │   │   ├── rng_state.pth
│   │   │   ├── scheduler.pt
│   │   │   ├── tokenizer_config.json
│   │   │   ├── tokenizer.json
│   │   │   ├── trainer_state.json
│   │   │   └── training_args.bin
.
.
.
│   │   ├── adapter_config.json
│   │   ├── adapter_model.safetensors
│   │   ├── all_results.json
│   │   ├── chat_template.jinja
│   │   ├── processor_config.json
│   │   ├── README.md
│   │   ├── tokenizer_config.json
│   │   ├── tokenizer.json
│   │   ├── trainer_log.jsonl
│   │   ├── trainer_state.json
│   │   ├── training_args.bin
│   │   ├── training_loss.png
│   │   └── train_results.json
│   ├── input
│   │   └── math_eqs
│   │       ├── images  [24900 entries exceeds filelimit, not opening dir]
│   │       ├── test_images  [100 entries exceeds filelimit, not opening dir]
│   │       └── labels.txt
│   ├── gradio_app.py
│   ├── README.md
│   └── requirements.txt
├── LLaMA-Factory
│   ├── assets
│   │   ├── sponsors
│   │   │   ├── serpapi.svg
│   │   │   └── warp.jpg
│   │   ├── thirdparty
│   │   │   ├── colab.svg
│   │   │   ├── discord.svg
│   │   │   ├── dsw.svg
│   │   │   ├── lab4ai.svg
│   │   │   └── online.svg
│   │   └── logo.png
│   ├── data  [26 entries exceeds filelimit, not opening dir]
│   ├── docker
│   │   ├── docker-cuda
│   │   │   ├── docker-compose.yml
│   │   │   ├── Dockerfile
│   │   │   ├── Dockerfile.base
│   │   │   ├── Dockerfile.megatron
│   │   │   └── README.md
│   │   ├── docker-npu
│   │   │   ├── docker-compose.yml
│   │   │   └── Dockerfile
│   │   └── docker-rocm
│   │       ├── docker-compose.yml
│   │       └── Dockerfile
│   ├── docs
│   │   ├── en
.
.
.
│   │   │   ├── hyperparameters
│   │   │   │   ├── data-argument.md
│   │   │   │   ├── model-argument.md
│   │   │   │   ├── sample-argument.md
│   │   │   │   └── training-argument.md
│   │   │   ├── inference
│   │   │   │   └── deploy.md
.
.
.
└── README.md
  • We have two main directories. One is the glm_ocr_app that contains the Gradio application script for testing the fine-tuned model. Along with that, it also contains the glm_ocr_lora_sft_math_eqs_data that contains the fine-tuned LoRA so that we can load it easily when running the test experiments.
  • Then we have the LLaMA-Factory directory which will be our primary working ground for preparing the dataset and fine-tuning. We are using the LlamaFactory repository here to handle the training process.

We will get into the setup steps in the next section.

The article comes with a zip file containing the dataset preprocessing script, the Gradio inference script, and the fine-tuned LoRA. The following sections explain how to handle the directory structure and where to copy the necessary files to carry on with the article.

Download Code

Exploring the Codebase for Fine-Tuning GLM-OCR

Let’s jump into the coding section for this article. We will cover the following here:

  • Dataset preparation for training the GLM-OCR model
  • Training the model
  • Inference using the trained LoRA model

Before moving further, please download the zip file that comes with this article, as we will need the dataset preparation script from there.

Setting Up Llama-Factory

Let’s set up LLaMA-Factory first. These steps are almost identical to the ones provided in the official GLM-OCR repository. The only changes are going to be our dataset preparation steps. Clone the repository and install the requirements.

git clone --depth 1 https://github.com/hiyouga/LLaMA-Factory.git
cd LLaMA-Factory
pip install -e .
pip install -r requirements/metrics.txt
pip install peft==0.19.1 

Next, install the latest version of Transformers.

pip install --upgrade transformers

In case you face issues, install the pinned version that was used for the codebase of this article.

pip install transformers==5.6.0

Dataset Preparation

Next comes the crucial part, preparing the training dataset.

First, copy the mathematical equation dataset that we previously downloaded into the data directory inside the LLaMA-Factory repository. The full path will be LLaMA-Factory/data.

We have renamed the mathematical equation directory to math_eqs for easier access, and it contains the images subdirectory and the labels.txt file.

Second, the zip file that comes with the article contains a dataset preparation script, prepare_math_eqs_data.py. Copy the same to the LLaMA-Factory/data directory. Finally, the directory should look similar to the following.

Directory structure of the data directory in Llama-Factory for fine-tuning GLM-OCR.
Figure 3. Directory structure of the data directory in Llama-Factory for fine-tuning GLM-OCR.

Let’s take a look at prepare_math_eqs_data.py script.

import os
import json

from tqdm import tqdm

DATA_ROOT_DIR = 'math_eqs'
IMAGE_PATH = os.path.join(DATA_ROOT_DIR, 'images')
LABEL_PATH = os.path.join(DATA_ROOT_DIR, 'labels.txt')

def prepare_math_eqs_data(image_dir_path, label_path):
    """
    The text file contains the separate lines in this format.

    eq_00000.png	\int_0^{4} \left(y_{40}\right)^{1} dx

    The image file name and the equation is separated by tab-space.
    """

    data = []
    with open(label_path, 'r', encoding='utf-8') as f:
        labels_data = f.read().strip().splitlines()

    for i, line in enumerate(tqdm(labels_data, desc="Processing labels")):
        image_name, formula_text = line.split('\t')
        image_path = os.path.join(image_dir_path, image_name)

        if os.path.exists(image_path):
            data.append({
                    "messages": [
                        {
                            "role": "user",
                            "content": "Formula recognition:"
                        },
                        {
                            "role": "assistant",
                            "content": f"\$$ {formula_text} \$$"
                        }
                    ],
                    "images": [image_path]
                })
        else:
            print(f"Warning: Image file {image_path} does not exist.")
    
    return data

if __name__ == '__main__':

    data = prepare_math_eqs_data(IMAGE_PATH, LABEL_PATH)
    output_file = 'math_eqs_data.json'
    with open(output_file, 'w', encoding='utf-8') as f:
        json.dump(data, f, ensure_ascii=False, indent=4)
    
    print(f"Data preparation complete. JSON file saved as {output_file}.")

This creates a math_eqs_data.json in the LLaMA-Factory/data directory in the ShareGPT format. We extract the image names and the raw LaTeX equations from the math_eqs/labels.txt file by splitting the data into separate lines. Then we get the relative image path from math_eqs/images directory and create the JSON file in the following structure.

[
  {
    "messages": [
      {
        "role": "user",
        "content": "Formula recognition:"
      },
      {
        "role": "assistant",
        "content": "\$\$\$\$"
      }
    ],
    "images": [
      "math_eqs/images/eq_00000.png"
    ]
  },
  {
    "messages": [
      {
        "role": "user",
        "content": "Formula recognition:"
      },
      {
        "role": "assistant",
        "content": "\$\$\$\$"
      }
    ],
    "images": [
      "math_eqs/images/eq_00000.png"
    ]
  }
  ...
]

We enclose the equations within the $$ signs so that the model learns to generate the equations in one shot.

We can execute the script within the LLaMA-Factory/data directory to generate the math_eqs_data.json file.

python prepare_math_eqs_data.py

Next, we need to register the dataset JSON file in the LLaMA-Factory/data/dataset_info.json file. It already contains a number of pre-registered datasets. We can register our own dataset at the end of the JSON structure.

"math_eqs_data": {
    "file_name": "math_eqs_data.json",
    "formatting": "sharegpt",
    "columns": {
      "messages": "messages",
      "images": "images"
    },
    "tags": {
      "role_tag": "role",
      "content_tag": "content",
      "user_tag": "user",
      "assistant_tag": "assistant"
    }
  }

It should look like the following.

Registering the Mathematical Equation data for fine-tuning GLM-OCR.
Figure 4. Registering the Mathematical Equation data for fine-tuning GLM-OCR.

Training the GLM-OCR Model

We will carry out LoRA fine-tuning of the GLM-OCR model here. The zip file that comes with this article also contains a glm_ocr_lora_sft.yaml. This is a modified training configuration file from the GLM-OCR repository. Copy and paste this into the parent LLaMA-Factory directory.

The following is the content of the file.

### model
model_name_or_path: zai-org/GLM-OCR    # or local path: /path/to/GLM-OCR
trust_remote_code: true

### method
stage: sft
do_train: true
finetuning_type: lora
lora_rank: 16
lora_target: all

### dataset
dataset: math_eqs_data
template: glm_ocr
cutoff_len: 2048
max_samples: 25000
preprocessing_num_workers: 8
dataloader_num_workers: 4

### output
output_dir: ./glm_ocr_lora_sft_math_eqs_data
logging_steps: 10
save_steps: 1000
plot_loss: true
overwrite_output_dir: true
save_only_model: false
report_to: none  # choices: [none, wandb, tensorboard, swanlab, mlflow]

### train
per_device_train_batch_size: 8
gradient_accumulation_steps: 4
learning_rate: 1.0e-4
num_train_epochs: 5
lr_scheduler_type: cosine
warmup_ratio: 0.1
bf16: true
ddp_timeout: 180000000
resume_from_checkpoint: null

### eval
# val_size: 0.1
# per_device_eval_batch_size: 1
# eval_strategy: steps
# eval_steps: 500

Although there are several parameters in the above YAML file, a few are quite important:

  • finetuning_type: lora: This tells the training script that we want to carry out a LoRA fine-tuning.
  • lora_rank: 16 and lora_target: all: We are using a LoRA rank of 16, and we are training all the linear weights in the adapter.
  • max_samples: 25000: We will be training the model on all 25000 samples.
  • preprocessing_num_workers: 8 and dataloader_num_workers: 4: 8 CPU workers will be used for tokenizing the dataset, and 4 will be used for the dataloader.
  • Finally, we are using a batch size of 8, with 4 gradient accumulation steps, and training for 5 epochs.

The above training setting uses 6 GB-7 GB of VRAM.

All the training and inference experiments shown here were carried out on a 10GB RTX 3080 GPU.

Let’s start the training process now. We will execute the following command within the Llama-Factory directory to start the training.

DISABLE_VERSION_CHECK=1 CUDA_VISIBLE_DEVICES=0   llamafactory-cli train glm_ocr_lora_sft.yaml

The adapter checkpoints will be saved in the glm_ocr_lora_sft_math_eqs_data directory.

The following are the training logs and the training loss graph.

{
    "epoch": 5.0,
    "total_flos": 1.4272293267033293e+17,
    "train_loss": 0.05630928651641702,
    "train_runtime": 4554.0756,
    "train_samples_per_second": 27.448,
    "train_steps_per_second": 0.859
}
Training loss plot after fine-tuning GLM-OCR.
Figure 5. Training loss plot after fine-tuning GLM-OCR.

Although the loss is low, we will get more insights once we run inference using the trained adapter.

Running Inference Using the Trained GLM-OCR Adapter

We have the gradio_app.py script in the glm_ocr_app directory. The file launches a Gradio application where we can either load the pretrained GLM-OCR model or our trained adapter model for running inference.

Let’s start the application and check a few results.

python gradio_app.py

The following video shows a workflow where we load the LoRA weights and run inference.

Video 1. Fine-tuned GLM-OCR in action for generating mathematical equations in LaTeX format from images.

Now, the real question is whether our fine-tuning process made an impact or not. To some extent, it did. For example, the following figure shows an original image (eq_24981.png), where the pretrained model gave a malformed LaTeX formula, and the fine-tuned one generated the correct equation.

Example where the fine-tuned GLM-OCR model generated correct result compared to the pretrained one.
Figure 6. Example where the fine-tuned GLM-OCR model generated correct result compared to the pretrained one.

However, there are some extremely complex samples like eq_24990.png and eq_24984.png where, even after fine-tuning, although some of the notations were generated correctly, the final formula was not entirely correct.

Examples showcasing wrong results generated by the fine-tuned GLM-OCR model because of complexity of samples.
Figure 7. Examples showcasing wrong results generated by the fine-tuned GLM-OCR model because of the complexity of the samples.

There could be several reasons for this. Maybe we need even more training data. Or perhaps the notations are so small and unclear that the model is unable to learn the features. However, training on even more high-quality and high-resolution data will surely solve this.

Summary and Conclusion

In this article, we focused on fine-tuning the GLM-OCR model on a mathematical equation dataset. We started with the dataset exploration, moved to preparing the dataset in ShareGPT format, and carried out training & inference. We also discussed where the trained adapter needs improvement. I hope this article was worth your time.

If you have any questions, thoughts, or suggestions, please leave them in the comment section. I will surely address them.

You can contact me using the Contact section. You can also find me on LinkedIn, and X.

Liked it? Take a second to support Sovit Ranjan Rath on Patreon!
Become a patron at Patreon!

Leave a Reply

Your email address will not be published. Required fields are marked *