Regułowy System ekspercki / Rule-driven expert system

Artur Protasewicz
  • Uwaga:*
  • Jeśli nie chcesz tego czytać, a zobaczyć, jak to działa, pobierz pełny spakowany (.zip) projekt C# z kodem źródłowym, *
  • klikając tutaj ExpertSystem.zip //
    Treść udostępniona na zasadach licencji Creative Commons Attribution
  • Autor uznaje również załączony projekt C# za integralną część poniższego artykułu *
  • i rezerwuje sobie prawo do modyfikacji tego projektu na rzecz poprawienia jego jakości *
  • bez dokonywania zmian w poniższym artykule, w szczególności bez informowania czytelników o tym fakcie. *

  • Note:*
  • If you do not want to read it, but you want to see how it works, please, download full zipped C# project with enclosed source code *
  • by clicking here ExpertSystem.zip //
    Content shared under license Creative Commons Attribution
  • Author also considers enclosed C# project as integral part of article below, *
  • and reserves himself the right to modify this project for purposes of increasing its quality *
  • without making changes to the rest of article, especially without informing readers about this fact. *

Wstęp / Introduction

W tym temacie pokazuję jak rozpocząć budowę systemu eksperckiego. | In this topic I show how to start building an expert system.
System ekspercki zawiera wiedzę ekspertów z jednej wąskiej dziedziny. Wiedza ta jest umieszczona w bazie wiedzy w postaci sformatowanej. | Expert system provides expert knowledge in one small area. This knowledge is contained in a knowledge base in formatted form.
Wiedzę ekspertów do systemu eksperckiego wprowadza inżynier wiedzy, który z jednej strony częściowo zna dziedzinę reprezentowaną przez ekspertów, a z drugiej strony potrafi ją sformatować do zapisów z dziedziny logiki. | Knowledge engineer introduces expert knowledge into an expert system. He is on the one hand partially familiar with the field represented by the experts, on the other hand is able to format it in the field of logic.
System ekspercki jest programem, któremu zadajemy pytania i od którego otrzymujemy odpowiedzi. Jeżeli odpowiedzi udzielone przez program są identyczne z odpowiedziami, jakich udzieliliby eksperci (w 70-80 lub więcej procentach), system uznaje się za działający prawidłowo. | Expert system is a program which we ask questions and get answers from it. If the answers given by the program are the same answers, which experts ones are (of 70-80 percent or more), the system is considered running properly.
Ostateczną decyzję podejmuje człowiek, a system ekspercki mu tylko pomaga. | The final decision is made by man, and an expert system only helps him.
Ważną cechą systemu eksperckiego jest udzielanie odpowiedzi na pytanie dlaczego podjął taką a nie inną decyzję. Pozwala to na weryfikację poprawności odpowiedzi systemu. Mechanizm uzasadniania odpowiedzi warto zaimplementować już na początku tworzenia systemu. To ułatwi testowanie poprawności wnioskowania. | An important feature of an expert system is to answer the question why it made this decision not a different one. This allows the verification of the correctness of system responses. We implement mechanism to justify the response at the beginning of the creation of the system. This facilitates testing the correctness of reasoning.
Składnikami systemu eksperckiego są: baza wiedzy złożona z bazy faktów i bazy reguł, mechanizmy wnioskowania, mechanizmy uzasadniania odpowiedzi oraz baza danych. | Components of an expert system are: a knowledge base consisting of a database of facts and database of rules, reasoning mechanism, mechanism of responses to question why.
Systemy eksperckie znalazły największe zastosowanie w medycynie i ekonomii. Pokazuję działanie systemu eksperckiego na prostym i zrozumiałym przykładzie szukania kandydata na męża dla Alicji. Przykład z medycyny lub ekonomii nie byłby dla wszystkich zrozumiały, dlatego wybrałem coś, co każdy zna z życia. | Expert systems have found greatest use in medicine and economics. I show the action of an expert system on a simple and understandable example of finding a candidate for a husband for Alice. An example from medicine or economics would not be to understand for everyone why I chose something that everyone knows from life.
Mój system jest sterowany regułami to znaczy, że reguły decydują o wnioskowaniu. Można spotkać systemy eksperckie zawierające tylko mechanizmy wnioskowania i pustą bazę wiedzy. Są to systemy szkieletowe, gotowe do przyjęcia wiedzy z dowolnej dziedziny. | My system is controlled by rules (rule-driven) that mean that the rules affect the reasoning. There can be found expert systems containing only reasoning mechanism and an empty knowledge base.
W przykładzie przyjąłem następujące założenia: Alicja jest heteroseksualna, mówimy o papieżu rzymsko-katolickim, jest rok 2013 (przed 28 lutego, kiedy to Benedykt XVI oficjalnie abdykował). | In this example, I assume that: Alice is heterosexual, we are talking about the Roman Catholic Pope, the year is 2013 (before February 28, when Benedict XVI officially abdicated).

