It is about conditional independence

I have been contemplating the reason why learning the weights of ER inference rules separately or jointly, essentially leads to the same weights given the same ground data (evidence and ground truth) as shown in the slides today. To understand the reason behind this I had to dig back to my notes when I was still learning about Markov Networks back in February 2014. While stratification might play a part in this, the actual reason is that the query predicates in my rules are independent from each other given the evidence predicates in the ground Markov Networks. As you recall the ER inference rules has the following form:

Evid1(X) ^ Evid2(X) ^ Evid3(X) => QUERY1(X)

Evid1(X) ^ !Evid2(X) ^ Evid4(X) => QUERY2(X)

Given a domain of values for X, in the ground Markov Network paths from nodes of QUERY1(X) are always conditionally independent from nodes of QUERY2(X) given the nodes of Evid(X), Evid2(X), Evid3(X), and Evid4(X). As a result of this, the ground atoms of QUERY1 and QUERY2 never appear together in the same potential function in the factorized probability density function. Thus the optimization process always optimizes the weights for such rules independently from each other. And as a consequence of this weights are always the same whether they learned together or individually.

On dbpedia-owl vs. dbprop namespaces

If you know about DBPedia then you will know that properties in DBPedia resources have two namespaces:  dbpedia-owl vs. dbprop. The dbprop name space is for properties extracted from the infobox directly.  The dbpedia-owl name space is for mapped properties.  If you are looking for consistent data in your project, then you should consider the use of dbpedia-owl name space properties.

Reference:

http://wiki.dbpedia.org/faq

Apache Derby Demo

I was recently looking for a some tutorial on using Embedded Apache Derby (JavaDB). The best example I found was provided by Wayne Pollock and it can be found here https://wpollock.com/AJava/DerbyDemo.htm. Here is the code on that page;

import java.sql.*;
import javax.swing.JOptionPane;

public class DerbyDemo {

