Skoči na vsebino

Analiza besedil

Uporabo ukaznega jezika lupine pri reševanju problemov s pomočjo superračunalnika bomo prikazali na enem izmed postopkov, ki se pogosto uporablja pri rudarjenju besedil oziroma procesiranju naravnega jezika. Lotili se bomo problema štetja besed: odkriti želimo, kolikokrat se posamezna beseda pojavi v dani zbirki besedil. Pri tem se ne bomo spuščali v podrobnosti, omenimo le, da se tako zgrajena statistika neke zbirke besedil lahko kasneje uporabi pri generiranju naravnega jezika, analizi sentimenta, klasifikaciji besedil, filtriranju neželene pošte in podobno.

Uporabili bomo preprost program, ki zna besedila obdelovati le enega za drugim. S pomočjo ukazne lupine bomo problem razdelili na podprobleme in jih obdelali vzporedno na gruči, brez sprememb v samem programu.

Podatki

Za vhodne podatke bomo uporabili podatkovno bazo uporabniških recenzij filmov na IMDb. Baza ima imeniško strukturo

aclImdb
├── README
├── test
│   ├── neg
│   │   ├── 0_2.txt
│   │   │   …
│   │   └── 9999_1.txt
│   └── pos
│       ├── 0_10.txt
│       │   …
│       └── 9999_10.txt
└── train
    ├── neg
    └── pos

Vsaka datoteka X_Y.txt vsebuje eno recenzijo; X predstavlja zaporedno številko recenzije, Y pa oceno, ki jo je recenzent namenil filmu. Recenzije so razdeljene v mape, ki predstavljajo testno test in učno množico train ter med negativne neg in pozitivne recenzije pos. Vsebino posamezne datoteke lahko izpišemo s programom cat:

$ cat aclImdb/test/neg/4435_1.txt
Legend has it that at the gala Hollywood premiere screening of 2001: A Space Odyssey, about 20 minutes into the film Rock Hudson yelled out "Would somebody please tell me what the hell this movie is about?" […]
cat aclImdb/test/neg/4435_1.txt

Program za štetje besed

Za štetje besed bomo vzeli preprost program count-words.py v Pythonu.

Koda programa count-words.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#!/usr/bin/python3

import argparse
import collections
import sys

import nltk
import nltk.corpus
import nltk.stem
import nltk.tokenize

# call this once before executing this script
# pip3 install --user nltk
# nltk.download('averaged_perceptron_tagger')
# nltk.download('punkt')
# nltk.download('wordnet')

ap = argparse.ArgumentParser()
ap.add_argument('inputs', metavar='INPUT', nargs='*', help='input files')
ap.add_argument('--output', '-o', metavar='OUTPUT', help='output file')
args = vars(ap.parse_args())

# if no inputs are given, read from standard input
if not args['inputs']:
    args['inputs'] = '-'

# prepare stuff
lemmatizer = nltk.stem.WordNetLemmatizer()
tokenizer = nltk.tokenize.RegexpTokenizer("[\w']+")

# return a list with normalized words
def lemmatize(words):
    # NLTK’s tagger and lemmatizer use different part-of-speech tags, this converts
    tags = {
        'J' : nltk.corpus.wordnet.ADJ,
        'R': nltk.corpus.wordnet.ADV,
        'V': nltk.corpus.wordnet.VERB,
    }
    def pos2tag(pos):
        return tags.get(pos[0], nltk.corpus.wordnet.NOUN)

    # add part-of-speech (noun, verb, adjective…) tags to words
    tagged_words = nltk.pos_tag(list(words))
    return (lemmatizer.lemmatize(word, pos2tag(pos)) for word, pos in tagged_words)

# count words in all the files
counts = collections.Counter()
for filename in args['inputs']:
    # special filename - reads from standard input
    with (sys.stdin if filename == '-' else open(filename)) as stream: 
        # read text from file and clean it up: convert to lowercase and remove HTML tags
        text = stream.read().lower().replace('<br />', ' ')

    # split text into words and normalize them
    tokens = tokenizer.tokenize(text)
    words = lemmatize(tokens)

    # add words to counter
    counts.update(words)

# write word counts to output file (or standard output if not given)
output = open(args['output'], 'w') if args['output'] else sys.stdout
for word, count in counts.most_common():
    print(f'{word} {count}', file=output)
Izvorna datoteka: count-words.py

