Translating PDF content using an LLM

“使用大语言模型翻译PDF内容”
text mining
deep learning
pytorch
Author

Li Yong Pan

Published

April 3, 2024

I have an extremely interesting and ambitious project that I am working on where I use React Native for the front end, and FastAPI as the backend. One of the things I am considering to do is to include the use of a Large Language Model (LLM). LLMs have been the hype for quite some time now, and I thought it was time to put one to use, aside from the usual ChatGPT prompts that I run.

Today I will use an LLM to translate content that we will extract from a PDF. Not very coincidentally it is a Dutch vocabulary list, which I want to translate to Mandarin Chinese.

Step 1: Finding a dataset

The very first relevant hit on Google brought me to a NT2 Vocabulary List. Let’s save this PDF as vocab.pdf.

Step 2: Exploring the PDF using pypdf

from pypdf import PdfReader
reader = PdfReader("vocab.pdf")

We should at least check if it indeed has read correctly that vocab.pdf contains 36 pages

print(f"There are {len(reader.pages)} pages.")
There are 36 pages.

Alright, seems good!

Step 3: Extracting text

The pages value is a list of PageObject objects and each of these come with the extract_text() method. The extracted content from the first page is the following:

first_page = reader.pages[0]
text_first_page = first_page.extract_text()
print(text_first_page)
1 Woordenlijst
nr. cursief woord uit de tekst uitleg
Les 1
1 gevorderden mensen die al een tijd bezig zijn met een studie of vaardigheid en dus enige 
kennis en ervaring hebben; de gevorderde
2 gemeenschappelijk met verschillende mensen dezelfde eigenschap of ruimte hebben
3 raden to guess/erraten/deviner/tahmin etmek; raden-raadde-geraden 
4 afvragen jezelf een vraag stellen; afvragen-vroeg af-afgevraagd 
5 stelt … voor dat stelt niks voor = dat is helemaal niet belangrijk; voorstelde-stelde voor-
voorgesteld 
6 piepklein heel erg klein 
7 bomvol heel erg vol
8 carpoolen met twee of meer mensen van één auto gebruikmaken om geld en het milieu te 
sparen; vooral van en naar je werk
9 omelet (de) een mengsel van gebakken eieren; omeletten
10 kebab gebakken vlees van een soort grill
11 yoghurt zuur zuivelproduct gemaakt van melk, wordt meestal gegeten na het avondeten of bij het ontbijt
12 chocola (de) bruin of wit snoep, gemaakt van cacao en suiker
13 tof goed, leuk, aardig
14 Hebreeuws (het) Hebreeuws = de Hebreeuwse taal (wordt gesproken in Israël)
15 paradijs (het) een ideale, mooie en rustige plaats; de tuin waarin Adam en Eva woonden volgens de Bijbel en de Koran; de paradijzen 
16 schaken een spel spelen met 16 witte en 16 zwarte figuren op een bord met witte en zwarte vierkantjes; schaken-schaakte-geschaakt 
17 Perzisch (het) Perzisch = de Perzische taal = het Farsi (wordt gesproken in Iran)
18 Indonesisch (het) Indonesisch = de Indonesische taal
19 sowieso Duits: in elk geval
20 terechtgekomen (toevallig) op een bepaalde plaats komen; terechtkomen-kwam terecht-terechtgekomen 
21 uitgeleend voor een tijdje aan iemand in gebruik gegeven; uitlenen-leende uit-uitgeleend 
22 verklap iets vertellen wat eigenlijk geheim moet blijven; verklappen-verklapte-verklapt 
23 discussiëren met argumenten bespreken; discussiëren-discussieerde-gediscussieerd
24 verbazen het zal je verbazen = je zult wel verbaasd zijn. Dat verbaast me; verbazen-verbaasde-verbaasd
25 cijfers getallen
26 taalkundige linguïst, expert op het gebied van talen
27 beschrijving (de) een verhaal vertellen over iets of iemand of over een gebeurtenis; beschrijvingen
28 grafiek (de) figuur uit de statistiek met lijnen om cijfers beter te kunnen begrijpen; grafieken
29 begrijpelijk goed te begrijpen
30 daarover over dat onderwerp
31 quitte quitte staan: gelijk staan; evenveel punten of evenveel hebben
32 buurlanden de buren van een land; Duitsland en België zijn de buurlanden van Nederland; het buurland
33 vanouds sinds lang geleden
34 weggeeft hier: uitleent, de ene taal neemt woorden uit de andere taal over; weggeven-gaf weg-weggegeven 
35 aanbieding iets in de aanbieding hebben = iets goeds of waardevols wat je weg wilt geven of wilt verkopen
36 emigreerden verhuisden naar een ander land; emigreren-emigreerde-geëmigreerd 
37 verhuisden van vaste plaats veranderen; verhuizen-verhuisde-verhuisd 
38 schets (de) tekening of ontwerp in grote lijnen; de schetsen
39 schaats (de) schoen met ijzer eronder; om mee over het ijs te bewegen; schaatsen