  public static void main ( String [] args ) {
      
      System.out.println(System.getProperty("user.dir"));

     String driver = "org.apache.derby.jdbc.EmbeddedDriver";
     String dbName="DerbyDemoDB";
     String connectionURL = "jdbc:derby:" + dbName + ";create=true";
       // The ";create=true" will create the DB if not created yet.

     String SQL_CreateTable = "create table addresses ( "
      + "ID     int not null generated always as identity "
      + "       (start with 1000), "
      + "lname  varchar(40) not null, fname varchar(40) not null, "
      + "phone  varchar(14), notes varchar(256), "
      + "primary key (ID) )";

     // This SQL inserts three records into the addresses table:
     String SQL_Insert = "insert into addresses "
      + "(lname, fname, phone, notes) values "
      + "('Pollock', 'Wayne', '253-7213', 'Professor'), "
      + "('Piffl', 'Hymie', NULL, 'Fake student name'), "
      + "('Jojo', 'Mojo', NULL, 'Super-villan')";

     String SQL_Query = "SELECT * FROM addresses";

/*
    // Load the Derby Embedded DB driver into the JRE:
    // Note this should not be needed for Java >=6, it is automatic!
    try { new org.apache.derby.jdbc.EmbeddedDriver();
    } catch ( Exception e ) {
       System.out.println( "**** Cannot load Derby Embedded DB driver!" );
       return;
    }
*/
    Connection con = null;
    Statement stmnt = null;

    // Try to connect to the DB:
    try {
      con = DriverManager.getConnection( connectionURL );
    } catch ( Exception e ) {
        System.err.println( "**** Cannot open connection to "
          + dbName + "!" );
        System.exit(1);
    }

    // Drop (delete) the table if it exists.  This is common for demo code,
    // otherwise every time you run the code, it keeps adding copies of the
    // data.  Current versions of Derby throw an Exception if you try to drop
    // a non-existing table, so check if it is there first:

    if ( tableExists( con, "addresses" ) )  {
      System.out.println ( "Dropping table addresses..." );
        try {
            stmnt = con.createStatement();
            stmnt.executeUpdate( "DROP TABLE addresses" );
            stmnt.close();
        } catch ( SQLException e ) {
            String theError = e.getSQLState();
            System.out.println( "Can't drop table: " + theError );
            System.exit(1);
        }
    }

    // Create the table addresses if it doesn't exist:
    if ( ! tableExists( con, "addresses" ) )  {
      System.out.println ( "Creating table addresses..." );
      try {
        stmnt = con.createStatement();
        stmnt.execute( SQL_CreateTable );
        stmnt.close();
      } catch ( SQLException e ) {
        String theError = e.getSQLState();
        System.out.println( "Can't create table: " + theError );
        System.exit(1);
      }
   }

    // Insert records into table (Note if you run this code twice
    // the same people get added but with different IDs):
    try {
      stmnt = con.createStatement();
      System.out.println ( "Inserting rows into table addresses..." );
      stmnt.executeUpdate( SQL_Insert );  // Add some rows
      stmnt.close();
    } catch ( SQLException e ) {
        String theError = e.getSQLState();
        System.out.println( "Can't insert rows in table: " + theError );
        System.exit(1);
    }

    // Query the table and display the results:
    try {
      stmnt = con.createStatement();
      // This is dangerous if the query string contains any external text!
      ResultSet rs = stmnt.executeQuery( SQL_Query );
      displayResults( rs );
      stmnt.close();

      // When not using your own data in SQL statement, you should use
      // PreparedStatements instead of Statements, to prevent SQL injection
      // attacks (a common security vulnerability in "textbook-quality"
      // code).  Here's an example to query the table with untrusted user data:

      // The SQL Query to use (note case-insensitive comparison):
      String dangerousQuery =
        "SELECT * FROM ADDRESSES WHERE UPPER(LNAME) = UPPER(?)";

      // Create a prepared statement to use:
      PreparedStatement pStmnt = con.prepareStatement( dangerousQuery );

      // Get the last name to query for, from the user:
      String lastName = JOptionPane.showInputDialog(
         "Please enter your name: " );

      if ( lastName != null ) {
        // Safely substitute data for "?" in query:
        // (Note there are many type-checking set* methods, e.g. "setInt")
        pStmnt.setString( 1, lastName );
        ResultSet lastNameSearchResults = pStmnt.executeQuery();
        System.out.println( "\n\tResults of last name query for " + lastName );
        displayResults( lastNameSearchResults );
      }

      pStmnt.close();
      con.close();
    } catch ( SQLException e ) {
        String theError = e.getSQLState();
        System.out.println("Can't query table: " + theError );
        System.exit(1);
    }

    // Shut down all databases and the Derby engine, when done.  Note,
    // Derby always throws an Exception when shutdown, so ignore it:
    System.out.println ( "Shutting down the database..." );
    try {
        DriverManager.getConnection("jdbc:derby:;shutdown=true");
    } catch ( SQLException e ) {} // empty: ignore exception

    // Note that nothing breaks if you don't cleanly shut down Derby, but
    // it will start in recovery mode next time (which takes longer to start).

 }

  // Derby doesn't support the standard SQL views.  To see if a table
  // exists you normally query the right view and see if any rows are
  // returned (none if no such table, one if table exists).  Derby
  // does support a non-standard set of views which are complicated,
  // but standard JDBC supports a DatabaseMetaData.getTables method.
  // That returns a ResultSet but not one where you can easily count
  // rows by "rs.last(); int numRows = rs.getRow()".  Hence the loop.

  private static boolean tableExists ( Connection con, String table ) {
    int numRows = 0;
    try {
      DatabaseMetaData dbmd = con.getMetaData();
      // Note the args to getTables are case-sensitive!
      ResultSet rs = dbmd.getTables( null, "APP", table.toUpperCase(), null);
      while( rs.next() ) ++numRows;
    } catch ( SQLException e ) {
        String theError = e.getSQLState();
        System.out.println("Can't query DB metadata: " + theError );
        System.exit(1);
    }
    return numRows > 0;
  }

