Предположим, вам интересно узнать численность населения в городе, где живёте. Откройте Google и введите запрос «population of philadelphia, pa». Ха! Для Google это слишком. Он пытается ответить на запрос, но выдаёт «Pennsylvania — Population: 12,281,054 ; 6th, 12/00». Не то, что мы искали. Итак, Google понял запрос. Мы можем это утверждать, поскольку он выдал нам соответствующий ответ. Но он не смог выдать нам правильный ответ, поскольку не может правильно понимать страницы, содержащие ответы. Мы не можем винить за это Google, правда-правда.
Отложим пока эту мысль. В конце данной статьи я продемонстрирую простую программу на языке Python, использующую Семантический Веб, которая может отвечать на вопросы о популяции и прочих вещах. Хотя, сперва нам потребуется собрать некоторую статистику о численности населения.
Правительство США — просто сокровищница структурированной информации. В моей последней статье я говорил о правительственной информации, но на деле её много больше — на гигабайты и гигабайты больше. Перепись 2000-го года позволила собрать тонны статистики о численности населения. Давайте переложим часть её на Семантический Веб.
Итак, я возьму один небольшой кусочек данных размером в 14MB из данных переписи и переведу в RDF. Файл имеет имя usgeo.uf1 и содержит некоторую основную информацию о популяции и географии США в целом, по штатам, их округам, городам и «другим местечкам». Вы можете взять этот файл здесь. Все файлы переписи доступны по FTP и как документы.
Этот файл недостаточно хорош. Он содержит колонки фиксированной ширины, причём нет и намёка на то, что они означают. Если какой файл и подчёркивает особенности XML, так это он и есть. Вот запись для Филадельфии, разбитая так, чтобы уместиться на этой странице по ширине:
uSF1F US05000000 027049212234210121 0 61622377N61609999900
349881748 19544394Philadelphia County
CN 1517550 661958+39998012-07514479306
Документация по переписи не так проста, зато подробна. С помощью короткого скрипта на языке Perl довольно просто извлечь всю информацию. Каждая строка разбивается на определённые колонки:
@FIELDS = (FILEID, 6, STUSAB, 2, SUMLEV, 3, GEOCOMP, ...); # name of each field
%FIELDSIZE = (FILEID => 6, STUSAB => 2, SUMLEV => 3, ...); # width
$start = 0;
foreach $field (@FIELDS) {
$value = substr($line, $start, $FIELDSIZE{$field});
$start += $FIELDSIZE{$field};
$info{$field} = $value;
}
Теперь
%info содержит всю информацию для этой записи. Далее, информация
просто записывается в RDF. Язык
Turtle (или Нотация 3,
относительный синтаксис) наиболее прост для этого. Наша цель в том, чтобы
получить для каждого из 497,515 штатов, округов и т.д. в файле переписи нечто
вроде этого:
@prefix dc: <http://purl.org/dc/elements/1.1/> .
@prefix census: <tag:govshare.info,2005:rdf/census/> .
<some URI representing Philadelphia>
dc:title "Philadelphia" ;
census:population "1517550" ;
census:households "661958" .
(Если что неясно, используйте validator, чтобы увидеть, какие тройки сгенерированы синтаксисом.)
Предикат
dc:title довольно распространён. Он используется в большинстве
RSS-источников, чтобы обозначить заголовок источника, и имеет смысл
использовать его в данном контексте для связи места и его названия. Я введу
несколько предикатов для популяции и хаусхолдов.
До того, как мы сможем вывести RDF, оформленный по правилам Turtle, нам следует создать URI, чтобы определить каждый регион, охватываемый переписью.
Первое, о чём следует спросить, так это используется ли уже существующий URI, представляющий штаты Америки, округа и т.д. В результате поиска в Swoogle был обнаружен один существующий URI для Филадельфии, основанный на представлении RDF машиночитаемого словаря WordNet, созданного в Принстоне. Если бы в WordNet имелась запись для каждого города, мы бы могли использовать эти URI. Поскольку это не так, мы просто создадим новые URI.
Иметь два URI для Филадельфии не очень хорошо, но не критично. Недостаток в том, что два информационных ресурса (RDF-WordNet и перепись) не будут ссылаться друг на друга, но, с другой стороны, они и раньше никак не были связаны.
Итак, какой URI должы получить хаусхолды? Потребуется довольно много
времени, чтобы присвоить каждому городу по URI вручную, так что мы используем
язык Perl, чтобы сгенерировать URI для каждой записи, объединяя URI объекта,
содержащего сущность, со слэшем и, затем, с именем самой сущности.
Если США получит URI
<tag:govtrack.us,2006:us> (произвольно), тогда
Пенсильвания должна получить URI
<tag:govtrack.us,2006:us/Pennsylvania>, а Филадельфия
получит
<tag:govtrack.us,2006:us/Pennsylvania/Philadelphia>.
Поскольку ни один штат не имеет двух округов с одинаковыми именами, это
гарантирует, что ни один URI не будет использоваться для представления двух
разных объектов. (Результирующий URI должен гарантированно быть правильным
URI, и, чтобы этого достичь, имена нормализованы за счёт удаления пробелов и
других символов, которые могли бы вызвать проблемы.)
На первый взгляд, RDF выглядит избыточным, поскольку содержит Филадельфию
в двух местах: в URI и как объект предиката
dc:title. Если пользователь желает получить имя, он может взять
остаток строки от последнего слэша. Решение не из лучших. Важно избежать
выкладывания информации
внутри URI, поскольку URI не структурируются каким-либо разумным
способом. Пользователь может вычислить, что строки
Pennsylvania и
Philadelphia соотносятся с сущностью, но не поймёт, почему.
Я опубликовал полный скрипт на языке Perl, который прокручивает файл и выводит RDF для каждой строки файла наряду с входным файлом и результирующим RDF.
Поскольку информация теперь находится в RDF, работа с ней сильно облегчается из-за того, что уже существуют средства работы с RDF для различных языков. Сравните их число с числом свободно распространяемых библиотек, доступных для обработки данных переписи (по нулям?).
Одним из средств является RDFLib для языка Python. С его помощью мы можем загрузить файл в формате Turtle в память, найти сущность, которая обозначает (представляет) Филадельфию, и затем выяснить численность населения по данной сущности.
Чтобы загрузить файл в память, создайте новый объект
Graph и вызовите его метод
parse. Объект
Graph является областью в памяти, куда складируются найденные в
файле записи RDF.
from rdflib import Graph, Namespace, Literal
store = Graph();
store.parse("census-data.n3", None, 'n3');
На следующем шаге следует получить ссылку на сущность, обозначающую
Филадельфию. Для этого существуют два способа. Если вам известен URI
Филадельфии, этого уже достаточно. Однако, допустим, что нам известно лишь
имя, введённое пользователем. В таком случае нам следует запросить объект
Graph на предмет сущности, чей предикат
dc:title содержит слово
"Philadelphia". Вот код:
dc = Namespace("http://purl.org/dc/elements/1.1/")
philadelphia = store.value(None, dc["title"],
Literal("Philadelphia"));
Метод
value имеет три аргумента: субъект, предикат, и объект, один из
которых должен иметь значение
None. Метод сканирует записи в графе на предмет такой, которая
совпадает с аргументами, т.е. такой, чьим предикатом является
dc:title, а объект имеет значение
"Philadelphia" типа literal. В случае, если подходящая запись
будет найдена, метод возвращает поле, которое было задано как
None, в данном случае субъект. Обратите внимание, что RDF,
оформленный по правилам Turtle, на самом деле переводится в три записи:
<tag:govtrack.us,2006:...ia/Philadelphia> dc:title "Philadelphia"
.
<tag:govtrack.us,2006:...ia/Philadelphia> census:population "1517550" .
<tag:govtrack.us,2006:...ia/Philadelphia> census:households "661958"
.
Первая запись удовлетворяет фильтру, поэтому метод возвращает субъект,
<tag:govtrack.us,2006:us/Pennsylvania/Philadelphia>.
На следующем шаге следует получить популяцию по данной сущности.
Используется тот же метод, но на этот раз значение
None имеет объект.
census = Namespace("tag:govshare.info,2005:rdf/census/")
population = store.value(philadelphia, census["population"], None);
Теперь фильтру удовлетворяет вторая запись, и на выходе мы получим
значение
"1517750" типа literal в переменную
population.
Давайте попытаемся сделать что-нибудь посложнее, например, найдём среднюю
численность населения 50-ти штатов. Общей частью всего API для RDF является
метод, который пробегает все записи, отвечающие маске. В RDFLib такой метод к
месту называется
triples. (
triple — это другое название записи RDF.) Для этого примера нам
потребуется найти сущности во вложенном графе, которые обозначают штаты, и
выяснить, что делает сущность штатом. Оказывается, признак того, что сущность
обозначает штат, содержится в субъекте записи, которая заканчивается
rdf:type
usgovt:State. Так что нам следует пробежаться по всем записям,
которые выглядят как «
____ rdf:type usgovt:State».
rdf = Namespace("http://www.w3.org/1999/02/22-rdf-syntax-ns#")
usgovt = Namespace("tag:govshare.info,2005:rdf/usgovt/");
for statement in store.triples((None, rdf["type"], usgovt["State"])):
state = statement[0] # the subject is the first element
Теперь
triples возвращает список записей. Каждая запись представлена в
виде массива: первым элементом является субъект, вторым предикат, третьим
объект.
Теперь мы можем получить численность населения так же, как ранее, и просуммировать:
totalpop = 0
statecount = 0
for statement in store.triples((None, rdf["type"], usgovt["State"])):
state = statement[0] # the subject is the first element
population = store.value(state, census["population"], None)
population = int(population) # convert from Literal to integer
totalpop = totalpop + population
statecount = statecount + 1
print totalpop/statecount
Полный исходник на языке Python также выложен в сеть.
Любая информация может моделироваться с помощью RDF, и RDF просто на высоте в случае, когда одна программа может с лёгкостью всецело получить доступ к источникам информации иного рода. Итак, какая другая информация относится к переписи? Перепись содержит информацию о штатах Америки, в Конгрессе существуют сенаторы от каждого штата, и, как вам известно из моей последней статьи, я уже создал RDF файлы, содержащие информацию о членах Конгресса.
Приведу здесь фрагмент RDF касательно сенатора Чарльза Скумера штата Нью-Йорк:
# people:S00148 is the URI for the senator
people:S00148 pol:hasRole [
time:from [ time:at "2005-01-01" ] ;
time:to [ time:at "2010-12-31" ] ;
pol:forOffice senate:ny . ] .
# senate:ny represents the office of senator for New York
senate:ny pol:represents <tag:govshare.info,2005:data/us/NewYork>
.
Поскольку я создал оба этих набора данных, я решил использовать один и тот
же URI для обозначения штата Нью-Йорк в обоих наборах. Из-за такого моего
решения оба набора данных
связаны посредством этих записей. Будучи загруженными в один и тот
же объект
Graph, запросы на тройки могут беспрепятственно охватить оба
набора данных.
Давайте попробуем выяснить, какой сенатор представляет штат с наибольшей популяцией. Примеры становятся несколько академическими, однако, по мере того, как всё больше информации преобразуется в RDF, мы получаем возможность найти ответы на всё более волнующие вопросы.
Как в случае нахождения средней популяции по штатам, мы просмотрим
сущности, которые представляются как
usgovt:State. Но после того, как мы найдём штат с наибольшей
численностью населения, мы также подадим запрос на определение имени
сенатора. Это не так просто, поскольку сущности, обозначающие сенаторов, лишь
косвенно связаны со штатами. Начиная со штата, предикат
pol:represents относит нас назад к абстрактному обозначению
должности персоны, и, наконец,
pol:hasRole возвращает нас к персоне, имеющей данную должность.
Тогда, чтобы получить имя, мы воспользуемся предикатом
foaf:name.
Чтобы воспользоваться предикатом, используйте метод
subjects. Он берёт предикат и объект и возвращает все субъекты,
найденные в записях, отвечающих данному предикату и объекту.
Заметьте, как структура фрагмента RDF, приведённого выше, указывает способ доступа к информации:
# 'maxstate' holds the entity denoting the most populous state
for office in store.subjects( pol["represents"] , maxstate ) :
for role in store.subjects( pol["forOffice"] , office ) :
for person in store.subjects( pol["hasRole"] , role ) :
print store.value( person, foaf["name"] , None ) ;
После запуска данный код выведет целую кучу имён, большинство из которых принадлежат людям, нам не подходящим, поскольку они представляли Калифорнию в прошлом. (В информации касательно бывших членов Конгресса присутствуют ошибки, вызывающие появление этих имён в общем списке.) Я предпочёл моделировать Конгресс исторически, включая в RDF даты, а не делать «снимок» современного Конгресса. В результате мы должны просматривать лишь те имена, владельцы которых ещё не покинули свои посты, т.е. такие, для которых конечная дата находится в будущем (а не в прошлом).
for office in store.subjects( pol["represents"] , maxstate ) :
for role in store.subjects( pol["forOffice"] , office ) :
enddate1 = store.value( role , time["to"] , None ) ;
enddate2 = store.value( enddate1 , time["at"] , None ) ;
if (str(enddate2) > "2006-03-27") :
for person in store.subjects( pol["hasRole"] , role ) :
print store.value( person , foaf["name"] , None ) ;
Теперь полученные данные верны: Барбара Боксёр и Диана Фейнштейн, сенаторы Калифорнии.
Интерактивность — вот в чём я вижу важность Семантического Веб для всего мира. Вспомните запрос для Google о популяции Филадельфии. Проблема Google была в том, что он не мог понять информацию на веб-страницах. Очевидно, если бы мы хотели построить систему, которая могла бы это сделать, могла бы понять знания, распространённые в Internet, нам всем бы потребовалось использовать некоторую общую структуру для представления знаний, как RDF.
Итак, давайте приступим и напишем небольшую вопросно-ответную систему относительно данных переписи, которые мы использовали. Она должна понимать запросы вида:
what is the ____ of _____ ?
ex. what is the population of California?
Довольно легко получить нечто работоспособное в первом приближении. Используя регулярные выражения, могут быть выделены два пропуска в вопросе:
import re;
m = re.search('what is the (.*) of (.*)\??', question);
if m:
predicatename = m.group(1);
entityname = m.group(2)
# do more processing
else :
print "I don't understand the question."
Затем нам следует найти сущности RDF, которые отвечают данным в вопросе
предикату и имени сущности. Для сущностей мы можем использовать предикат
dc:title:
entity = store.value(None, dc["title"], Literal(entityname));
Чтобы обнаружить предикатную сущность, у нас нет в наличии ни одной записи RDF, которая бы относилась к предикату по имени. Так что мы вынуждены сделать так:
census:population rdfs:label "population" .
Вот пример записи, которую вы могли бы обнаружить в схеме RDF. Если бы она
была нам доступна, мы могли бы использовать такой же подход, каким мы
пользовались в случае с
dc:title, за исключением
rdfs:label. Поскольку запись нам недоступна, нам следует сделать
шаг назад и просмотреть URI предикатов:
predicate = None
for p in store.predicates() :
if (p.lower().endswith(predicatename.lower().replace(' ', ''))) :
predicate = p
Как только мы имеем предикат и сущность, нам следует сделать лишь один шаг, чтобы обнаружить соответствующее значение:
value = store.value(entity, predicate, None);
print entityname + "'s " + predicatename + " is " + value;
Исходник на языке Python для этой программы выложен в открытый доступ.
Запущенная программа выдаёт следующее:
# python qa.py what is the population of California?
California's population is 33871648
Если бы это бы единственный вопрос, который мы хотели бы задать, мы не писали бы программу. Конечно же, мы можем задать его касательно любого штата, округа, или города, для которого в переписи существует статистика (если только нам известно точное название, используемое для этого в переписи). Но мы также можем использовать другие предикаты.
# python qa.py what is the USPS state code of Mississippi?
Mississippi's USPS state code is MS
# python qa.py what is the land area of New York?
New York's land area is 122283145776 m^2
Удивлены, правда? Разве вы не забыли об аббревиатурах штатов, используемых
для почтовой связи? Я не упоминал о них, однако, в файлах переписи,
переложенной в RDF, которую я выложил в доступ, существуют предикаты, имеющие
название
census:landArea и
census:uspsStateCode наряду с предикатами популяции. Может быть,
нам просто чуть-чуть повезло, что я выбрал хорошие URI для предикатов.
Однако, это всё же сработало.
И вот, что можно сказать касательно RDF. Мы могли бы написать всеобъемлющую вопросно-ответную программу. Она имела бы возможность давать ответы лишь на вопросы определённой формы, однако, не была бы связана с какой-либо определённой тематикой. Без внесения изменений, программа могла бы отвечать на вопросы любого характера, только бы ответы содержались в RDF.