It is evident from scrolling through the PDF that it is quite well-structured: every word and its meaning start with some index. This is reaffirmed with the string printed above. However, if we would use the string representation above, it would be incredibly tedious to find some algorithm that can help extract the most important information: the words and their corresponding meaning. One naive way would be to define

  1. the first word as the index;
  2. the second word as the word in the vocabulary list;
  3. the remaining words to be the corresponding meaning.

But this sucks. To visualise that, let’s first split the lines for this very long string and show the first 10 results.

lines = text_first_page.splitlines()[:9]
print("\n".join(lines))
1 Woordenlijst
nr. cursief woord uit de tekst uitleg
Les 1
1 gevorderden mensen die al een tijd bezig zijn met een studie of vaardigheid en dus enige 
kennis en ervaring hebben; de gevorderde
2 gemeenschappelijk met verschillende mensen dezelfde eigenschap of ruimte hebben
3 raden to guess/erraten/deviner/tahmin etmek; raden-raadde-geraden 
4 afvragen jezelf een vraag stellen; afvragen-vroeg af-afgevraagd 
5 stelt … voor dat stelt niks voor = dat is helemaal niet belangrijk; voorstelde-stelde voor-

By using the naive way to divide the strings, the final line would give

Index Word Meaning
5 stelt … voor dat stelt niks voor = dat is helemaal niet belangrijk; voorstelde-stelde voor-’

But the actual result should be

Index Word Meaning
5 stelt … voor dat stelt niks voor = dat is helemaal niet belangrijk; voorstelde-stelde voor-’

Not great, so we have to find something else. The same method has an argument extraction_mode which is set to plain by default. If we use the extract_text(extraction_mode="layout"), it allows us to apply a more rigid and robust method.

text_first_page = first_page.extract_text(extraction_mode="layout")
lines = text_first_page.splitlines()[:9]
print("\n".join(lines))
nr.                                                                                                           cursief woord uit de tekst                                                                                                           uitleg


                                                              Les 1

1                                                                                                                             gevorderden                                                                                                                                                                                                                                                                                   mensen die al een tijd bezig zijn met een studie of vaardigheid en dus enige
                                                                                                                                                                                                                                                            kennis en ervaring hebben; de gevorderde
2                                                                                                                             gemeenschappelijk                                                                                                                                                                                       met verschillende mensen dezelfde eigenschap of ruimte hebben
3                                                                                                                             raden                                                                                                                                                                                                                                                                                                                                                                             to guess/erraten/deviner/tahmin etmek; raden-raadde-geraden

Now there are a lot more whitespace characters between each ‘column’. A better - not necessarily the best - method would be to:

  1. Filter the lines which are non-empty and start with a digit.
  2. Aggregate the rows which belong to one word/meaning combination.
  3. Split each line by at least three or more whitespace characters.
  4. Define the first part as the word and define the remaining text as its meaning.
  5. Create a Pandas DataFrame object for each page.
  6. Concatenate all dataframes into one dataframe.

Let’s set up this pipeline.

Step 4: Set up a processing pipeline

Below is the VocabExtractor.py file containing all the necessary steps to create a Pandas DataFrame containing the entire vocabulary list. The code should be self-explanatory, but we will highlight and explain some bits.

import pandas as pd
from pypdf import PdfReader