Dodatkowe artykuły na tym portalu (tylko po polsku) / Additional articles at this site (Polish only)

Metody i Style Zarządzania, LOSMARCELOS, 4programmers.net
Ekstrakcja reguł z danych / Extraction of rules from data, Artur Protasewicz, 4programmers.net

Baza danych i postać reguły Database and form of rule
Fakty i reguły tworzące bazę wiedzy będziemy przechowywać w bazie danych. Moja baza danych nie zawiera kluczy. Nie jest to przedmiotem tego artykułu. Zakładam, że je stworzysz. Facts and rules for creating a knowledge base will be stored in a database. My database does not contain primary keys. This is not the subject of this article. I assume that you will create them.
Jako dodatek znajdziesz w kodzie na końcu funkcje odczytujące i zapisujące dane do bazy danych. As a supplement you can find in the code at the end reading and inserting functions for the database.
Najpierw pokazuję poniżej jaką sformatowaną postać reguły przyjąłem, a potem ogólne postaci reguł stosowane zwykle w systemach eksperckich. First, I show below a formatted form of rule I use, then the general form of rules typically used in expert systems.
Moja postać reguły My form of rule
wniosek := (faktA and or faktB) and (wniosekX and or wniosekY) conclusion := (factA and or factB) and (conclusionX and or conclusionY)

Obrazek1-1.jpg Obrazek1-2.jpg

Obrazek 1

|

Figure 1

---------------- | ----------------

Tabele bazy danych

|

Database tables

Stosowane postaci reguł

wniosekA or wniosekB or ... or wniosekM := [not] fakt1|wniosek1 and [not] fakt2|wniosek2 and ... and [not] faktN|wniosekN
wniosekA and wniosekB and ... and wniosekM := [not] fakt1|wniosek1 or [not] fakt2|wniosek2 or ... or [not] faktN|wniosekN
Used forms of rules

conclusionA or conclusionB or ... or conclusionM := [not] fact1|conclusion1 and [not] fact2|conclusion2 and ... and [not] factN|conclusionN
conclusionA and conclusionB and ... and conclusionM := [not] fact1|conclusion1 or [not] fact2|conclusion2 or ... or [not] factN|conclusionN

Operatory AND, OR AND, OR operators
Aby poprawnie wprowadzić reguły, musisz wiedzieć, co to jest relacja AND i relacja OR. Obrazują to poniższe tabele dla dwóch parametrów relacji. To correctly implement the rules, you need to know what are the AND and OR relationships. The following tables illustrate them for two parameters.
Zakładam, że wiesz, co to jest zaprzeczenie NOT (0 daje 1, 1 daje 0). I assume you know what it is the negation NOT (0 gives 1, 1 gives 0).
Liczba 0 w tabeli oznacza wartość false, liczba 1 wartość true. Number 0 in the table means false, number 1 true.
Możesz rozbudować moją postać reguły tak, aby zawierała więcej parametrów np. You can extend my form of a rule to include more parameters such as:
wynik = (x1 and (x2 and (x3 and x4))) lub wynik = (x1 or (x2 or (x3 or x4))) result = (x1 and (x2 and (x3 and x4))) or result = (x1 or (x2 or (x3 or x4)))
Mój kod operuje na dwóch parametrach relacji, ale jeżeli zrobisz jak powyżej, nic w nim nie będziesz musiał zmieniać oprócz treści uzasadnień, bo nadal maszyna wnioskująca będzie działać prawidłowo. My code operates on two parameters of the relationship, but if you do as above, you will have nothing to change except justifying texts, because the reasoning machine will continue to work properly.

AND.jpg

Tabela 1

|

Table 1

---------------- | ----------------

Operator AND

