from pypdf import PdfReader
= PdfReader("vocab.pdf") reader
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
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:
= reader.pages[0]
first_page = first_page.extract_text()
text_first_page 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
- the first word as the index;
- the second word as the word in the vocabulary list;
- 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.
= text_first_page.splitlines()[:9]
lines 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.
= first_page.extract_text(extraction_mode="layout")
text_first_page = text_first_page.splitlines()[:9]
lines 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:
- Filter the lines which are non-empty and start with a digit.
- Aggregate the rows which belong to one word/meaning combination.
- Split each line by at least three or more whitespace characters.
- Define the first part as the word and define the remaining text as its meaning.
- Create a Pandas DataFrame object for each page.
- 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 bits1.
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()]
1def remove_overflow_lines(self, lines):
= [lines[0]]
res for current_item, next_item in zip(lines, lines[1:]):
if next_item[0].isdigit():
res.append(next_item)else:
-1] += next_item
res[return res
2def trim_index(self, lines):
= [line[line.find(' '):] for line in lines]
no_index_lines return [line.strip() for line in no_index_lines]
def lines_to_df(self, lines):
= [line.split(" ") for line in lines]
split_lines = [line[0] for line in split_lines]
words = [''.join(line[1:]).strip() for line in split_lines]
meanings return pd.DataFrame.from_dict({"Words": words, "Meanings": meanings})
def pipeline_lines(self, text):
= text.splitlines()
lines = self.validate_lines(lines)
page_lines = self.remove_overflow_lines(page_lines)
no_overflow_lines = self.trim_index(no_overflow_lines)
no_index_lines return self.lines_to_df(no_index_lines)
3def extract_from_pdf(self):
= PdfReader(self.pdf_path)
reader = reader.pages
pages
= []
res
for page in pages:
= page.extract_text(extraction_mode="layout")
page_text self.pipeline_lines(page_text))
res.append(
= pd.concat(res, ignore_index=True)
df = df[df["Words"] != "Derde Ronde Nederlands voor buitenlanders"].reset_index(
df =True)
drop
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.2
Now we can run the following code:
= VocabExtractor("vocab.pdf")
vocab_extractor = vocab_extractor.extract_from_pdf()
df 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
- Dutch articles could potentially add noise to the context of the word and there are no direct translation for these articles;
- 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]+
.
"Words_trimmed"] = df["Words"].replace(regex=r'\s\((de|het)\)', value='')
df["Meanings_trimmed"] = df["Meanings"] \
df[=r';\s*[\w\s]+-[\w\s]+-[\w\s]+', value='') \
.replace(regex=r'\((de|het)\)', value='') \
.replace(regexstr.strip() .
"Words_trimmed"] != df["Words"]] df[df[
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!
"Meanings_trimmed"] != df["Meanings"]] df[df[
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.
= df["Words_trimmed"].tolist()
words = df["Meanings_trimmed"].tolist() meanings
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
= pipeline("translation", model="facebook/nllb-200-distilled-600M", device="cuda:0") pipe
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"):
= time.time()
start = pipe(tokens, src_lang=src_lang, tgt_lang=tgt_lang)
translation = time.time()
end return {"translation": translation, "time": end-start}
Let’s test the function by translating the English title of this post into Mandarin Chinese.
= translate("Translating PDF content using a Large Language Model", src_lang="eng_Latn")
title "translation"][0]["translation_text"] title[
'使用大语言模型翻译PDF内容'
Looks decent3 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.
= translate(words)
translations_words_nllb = translate(meanings)
translations_meanings_nllb
"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[
"vocab.csv", sep=";", index=False, encoding="utf-8-sig")
df.to_csv( 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!
Footnotes
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.↩︎
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.↩︎
Other translators could return “大型语言” instead of “大语言” and the former is indeed more accurate.↩︎