class VocabExtractor:
    def __init__(self, pdf_path):
        self.pdf_path = pdf_path

    def validate_lines(self, lines):
        return [line for line in lines if line and line[0].isdigit()]

1    def remove_overflow_lines(self, lines):
        res = [lines[0]]
        for current_item, next_item in zip(lines, lines[1:]):
            if next_item[0].isdigit():
                res.append(next_item)
            else:
                res[-1] += next_item
        return res

2    def trim_index(self, lines):
        no_index_lines = [line[line.find(' '):] for line in lines]
        return [line.strip() for line in no_index_lines]

    def lines_to_df(self, lines):
        split_lines = [line.split("  ") for line in lines]
        words = [line[0] for line in split_lines]
        meanings = [''.join(line[1:]).strip() for line in split_lines]
        return pd.DataFrame.from_dict({"Words": words, "Meanings": meanings})

    def pipeline_lines(self, text):
        lines = text.splitlines()
        page_lines = self.validate_lines(lines)
        no_overflow_lines = self.remove_overflow_lines(page_lines)
        no_index_lines = self.trim_index(no_overflow_lines)
        return self.lines_to_df(no_index_lines)

3    def extract_from_pdf(self):
        reader = PdfReader(self.pdf_path)
        pages = reader.pages

        res = []

        for page in pages:
            page_text = page.extract_text(extraction_mode="layout")
            res.append(self.pipeline_lines(page_text))

        df = pd.concat(res, ignore_index=True)
        df = df[df["Words"] != "Derde Ronde Nederlands voor buitenlanders"].reset_index(
            drop=True)

        return df
1
Initialise a list of which its only element is the first line of extracted text from the page. Then loop over the pairs of subsequent item pairs and check if the second element of the pair starts with a digit. If it does, then there is no overflow and the succeeding element is a valid new line of text which we append to the initial list. If it does not, then it means the line was overflown and we add this newline to the final element of the initial list.
2
For each line, extract the substring starting from the first ‘word’ following the first whitespace character. Effectively it removes the first word from each line, which should really be the index of the line.
3
Combine all methods defined above and loop through the pages to create a dataframe for each page. Finally concatenate all these dataframes and filter the lines which contain the ‘word’ “Derde Ronde Nederlands voor buitenlanders”, as it is noise from the footer that appear on every even page.

Now we can run the following code:

vocab_extractor = VocabExtractor("vocab.pdf")
df = vocab_extractor.extract_from_pdf()
df
Words Meanings
0 gevorderden mensen die al een tijd bezig zijn met een stud...
1 gemeenschappelijk met verschillende mensen dezelfde eigenschap o...
2 raden to guess/erraten/deviner/tahmin etmek; raden-r...
3 afvragen jezelf een vraag stellen; afvragen-vroeg af-af...
4 stelt … voor dat stelt niks voor = dat is helemaal niet bel...
... ... ...
1764 matchen combineren, koppelen, bij elkaar brengen; matc...
1765 op goed geluk willekeurig, blindelings, zonder planning
1766 presteren prestaties leveren, werken; presteren-presteer...
1767 revalideren weer leren bewegen na een ongeluk of operatie;...
1768 gerepareerd in orde gemaakt; repareren-repareerde-gerepareerd

1769 rows × 2 columns

The PDF also contained 1769 words. Looks good to me!

Step 5: Trimming the extracted text for translation

Now that we have our hands on the entirety of the PDF content, the only thing that remains to be done is to remove word redundancy. A quick scan through the PDF shows that (nearly) every noun shows a corresponding article (de/het) in the Words column and has its conjugations in the Meanings column. We should remove these as

  1. Dutch articles could potentially add noise to the context of the word and there are no direct translation for these articles;
  2. Chinese Mandarin deals with conjugations differently: conjugations (usually) do not add relevant information to a word.

The pattern for the articles seems to be (de/het). Using regular expressions it should be \s\s((de|het)\). The pattern for the conjugations seems to be ; words-words-words. Using regular expressions it would be a regex pattern of ;\s*[\w\s]+-[\w\s]+-[\w\s]+.