|

AND operator

OR.jpg

Tabela 2

|

Table 2

---------------- | ----------------

Operator AND

|

AND operator

Przykład / Example

Słownik / Vocabulary

Polski English
jest kobietą is woman
lubi grać na gitarze likes play guitar
lubi grać w brydża likes play bridge
jest papieżem is pope
jest mężczyzną is man
lubi grać na pianinie likes play piano
lubi hazard likes gambling
mogą być przyjaciółmi can be friends
lubi muzykę likes music
może poślubić can marry
nie może poślubić cannot marry
lubi grać w karty likes play cards
lubi pasjansa likes solitaire
wniosek conclusion
jest prawdą is true
nie jest prawdą is false
kto who
dlaczego why
fakty facts
reguły rules
obiekt object
atrybut, cecha attribute
kto może poślubić Alicję? who can marry Alice?
odpowiedź answer
fakt fact
dlaczego obiekt może poślubić Alicję? why object can marry Alice?
dlaczego obiekt nie może poślubić Alicji? why object cannot marry Alice?
Cel poszukiwań Search goal
Szukając osoby, która może poślubić Alicję musimy wziąć pod uwagę regułę "może poślubić" i jej zaprzeczenie "nie może poślubić", a następnie stworzyć iloczyn warunków: Searching for a person who can marry Alice, we must consider the rule "can marry" and its denial "cannot marry" and then create a product of conditions:
cel = "może poślubić" and not "nie może poślubić" goal = "can marry" and not "cannot marry"

Obrazek2.jpg

Obrazek 2

|

Figure 2

---------------- | ----------------

Baza wiedzy i odpowiedź na pytanie "Kogo może poślubić Alicja?

|

Knowledge base and the answer to the question "Who can marry Alice?"

Obrazek3.jpg

Obrazek 3

|

Figure 3

---------------- | ----------------

Uzasadnienie odpowiedzi

|

Answer justification

Kod programu - C# / Program code C

Założenie wstępne Pre-assumption
Kod programu wymaga, abyś stworzył formę, odpowiednio ponazywał kontrolki i dodał do gridów odpowiednie kolumny. Zakładam, że korzystasz z SQL Server Express i stworzyłeś bazę AI_KB2. The code of the program requires you to create form, appropriately named controls and add to the grids corresponding columns. I assume that you are using SQL Server Express and you created a database AI_KB2.
Aby zmienić fakty i reguły, należy wpisać teksty bezpośrednio do grida, choć możesz sobie rozbudować aplikację i robić to w bardziej elegancki sposób. To change facts and rules, type text directly into the grid, but you can upgrade the app and do it in a more elegant way.

UWAGA: Reguły nie mogą zawierać odwołań cyklicznych. / WARNING: Rules shall not be recursive.

using System;
using System.Data;
using System.Windows.Forms;
using System.Data.SqlClient;
using System.Collections;
 