  private static void displayResults ( ResultSet rs ) {
    // Collect meta-data:
    try {
      ResultSetMetaData meta = rs.getMetaData();
      String catalog = meta.getCatalogName(1);
      String schema  = meta.getSchemaName(1);
      String table   = meta.getTableName(1);
      int numColumns = meta.getColumnCount();

    // Display results:
    System.out.print( "\n\t\t---" );
    if ( catalog != null && catalog.length() > 0 )
       System.out.print( " Catalog: " + catalog );
    if ( schema != null && schema.length() > 0 )
       System.out.print( " Schema: " + schema );

    System.out.println( " Table: " + table + " ---\n" );

    for ( int i = 1; i <= numColumns; ++i )
      System.out.printf( "%-12s", meta.getColumnLabel( i ) );
    System.out.println();

    while ( rs.next() )       // Fetch next row, quit when no rows left.
    {   for ( int i = 1; i <= numColumns; ++i )
        {   String val = rs.getString( i );
            if ( val == null )
                val = "(null)";
            System.out.printf( "%-12s", val );
        }
        System.out.println();
    }
   } catch ( SQLException e ) {
        String theError = e.getSQLState();
        System.out.println("Can't view resultSet: " + theError );
        System.exit(1);
    }

  }
}

Note on MLN Predicate Categories

Markov Logic Networks (MLNs) is an approach to Statistical Relation Learning (SRL) that combines weighted First Order Logic formuals  and Markov networks to allow probabilistic learning an inference over multi-relational data. Learning and inference in MLNs is supported by a number of algorithms such as Gibbs Sampling, L-BFGS, Voted Perceptron, and MC-SAT. There a number of software package that implement MLNs such as: Alchemy, TuffyMarkov the beast, and ProbCog.

In Markov Logic Networks one can generalize  two dimensions for categorizing the predicates in the MLN. The first is by the component being modelled in the domain. To apply the MLNs approach to a problem domain, we need to recognize that a domain is made up from three components: objects of interest, attributes of the objects, and relations between the objects. Hence a predicate can be categorized as either class predicate, a relation predicate, or an attribute predicate. The second dimension for categorizing predicates in MLNs is by the end-user data need. In most probabilistic inference systems, a user query is composed of a target query and evidence which is used to estimate the probability of the target query.  Hence, a predicate can be categorized as  either an evidence predicate or a query (target) predicate.

In MLNs learning can be done either discriminatively or generatively.  In discriminative learning one or more predicates whose values are unknown during testing is designated as target predicates. The learning optimizes the performance with respect  to such predicates assuming that all the remaining predicates are given. In generative learning all predicates are treated equally.  Discriminative learning is used when we know a head of time what type of prediction we want to make. On the other hand generative learning is used when we want to capture all the aspects of domain.  So from a system point of view the second categorization is essential if we perform discriminative learning of a model.

The first categorization allow us to relate a problem to one or more common Statistical Relations Learning tasks. Take for example collective classification, link prediction, and social networks analysis. In collective classification the goal is predict  the class of the object given the object’s attributes and relations as evidence. Whereas in link prediction that goal is to predict the relation given the class(s) of the object and its relations.  In social networks analysis that target query could be the attributes of the objects or the relations between objects. In general these tasks fall under “classification” in Machine Learning theory.

Handling Persistent RDF models using Jena SDB

Jena API is one of the most commonly used API for handling RDF. It allows for creating different types of RDF models which includes: memory models, file-based models, inferencing models and database-backed models. The older versions of Jena used to be shipped with database subsystem that supported database-backed models . Jena authors later shifted two SDB and TDB. SDB uses an SQL database fir the storage and query of RDF data. As per Jena SDB documentation the use of SDB in new applications is not recommended as it may be pulled from the main build at very short notice. Developers are encouraged to use TDB which is a non-transactional, faster database soultion. This post explains the setup of SDB and how it is used for storing a simple RDF file in a MySQL database.

For this I will assume that you have the latest release of Jena (Jena 2.11.0). I will also assume that you have loaded your IDE and imported Jena API into you newly created project.

The first thing you will need to do is to setup a mysql database. In my case I created a mysql database named “rdfplay”. Now we have the database ready, you should download the maintenance release of SDB. Save and extract the files to you favorite location. Form here on I will assume that you extracted SDB to “/path/to/SDB”. Under this folder you will find a “lib” directory which contains the necessary jar files for running SDB tools. Get your hand into a copy of mysql JDBC driver jar file and place it under “lib” . We will need this we creating the format of our RDF model in MySQL. Next you need to create the store description file for MySQL which will look like this:

@prefix sdb:     <http://jena.hpl.hp.com/2007/sdb#> .
@prefix rdfs:	 <http://www.w3.org/2000/01/rdf-schema#> .
@prefix rdf:     <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix ja:      <http://jena.hpl.hp.com/2005/11/Assembler#> .

# MySQL - InnoDB