Podrobnosti delovanja programa nas tu ne zanimajo. V splošnem niti ni nujno, da imamo dostop do izvorne kode programov, ki jih uporabljamo za obdelavo naših podatkov, ali pa je ta preveč kompleksna. Zato bomo tudi program count-words obravnavali kot nespremenljivo črno škatlo, ki jo bomo s pomočjo ukazne lupine poskušali uporabiti na način, ki ga pisec programa ni predvidel.

Kot večina programov tudi count-words ni popolnoma samostojen, temveč se naslanja na obstoječe programe in knjižnice. Kot skripta seveda potrebuje Python za izvajanje. Poleg tega za razčlenjevanje besedila in lematizacijo posameznih besed uporablja uveljavljeno knjižnico za delo z naravnim jezikom NLTK.

Okolje

V kasnejših poglavjih si bomo ogledali bolj robustne metode za distribucijo programske opreme, z vsebniki. V tem poglavju pa bomo uporabili kar standardno različico tolmača za Python, ki je v večini sistemov – tudi na gruči – že nameščen. Poleg samega tolmača moramo namestiti še modul NLTK, kar lahko naredimo na dva načina.

Možnost 1: pip

Python ima svojo uradno zbirko dodatnih modulov Python Package Index. Paket nltk namestimo z ukazom

$ pip3 install --user nltk
Collecting nltk
[…]
Installing collected packages: regex, tqdm, click, joblib, nltk
Successfully installed click-7.1.2 joblib-1.0.1 nltk-3.6.1 regex-2021.4.4 tqdm-4.60.0
pip3 install --user nltk

Vidimo, da pip3 poleg NLTK samodejno namesti še nekaj drugih paketov, ki jih ta rabi za delovanje. Zastavica --user poskrbi, da pip namesti module v naš domači imenik. Brez nje bi jih poskusil namestiti v sistemski imenik za vse uporabnike, kar mu brez skrbniškega dostopa ne bi uspelo. Tako nameščeni moduli se nahajajo v imeniku ~/.local/lib/pythonX.Y1.

Nameščeni moduli so na voljo tako na prijavnem kot na računskih vozliščih. NLTK za delo poleg programske kode, ki smo jo pravkar namestili, potrebuje še nekaj zbirk podatkov, ki jih prenesemo s pomočjo modula nltk.downloader:

$ python3 -m nltk.downloader averaged_perceptron_tagger punkt wordnet
[nltk_data] Downloading package averaged_perceptron_tagger to
[nltk_data]     $HOME/nltk_data...
[nltk_data]   Unzipping taggers/averaged_perceptron_tagger.zip.
[…]
python3 -m nltk.downloader averaged_perceptron_tagger punkt wordnet

Možnost 2: conda

Namesto pip lahko za nameščanje knjižnic uporabimo tudi distribucijo conda, ki je del priljubljenega okolja za znanstveno delo Anaconda in poleg Pythona podpira tudi druge jezike. Na gruči privzeto ni nameščena, a je na voljo kot okoljski modul. Za začetek ga naložimo z

$ module load Anaconda3
module load Anaconda3

Ker conda ni nameščena sistemsko, temveč le za našega uporabnika, moramo pred uporabo sami nastaviti okolje z ukazom

$ source $(conda info --base)/etc/profile.d/conda.sh
source $(conda info --base)/etc/profile.d/conda.sh

Namig

Ta ukaz moramo pognati vsakič, ko se prijavimo v gručo. Alternativno ga lahko zapišemo na konec datoteke ~/.bashrc z echo source $(conda info --base)/etc/profile.d/conda.sh >> ~/.bashrc.

Nato ustvarimo in aktiviramo nov projekt conda kot običajno:

$ conda create --name moj-projekt nltk
$ conda activate moj-projekt
conda create --name moj-projekt nltk
conda activate moj-projekt

Kot pri prvi možnosti bo s tem NLTK na voljo tako na prijavnem kot na računskih vozliščih. Če še nismo, zdaj prenesemo še potrebne podatke z modulom nltk.downloader tako kot zgoraj.

Moduli ostanejo naloženi, dokler se ne odjavimo, zato jih moramo ob vsaki prijavi vnovič naložiti. Namesto tega lahko namestitev opravimo kar znotraj našega posla tako, da na začetek skripte za sbatch dodamo vrstice

module load Anaconda3
source $(conda info --base)/etc/profile.d/conda.sh
conda activate moj-projekt

Tako zagotovimo, da je ustrezno okolje vzpostavljeno vsakič, ko poženemo posel. Pred tem moramo moj-projekt ustvariti z ukazom conda create, kot zgoraj.

Preveri svoje znanje

Vaja

Vaja 01: pripravi podatke in okolje po navodilih.


  1. Znak ~ označuje domači imenik trenutnega uporabnika.