df["Words_trimmed"] = df["Words"].replace(regex=r'\s\((de|het)\)', value='')
df["Meanings_trimmed"] = df["Meanings"] \
    .replace(regex=r';\s*[\w\s]+-[\w\s]+-[\w\s]+', value='') \
    .replace(regex=r'\((de|het)\)', value='') \
    .str.strip()
df[df["Words_trimmed"] != df["Words"]]
Words Meanings Words_trimmed Meanings_trimmed
8 omelet (de) een mengsel van gebakken eieren; omeletten omelet een mengsel van gebakken eieren; omeletten
11 chocola (de) bruin of wit snoep, gemaakt van cacao en suiker chocola bruin of wit snoep, gemaakt van cacao en suiker
14 paradijs (het) een ideale, mooie en rustige plaats; de tuin w... paradijs een ideale, mooie en rustige plaats; de tuin w...
26 beschrijving (de) een verhaal vertellen over iets of iemand of o... beschrijving een verhaal vertellen over iets of iemand of o...
27 grafiek (de) figuur uit de statistiek met lijnen om cijfers... grafiek figuur uit de statistiek met lijnen om cijfers...
... ... ... ... ...
1739 echtpaar (het) getrouwde mensen; echtparen echtpaar getrouwde mensen; echtparen
1747 vuur (het) fire/feu/Feuer/ate; vuren vuur fire/feu/Feuer/ate; vuren
1754 vloeistof (de) liquid/liquide/Flüssigkeit/sıvı; vloeistoffen vloeistof liquid/liquide/Flüssigkeit/sıvı; vloeistoffen
1755 begroeiing (de) planten die erop groeien begroeiing planten die erop groeien
1762 DNA (het) genen DNA genen

548 rows × 4 columns

All articles are gone in the Words_trimmed column, great!