rdf:type sdb:Store ;
sdb:layout "layout2" ;
sdb:connection ;
sdb:engine "InnoDB" ; # MySQL specific
.

rdf:type sdb:SDBConnection ;
sdb:sdbType "MySQL" ; # Needed for JDBC URL
sdb:sdbHost "localhost" ;
sdb:sdbName "rdfplay" ; # database name
sdb:driver "com.mysql.jdbc.Driver" ;
sdb:sdbUser "rdfplay" ;
sdb:sdbPassword "password";
.

Save this file with the name sdb-mysql-innodb.ttl in the SDB folder. Next you need to configure a couple environment variables for creating before configuring the RDF store.

$ export SDBROOT=/path/to/sdb
$ export PATH=$SDBROOT/bin:$PATH
$ export SDB_JDBC=com.mysql.jdbc.Driver

Then you need to run the following to initialize the database. Be aware that this will wipe any existing data in the database. So you may need to check first.


$ sdbconfig --sdb sdb-mysql-innodb.ttl --format

This will create a basic layout in you database. Four tables should be created given that we have used layout2 in our store description file. The tables are: Nodes, Prefixes, Quads and Triples. Now we are ready to load some RDF to our database. Copy the following codes to eclipse and run it. The code simply, connects to the database store, checks of the model in the database (using the URI of the RDF file), if not it reads the RDF in an in-memory model and then adds it to the store. Otherwise it fetches the model from the store. The last statement prints the statements in the model. The code is pretty much self explanatory. Happy coding.

import com.hp.hpl.jena.query.Dataset;
import com.hp.hpl.jena.rdf.model.Model;
import com.hp.hpl.jena.rdf.model.ModelFactory;
import com.hp.hpl.jena.rdf.model.Property;
import com.hp.hpl.jena.rdf.model.Resource;
import com.hp.hpl.jena.rdf.model.Statement;
import com.hp.hpl.jena.rdf.model.StmtIterator;
import com.hp.hpl.jena.sdb.SDBFactory;
import com.hp.hpl.jena.sdb.Store;
import com.hp.hpl.jena.util.PrintUtil;

public class DBModelTester {

	private static String MODEL_URI = "http://example.me/foaf.rdf";

	public static void main(String[] args) throws Exception {

		// connect to a store
		Store store = SDBFactory.connectStore("sdb-mysql-innodb.ttl");

		// connect to an RDF dataset in the store
		Dataset ds = SDBFactory.connectDataset(store);

		// here is our mode
		Model m = null;

		// check the dataset if it contains our model
		if (!ds.containsNamedModel(MODEL_URI)) { // if not
			
			System.out.println("Loading Instance --- done once");
			
			// create an in memory model
			m = ModelFactory.createDefaultModel();
			
			// read the RDF in my model
			m.read(MODEL_URI);
			
			// add it to the data sore
			ds.addNamedModel(MODEL_URI, m);
		} else { // already in the story - just fetch it
			m = ds.getNamedModel(MODEL_URI);
		}

		// print the model
		printStatements(m, null, null, null);
		
		// close the data sore
		ds.close();

	}

	private static void printStatements(Model m, Resource s, Property p,
			Resource o) {

		for (StmtIterator iter = m.listStatements(s, p, o); iter.hasNext();) {
			Statement stmt = iter.next();

			System.out.println(" - " + PrintUtil.print(stmt));
		}

	}

}

Querying DBPedia using Jena API

Here is a code example that show how to query DBPedia SPARQL endpoint using Jena

String query=
"PREFIX p: "+
"PREFIX dbpedia: "+
"PREFIX category: "+
"PREFIX rdfs: "+
"PREFIX skos: "+
"PREFIX geo: "+

"SELECT DISTINCT ?m ?n ?p ?d"+
"WHERE {"+
" ?m rdfs:label ?n."+
" ?m skos:subject ?c."+
" ?c skos:broader category:Churches_in_Paris."+
" ?m p:abstract ?d."+
" ?m geo:point ?p"+
" FILTER ( lang(?n) = "fr" )"+
" FILTER ( lang(?d) = "fr" )"+
" }"

// now creating query object
Query query = QueryFactory.create(queryString);
// initializing queryExecution factory with remote service.
// **this actually was the main problem I couldn't figure out.**
QueryExecution qexec = QueryExecutionFactory.sparqlService("http://dbpedia.org/sparql", query);