namespace Expert
{
    public partial class FormExpert : Form
    {
        String ConnStr = "Data Source=.\\SQLEXPRESS;Integrated Security=True;
          Initial Catalog=AI_KB2;";
        int orCol = 0;
        int conclusionCol = 1;
        int bracketLeftCol = 2;
        int factACol = 3;
        int abRelationCol = 4;
        int factBCol = 5;
        int andCol = 6;
        int conclusionXCol = 7;
        int xyRelationCol = 8;
        int conclusionYCol = 9;
        int bracketRightCol = 10;
        int objectCol = 0;
        int attributeCol = 1;
        int FactCount;
        int ConclusionCount;
        int Nested;
        RichTextBox why;
        public FormExpert()
        {
            InitializeComponent();
        }
        bool Is(String Conclusion, String Candidate)
        {
            if (Conclusion == "can marry")
            {
                why = WhyYes;
            }
            if (Conclusion == "cannot marry")
            {
                why = WhyNot;
            }
            Nested++;
            for (int ConclusionIndex = 0; ConclusionIndex < ConclusionCount;
              ConclusionIndex++)
            {
                if (Conclusions(conclusionCol, ConclusionIndex) == Conclusion)
                {
                    String sFact = "";
                    String sConclusion = "";
                    String s = "";
                    String FactA = Conclusions(factACol, ConclusionIndex);
                    String FactB = Conclusions(factBCol, ConclusionIndex);
                    String ConclusionX = Conclusions(conclusionXCol,
                      ConclusionIndex);
                    String ConclusionY = Conclusions(conclusionYCol,
                      ConclusionIndex);
                    bool resultORFacts = false;
                    bool resultORConclusions = false;
                    bool resultANDFacts = false;
                    bool resultANDConclusions = false;
                    if (Conclusions(abRelationCol, ConclusionIndex) == "and")
                    {
                        sFact += "(" + FactA + " and " + FactB + ")";
                    }
                    if (Conclusions(abRelationCol, ConclusionIndex) == "or")
                    {
                        sFact += "(" + FactA + " or " + FactB + ")";
                    }
                    if (Conclusions(xyRelationCol, ConclusionIndex) == "and")
                    {
                        sConclusion += "(" + ConclusionX + " and "
                          + ConclusionY + ")";
                    }
                    if (Conclusions(xyRelationCol, ConclusionIndex) == "or")
                    {
                        sConclusion += "(" + ConclusionX + " or "
                          + ConclusionY + ")";
                    }
                    s = sFact + " and " + sConclusion;
                    WhyPath(Conclusion + " if " + s);
                    if (Conclusions(abRelationCol, ConclusionIndex) == "and")
                    {
                        resultANDFacts = (IsFactA(ConclusionIndex, Candidate)
                          && IsFactB(ConclusionIndex, Candidate));
                    }
                    if (Conclusions(abRelationCol, ConclusionIndex) == "or")
                    {
                        resultORFacts = (IsFactA(ConclusionIndex, Candidate)
                          || IsFactB(ConclusionIndex, Candidate));
                    }
                    if (Conclusions(xyRelationCol, ConclusionIndex) == "and")
                    {
                        resultANDConclusions = (IsConclusionX(ConclusionIndex,
                          Candidate) && IsConclusionY(ConclusionIndex,
                          Candidate));
                    }
                    if (Conclusions(xyRelationCol, ConclusionIndex) == "or")
                    {
                        resultORConclusions = (IsConclusionX(ConclusionIndex,
                          Candidate) || IsConclusionY(ConclusionIndex,
                          Candidate));
                    }
                    bool result;
                    result = (resultANDFacts && resultANDConclusions);
                    result = (result || (resultANDFacts
                      && resultORConclusions));
                    result = (result || (resultORFacts
                      && resultANDConclusions));
                    result = (result || (resultORFacts && resultORConclusions));
 
                    if (result)
                    {
                        WhyPath("conclusion " + Conclusion + " is true");
                        Nested--;
                        return true;
                    }
                }
            }
            WhyPath("conclusion " + Conclusion + " is false");
            Nested--;
            return false;
        }
        bool IsFactA(int ConclusionIndex, String Candidate)
        {
            String FactA = Conclusions(factACol, ConclusionIndex);
            if (FactA == "1")
            {
                return true;
            }
            if (FactA == "0")
            {
                return false;
            }
            Nested++;
            for (int iFact = 0; iFact < FactCount; iFact++)
            {
                if (Facts(objectCol, iFact) == Candidate && Facts(attributeCol,
                  iFact) == FactA)
                {
                    WhyPath("fact " + FactA + " is true for " + Candidate);
                    Nested--;
                    return true;
                }
            }
            WhyPath("fact " + FactA + " is false for " + Candidate);
            Nested--;
            return false;
        }
        bool IsFactB(int ConclusionIndex, String Candidate)
        {
            String FactB = Conclusions(factBCol, ConclusionIndex);
            if (FactB == "1")
            {
                return true;
            }
            if (FactB == "0")
            {
                return false;
            }
            Nested++;
            for (int iFact = 0; iFact < FactCount; iFact++)
            {
                if (Facts(objectCol, iFact) == Candidate && Facts(attributeCol,
                  iFact) == FactB)
                {
                    WhyPath("fact " + FactB + " is true for " + Candidate);
                    Nested--;
                    return true;
                }
            }
            WhyPath("fact " + FactB + " is false for " + Candidate);
            Nested--;
            return false;
        }
        bool IsConclusionX(int ConclusionIndex, String Candidate)
        {
            String ConclusionX = Conclusions(conclusionXCol, ConclusionIndex);
            if (ConclusionX == "1")
            {
                return true;
            }
            if (ConclusionX == "0")
            {
                return false;
            }
            return Is(ConclusionX, Candidate);
        }
        bool IsConclusionY(int ConclusionIndex, String Candidate)
        {
            String ConclusionY = Conclusions(conclusionYCol, ConclusionIndex);
            if (ConclusionY == "1")
            {
                return true;
            }
            if (ConclusionY == "0")
            {
                return false;
            }
            return Is(ConclusionY, Candidate);
        }
        ArrayList SolveWhoCanMarryAlice()
        {
            ArrayList result = new ArrayList();
            ArrayList Persons = new ArrayList();
            WhyYes.Clear();
            WhyNot.Clear();
            for (int iFact = 0; iFact < FactCount; iFact++)
            {
                String Person = Facts(objectCol, iFact);
                if (Person != "Alice" && Persons.IndexOf(Person) < 0)
                {
                    Persons.Add(Person);
                }
            }
            foreach (String Person in Persons)
            {
                Nested = -1;
                if (Is("can marry", Person) && !Is("cannot marry", Person))
                {
                    result.Add(Person);
                }
            }
            return result;
        }
        private void btnWhoCanMarryAlice_Click(object sender, EventArgs e)
        {
            Answer.Text = "";
            foreach (String Person in SolveWhoCanMarryAlice())
            {
                Answer.Text += Person + Environment.NewLine;
            }
        }
        void WhyPath(String Text)
        {
            why.Text += strNested() + Text + Environment.NewLine;
        }
        String Facts(int Column, int Row)
        {
            return (String)grdFacts[Column, Row].Value;
        }
        String Conclusions(int Column, int Row)
        {
            return (String)grdConclusions[Column, Row].Value;
        }
        String strNested()
        {
            String s = "";
            for (int j = 0; j < Nested; j++)
            {
                for (int i = 0; i < 10; i++)
                {
                    s += " ";
                }
            }
            return s;
        }
        void LoadConclusions()
        {
            SqlConnection Conn = new SqlConnection(ConnStr);
            Conn.Open();
            SqlCommand CmdSelectConclusions = new SqlCommand("select * from
              Rules");
            CmdSelectConclusions.Connection = Conn;
            SqlDataReader readerConclusions
              = CmdSelectConclusions.ExecuteReader();
            int row = 0;
            ConclusionCount = 0;
            grdConclusions.Rows.Clear();
            while (readerConclusions.Read())
            {
                grdConclusions.Rows.Add();
                ConclusionCount++;
                grdConclusions[conclusionCol, row].Value = readerConclusions[0];
                grdConclusions[factACol, row].Value = readerConclusions[1];
                grdConclusions[abRelationCol, row].Value = readerConclusions[2];
                grdConclusions[factBCol, row].Value = readerConclusions[3];
                grdConclusions[conclusionXCol, row].Value = readerConclusions[4]
                  ;
                grdConclusions[xyRelationCol, row].Value = readerConclusions[5]
                  ;
                grdConclusions[conclusionYCol, row].Value = readerConclusions[6]
                  ;
                if (row == 0)
                {
                    grdConclusions[orCol, row].Value = "";
                }
                else
                {
                    grdConclusions[orCol, row].Value = "or";
                }
                grdConclusions[bracketLeftCol, row].Value = "(";
                grdConclusions[andCol, row].Value = ") and (";
                grdConclusions[bracketRightCol, row].Value = ")";
                row++;
            }
            readerConclusions.Close();
            Conn.Close();
        }
        void LoadFacts()
        {
            SqlConnection Conn = new SqlConnection(ConnStr);
            Conn.Open();
            SqlCommand CmdSelectFacts = new SqlCommand("select * from Facts");
            CmdSelectFacts.Connection = Conn;
            SqlDataReader readerFacts = CmdSelectFacts.ExecuteReader();
            int row = 0;
            FactCount = 0;
            grdFacts.Rows.Clear();
            while (readerFacts.Read())
            {
                grdFacts.Rows.Add();
                FactCount++;
                for (int col = 0; col < grdFacts.ColumnCount; col++)
                {
                    grdFacts[col, row].Value = readerFacts[col];
                }
                row++;
            }
            readerFacts.Close();
            Conn.Close();
        }
        void SaveConclusions()
       {
            SqlConnection Conn = new SqlConnection(ConnStr);
            Conn.Open();
            SqlCommand CmdDeleteAllConclusions = new SqlCommand("delete from
              Rules");
            CmdDeleteAllConclusions.Connection = Conn;
            CmdDeleteAllConclusions.ExecuteNonQuery();
            SqlCommand CmdInsertConclusion = 
              new SqlCommand("insert Rules (Conclusion, FactA, abRelation,
                FactB, ConclusionX, xyRelation, ConclusionY) " +
                "values (@par0, @par1, @par2, @par3, @par4, @par5, @par6)");
            CmdInsertConclusion.Connection = Conn;
            for (int par = 0; par < 7; par++)
            {
                CmdInsertConclusion.Parameters.Add("@par" + par.ToString(),
                  SqlDbType.Text);
            }
            for (int row = 0; row < grdConclusions.RowCount; row++)
            {
                CmdInsertConclusion.Parameters[0].Value
                  = grdConclusions[conclusionCol, row].Value;
                CmdInsertConclusion.Parameters[1].Value
                  = grdConclusions[factACol, row].Value;
                CmdInsertConclusion.Parameters[2].Value
                  = grdConclusions[abRelationCol, row].Value;
                CmdInsertConclusion.Parameters[3].Value
                  = grdConclusions[factBCol, row].Value;
                CmdInsertConclusion.Parameters[4].Value
                  = grdConclusions[conclusionXCol, row].Value;
                CmdInsertConclusion.Parameters[5].Value
                  = grdConclusions[xyRelationCol, row].Value;
                CmdInsertConclusion.Parameters[6].Value
                  = grdConclusions[conclusionYCol, row].Value;
                try
                {
                    CmdInsertConclusion.ExecuteNonQuery();
                }
                catch { }
            }
            Conn.Close();
        }
        void SaveFacts()
        {
            SqlConnection Conn = new SqlConnection(ConnStr);
            Conn.Open();
            SqlCommand CmdDeleteAllFacts = new SqlCommand("delete from Facts");
            CmdDeleteAllFacts.Connection = Conn;
            CmdDeleteAllFacts.ExecuteNonQuery();
            SqlCommand CmdInsertFact = new SqlCommand("insert Facts (Object,
              Attribute) values (@par0, @par1)");
            CmdInsertFact.Connection = Conn;
            for (int par = 0; par < 2; par++)
            {
                CmdInsertFact.Parameters.Add("@par" + par.ToString(),
                  SqlDbType.Text);
            }
            for (int row = 0; row < grdFacts.RowCount; row++)
            {
                for (int par = 0; par < grdFacts.ColumnCount; par++)
                {
                    CmdInsertFact.Parameters[par].Value
                      = grdFacts[par, row].Value;
                }
                try
                {
                    CmdInsertFact.ExecuteNonQuery();
                }
                catch { }
            }
            Conn.Close();
        }
        private void btnLoadData_Click(object sender, EventArgs e)
        {
            LoadFacts();
            LoadConclusions();
        }
        private void btnSaveData_Click(object sender, EventArgs e)
        {
            SaveFacts();
            SaveConclusions();
        }
    }
}

Zrzuty ekranu - uruchomiony projekt zlokalizowany na polski / Screenshots - running project localized to Polish

001_zrzut_przyciski_pl.jpg

002_zrzut_fakty_pl.jpg

003_zrzut_reguly_pl.jpg

004_zrzut_odpowiedzi_pl.jpg

005_zrzut_dlaczego_tak_pl.jpg

006_zrzut_dlaczego_nie_pl.jpg

Prototypowa wersja projektu (pełna) do pobrania / Prototype version of the project (full) to download

ExpertSystem.zip

4 komentarzy

Bardzo ciekawy artykuł, pozdrawiam.

Szacun za artykuł. Temat ciekawy i wykonanie również :)

Bardzo ciekawy ;) Chociaż ze względu na osoby, które chcą się nauczyć języka czy bardziej podszkolić się bardziej podzieliłbym zwarty tekst na pojedyncze paragrafy zawierające maksymalnie trzy, cztery zdania. Wiem, że to może by trochę rozbiło jedność tego artykułu, ale byłoby bardziej czytelne/przydatne dla osób, które chcą w ten sposób poduczyć się takiego języka...

Bardzo ciekawy pomysł z dwoma językami.