df[df["Meanings_trimmed"] != df["Meanings"]]
Words Meanings Words_trimmed Meanings_trimmed
2 raden to guess/erraten/deviner/tahmin etmek; raden-r... raden to guess/erraten/deviner/tahmin etmek
3 afvragen jezelf een vraag stellen; afvragen-vroeg af-af... afvragen jezelf een vraag stellen
13 Hebreeuws (het) Hebreeuws = de Hebreeuwse taal (wordt ge... Hebreeuws Hebreeuws = de Hebreeuwse taal (wordt gesproke...
16 Perzisch (het) Perzisch = de Perzische taal = het Farsi... Perzisch Perzisch = de Perzische taal = het Farsi (word...
17 Indonesisch (het) Indonesisch = de Indonesische taal Indonesisch Indonesisch = de Indonesische taal
... ... ... ... ...
1760 keken onderzochten; kijken-keek-gekeken keken onderzochten
1763 sluiten van vriendschappen vrienden maken; sluiten-sloot-gesloten sluiten van vriendschappen vrienden maken
1764 matchen combineren, koppelen, bij elkaar brengen; matc... matchen combineren, koppelen, bij elkaar brengen
1766 presteren prestaties leveren, werken; presteren-presteer... presteren prestaties leveren, werken
1768 gerepareerd in orde gemaakt; repareren-repareerde-gerepareerd gerepareerd in orde gemaakt

307 rows × 4 columns

All conjugations are gone in the Meanings_trimmed column, great!

Alright! Let’s make lists of these words and their meanings to serve as input for an LLM.

words = df["Words_trimmed"].tolist()
meanings = df["Meanings_trimmed"].tolist()

Step 6: Incorporating an LLM

The scope of this post is not to train or finetune an LLM ourselves, which means we can use any suitable model on the Hugging Face platform. When navigating to the Models page on Hugging Face, we filter the LLMs by selecting Translation as NLP task and Dutch and Chinese as languages. The first LLM sorted by Trending is facebook/nllb-200-distilled-600M and we will try it out.

The modal Use in Transformers is incredibly useful as it displays a copy-able code snippet. The only thing that is missing is the explicit specification of using an NVIDIA GPU, as I am running the code using a NVIDIA GTX 1080 that has 8GB of VRAM.

from transformers import pipeline

pipe = pipeline("translation", model="facebook/nllb-200-distilled-600M", device="cuda:0")

Let’s now define a translate function that translates a list of tokens. Something to take into account is to add the specifications of the src_lang (source language) and tgt_lang (target language) in the pipeline, which we we add as optional arguments in the function. To get a better idea of how long it takes for the translations to finish using 4GB (default) of the GPU, we can return a dictionary with the translations

import time

def translate(tokens, src_lang="nld_Latn", tgt_lang="zho_Hans"):
    start = time.time()
    translation = pipe(tokens, src_lang=src_lang, tgt_lang=tgt_lang)
    end = time.time()
    return {"translation": translation, "time": end-start}

Let’s test the function by translating the English title of this post into Mandarin Chinese.

title = translate("Translating PDF content using a Large Language Model", src_lang="eng_Latn")
title["translation"][0]["translation_text"]
'使用大语言模型翻译PDF内容'

Looks decent to me!

Step 7: Translating a batch of words

The words and meanings variables are ready to be plugged into the translate function. We will add the translations to the existing df and write the dataframe to a csv file.

translations_words_nllb = translate(words)
translations_meanings_nllb = translate(meanings)

df["CN_Words_NLLB"] = [item["translation_text"] for item in translations_words_nllb["translation"]]
df["CN_Meanings_NLLB"] = [item["translation_text"] for item in translations_meanings_nllb["translation"]]

df.to_csv("vocab.csv", sep=";", index=False, encoding="utf-8-sig")
df
Words Meanings Words_trimmed Meanings_trimmed CN_Words_NLLB CN_Meanings_NLLB
0 gevorderden mensen die al een tijd bezig zijn met een stud... gevorderden mensen die al een tijd bezig zijn met een stud... 领先者 那些已经在学习或技能中工作过的,
1 gemeenschappelijk met verschillende mensen dezelfde eigenschap o... gemeenschappelijk met verschillende mensen dezelfde eigenschap o... 共同 具有不同的人的特性或空间
2 raden to guess/erraten/deviner/tahmin etmek; raden-r... raden to guess/erraten/deviner/tahmin etmek 为了猜测/猜测/猜测/推测
3 afvragen jezelf een vraag stellen; afvragen-vroeg af-af... afvragen jezelf een vraag stellen 问问 问自己一个问题
4 stelt … voor dat stelt niks voor = dat is helemaal niet bel... stelt … voor dat stelt niks voor = dat is helemaal niet bel... 代表 没有什么意思.
... ... ... ... ... ... ...
1764 matchen combineren, koppelen, bij elkaar brengen; matc... matchen combineren, koppelen, bij elkaar brengen 匹配 结合,结合,组合
1765 op goed geluk willekeurig, blindelings, zonder planning op goed geluk willekeurig, blindelings, zonder planning 祝你好运 随机,盲目,没有计划
1766 presteren prestaties leveren, werken; presteren-presteer... presteren prestaties leveren, werken 能做到 提供工作,工作
1767 revalideren weer leren bewegen na een ongeluk of operatie;... revalideren weer leren bewegen na een ongeluk of operatie;... 恢复 事故或手术后重新学习运动;
1768 gerepareerd in orde gemaakt; repareren-repareerde-gerepareerd gerepareerd in orde gemaakt 修复 整好了

1769 rows × 6 columns

Download the vocab.csv if you are interested or would like to work with this dataset!

TL;DR

My Mandarin is nowhere near native level, but when quickly skimming the dataset it is evident that the direct translation of some words are not correct. It uses the character 子 in those occasions, which has various meaning and uses in different contexts. Also, the meanings are sometimes oddly translated, as for some words the corresponding meaning is really not that useful.

In general, the direct translations of the words could serve as a potential starting point for my upcoming project. However, a better starting point would probably to find a Dutch-Chinese vocabulary list. An LLM could then be used to explain the words, or find sample sentences to include.

That’s all for today, thanks for reading!

Back to top

Footnotes

  1. You might wonder: “Did you really need to write this as a class? It contains pretty much only methods that could be used statically.”, and I wouldn’t blame you. However I just wanted a quick way to provide a better overview of all the code, and this was my best excuse for it.↩︎

  2. It would also have been possible to crop the page before extracting the text, but to me it seemed like more work experimenting with the dimensions.↩︎

  3. Other translators could return “大型语言” instead of “大语言” and the former is indeed more accurate.↩︎