//after it goes standard query execution and result processing which can
// be found in almost any Jena/SPARQL tutorial.
try {
ResultSet results = qexec.execSelect();
for (; results.hasNext();) {

// Result processing is done here.
}
}
finally {
qexec.close();
}

هل فشلت وزارة القوى العاملة في استقطاب المحاضرين العمانيين؟

سؤال يتبادر إلى ذهني في كل مرة ازور فيها احدى الكليات التقنية، فالمحاضر العماني عملة نادرة في هذه الكليات، فعلى سبيل المثال وليس الحصر فإن عدد المحاضرين العمانيين في أقسام تقنية المعلومات لا يتعدى 31 محاضرا متوزعين على الكليات السبع، حيث يشكل العمانييون نسبة 7.4% من إجمالي عدد محاضري تقنية المعلومات البالغ عددهم 421 متوزعين على السبع كليات، وهذه النسبة نوعا ما خجولة اذا ما قورنت بنسبة المحاضرين العمانيين في قسم علوم الحاسب الالي (كون هذا القسم هو الاقرب لتقنية المعلومات) في جامعة السلطان قابوس والتي تبلغ 32%، فيا ترى ما هي اسباب تدني نسبة المحاضرين العمانيين؟ وما هي الاثار المترتبة على تدني هذه النسبة؟

تعتبر منظومة الكليات التقنية (الكليات السبع) الاكبر من حيث عدد الطلاب المقبولين والمقيدين في مؤسسات التعليم العالي، حيث بلغ عدد الطلاب المقبولين في هذه الكليات 10500 طالبا وطالبة في العام الاكاديمي 2012/2013، اي 35% من اجمالي عدد الطلاب المقبولين في مؤسسات العليم العالي الحكومية والخاصة، في حين يبلغ عدد المقيديين في هذه الكليات اكثر من 32 ألف طالبا وطالبة، أي اكثر من ضعف عدد الطلاب الدارسين في جامعة السلطان قابوس، وبالرغم من هذا فإن ما يصرف على الطالب في الكليات التقنية لا يصل إلى خمس ما يصرف على الطالب في جامعة السلطان قابوس، وأقل من ثلث ما يصرف على الطالب في كليات العلوم التطبيقية التابعة لوزارة التعليم العالي.

لقد أنعكس هذا التقشف الحكومي في ادارة الكليات التقنية على برامج التطوير و التدريب للموظفين العمانيين، سؤاء الاكاديميين أو الاداريين، حيث أن نسبة عدد المحاضرين العمانيين في الكليات التقنية لا تتعدى 12% من اجمالي عدد المحاضرين.

أن التصاريح الرسمية للمسؤولين تشير إلى أن عدد العمانيين الذين تم تدريبهم في برنامج إعداد المحاضرين العمانيين في الوزارة بلغ 286 في نهاية العام 2012، اي بلغ متوسط عدد المتدربين سنويا في هذا البرنامج 35 محاضرا منذ بداية البرنامج في العام 2004، وهذا العدد نوعا ما خجول اذا ما قورن بالزيادة السنوية في اعداد المحاضرين الاجانب في هذه الكليات.

ان المطلع على آلية أستقطاب المحاضرين الاجانب في الكليات التقنية، يخرج بإستنتاج وحيد، وهو أن نوعية المحاضرين الذين تتعاقد معهم الوزارة لاترقى وطموحات الذين يعوولون على هذه الكليات في اعداد كادر مؤهل ومدرب لخوض غمار الحياة العملية، فأغلبية من توظفهم الكليات هم من الجنسيات الهندية، والباكستانية، والفلبينية، وتتم عملية التوظبيف عن طريق لجان تقوم بإختيار المرشحين عن طريق مقابلات شخصية للمترشحين في هذه الدول، حيث تقابل هذه اللجان أكثر من 400 مرشح في فترة لا تتعدى 5 أيام، أي يتم التوظيف بناء على مقابلة لا تتعدى في كثير من الاحيان أكثر من 10 دقائق، وللأسف لا توجد آلية لإختبار قدرات هؤلاء المحاضرين علميا، مع العلم أن الخبرة الفنية لا تشترط على المترشحين للتقدم، اي كل ما يطلب من المترشح خبرة اكاديمية مدتها 4 سنوات على الاقل،مع العلم ان غالبية هؤلاء هم خريجي جامعات وكليات غير معترف في وزارة التعليم العالي.

في المقابل فإن على المترشح لبرنامج إعداد المحاضرين العمانيين ان يكون حاصل على تقدير جيد جدا على اقل تقدير، او خبرة عملية في مجال العمل تصل إلى اربع سنوات، مع اشتراط ان تكون الشهادة من مؤسسة أكاديمية معترف بها، وعلى المترشح اجتياز الاختبار التحريري، والمقالبة الشخصية كي ينتقل للمرحلة الثانية.
في المرحلة الثانية يتم إلحاق المترشح ببرنامج تدريبي تصل مدته إلى سنتين، يستلم خلالها المتدرب 300 ريال عماني، يقضي الستة اشهر الاولى منها في إحدى الكليات التقنية، ثم يتم ابتعاث المرشح لنيل درجة الماجستير في ارقى جامعات المملكة المتحدة، ثم يعود ليتدرب في احدى مؤسسات القطاع الخاص في مجال التخصص، ثم بعدها يعين كمحاضر في الكليات التقنية، طبعا خلال هذه المدة يضل راتب المتدرب 300 ريال، وفي حال اراد المتدرب الخروج من البرنامج، يشترط عليه دفع كل المبالغ التي صرفت عليه خلال فترة التدريب وذلك حسب عقد يوقعه المترشح قبل بداية التدريب، واحد شروط هذا العقد هو ان يعمل كمحاضر في الكليات التقنية لمدة 5 اعوام بعد إكمال الماجستير.

اذا ما قورنت آلية توظيف المحاضر العماني بالمحاضر الاجنبي، يجد تناقضا غريبا في فكر الوزارة، فصعوبة آلية استقطاب المحاضر العماني يقابلها تراخي في آلية توظيف المحاضر الاجنبي، فصعوبة آلية توظيف المحاضرين، مع ضعف المخصصات خلال فترة التدريب احد العوائق التي تجعل الكثير يعيد التفكير في مسألة دخول هذا البرنامج، لماذا لا يتم إعطاء المترشح في فترة التدريب الدرجة المستحقة للحاصل على شهادة البكلريوس في الخدمة المدنية (السادسة) اسوة بنظام كليات العلوم التطبيقية؟ ولماذا شرط الخمس سنوات ايضا؟ لماذا لا يتم ايجاد طريقة أفضل للحفاظ على المحاضر العماني بعد توظيفة مثل أيجاد وضع خطة لتطوير المحاضر العماني كايفاده لموتمرات في مجال التخصص، وتشجيعه وتدريبه على البحث العلمي، وابتعاثة لنيل الدكتوراة في خطة زمنية واضحة؟

أن كثرة عدد المحاضرين الاجانب في الكليات التقنية له انعكاس سلبي على سير العملية التعليمية في الكليات التقنية، فالاسف الكثير من الطلاب لا يحترمون الاجنبي وذلك احيانا لضعف شخصية هذا المحاضر الاجنبي، وفي كثير من الاحيان لا يستمر المحاضر اكثر من سنة في الكلية وذلك لعدم مقدرته التأقلم في بيئة الكلية، كذلك يتحتم الامر على على المسؤولين في الوزارة انهاء خدمات بعض المحاضرين، كون هؤلاء غير مؤهلين علميا للكليات، كل هذا يؤدي إلى نوع من انعدام الاستقرار في طريقة سير العمل بالكليات.

ايضا انعدام او قلة العمانيين الاكاديميين في الكليات التقنية يؤدي إلى غياب القدوة الحسنة والمثال الذي ينظر إليه الطالب، وهذا يفقد الطالب الدافعية للعطاء والاستمرار، فوجود العماني يؤدي إلى خلق بيئة عمل متوازنة وصحية في الكليات.

الجميع يتفق على الدور الذي بإمكان الكليات التقنية أن تلعبه في النهوض بمستوى التعليم التقني في السلطنة، ولكن هذا لن يتأتى في غياب الاهتمام بالمحاضر العماني في هذه الكليات، وفي حال استمرارية فشل الوزارة في تحسين آلية أستقطاب المحاضرين العمانيين إلى هذه الكليات.

دمتم بود …….