Обработка естественного языка (NLP) — это процесс искусственного интеллекта, используемый компьютерными системами для поиска смысла текстового контента. Это позволяет пользователям-людям задавать вопросы, используя предложения, написанные естественным образом, используя обычные предложения. Успешные системы НЛП позволят пользователям задавать вопросы по-разному и при этом смогут понять смысл задаваемого вопроса.

В этой статье мы используем обработку естественного языка (NLP) для создания примера приложения Java, которое может находить данные о погоде с помощью текстовых вопросов на «естественном языке». Это позволяет пользователям находить данные о погоде с помощью таких запросов, как:

  • Расскажите мне о погоде в Токио, Япония, 6 января 1975 года.
  • Какая погода будет в Лондоне в следующую среду?
  • Какая погода была в Вашингтоне 4 июля прошлого года?

Полный исходный код этого примера можно загрузить из репозитория GitHub:https://github.com/visualcrossing/WeatherApi/tree/master/Java/com/visualcrossing/weather/samples

Шаги по созданию простого бота с ответами о погоде NLP

В нашем примере мы ограничим нашу обработку NLP определением, задает ли пользователь вопрос о данных о погоде. Поддерживаемые вопросы будут заключаться в том, чтобы узнать предстоящий прогноз погоды или просмотреть исторические наблюдения за погодой.

Для этого нам нужно идентифицировать две части информации:

  1. Местоположение, для которого пользователь пытается найти информацию о погоде.
  2. Дата или диапазон дат для данных о погоде.

Как только мы нашли эту информацию, мы можем искать данные о погоде. Для этого воспользуемся Visual Crossing Weather API. Этот API-интерфейс Timeline Weather позволяет легко просматривать как прошлые, так и будущие данные о погоде, поскольку он автоматически подстраивается под диапазон дат, который мы запрашиваем. Если мы запросим даты в будущем, он даст нам прогноз погоды. Если мы запросим данные в прошлом, то это даст нам исторические наблюдения за погодой.

Для запуска примера кода вам потребуется бесплатный ключ Weather API. Если у вас нет ключа API, перейдите на страницу бесплатной регистрации, чтобы создать ключ.

Шаг первый — настройка процессинга НЛП

Существует ряд отличных библиотек Java с открытым исходным кодом, которые можно использовать для обработки НЛП. К ним относятся Apache OpenNLP и CoreNLP, созданные Standard NLP Group. В этом примере мы будем использовать CoreNLP, поскольку он обеспечивает простой способ начать работу и включает предварительно обученные модели для многих вопросов, на которые нам нужен API.

Определение места и даты

Мы будем использовать нашу обработку NLP, чтобы найти две части информации из вопросов пользователей, чтобы мы могли найти соответствующие данные о погоде — местоположение и даты. Поэтому мы сосредоточимся на правильном использовании распознавания именованных сущностей для поиска упоминаний сущностей в тексте. Упоминания объектов представляют собой элементы, представляющие особый интерес в тексте, такие как человек, место, дата или время.

Мы ищем упоминания объектов, которые определяют целевое местоположение данных о погоде (например, город, штат, провинция или страна) и даты, для которых мы должны найти данные о погоде.

Поэтому нам нужно, чтобы процессор НЛП мог определять местоположение из текста. Рассмотрим пару примеров.

В этом случае процессор должен указать, что это Токио, Япония, и что пользователь указал точную дату — 6 января 1975 года. CoreNLP включает демонстрационный сайт, который обеспечивает графическую интерпретацию текста:

Мы видим, что библиотека правильно определяет местоположение как комбинацию города и страны, а затем правильно определяет и анализирует предполагаемую дату.

Мы сможем передать местоположение «Токио, Япония» и дату в Weather API для поиска информации о погоде.

Какая погода будет в Лондоне, Англия в следующую среду?

В данном случае это Лондон, Англия. Дата немного сложнее, так как это не простая дата (хотя есть много способов, которыми пользователь может запросить дату, так что даже это нетривиальное упражнение!) Мы хотели бы, чтобы НЛП могло интерпретировать «следующую среду» как запросить дату, а затем преобразовать ее в точную дату.

Если мы пропустим приведенный выше текст через онлайн-текст, мы увидим, что библиотека снова правильно находит местоположение и предполагаемую дату:

Мы можем видеть, что процессинг НЛП правильно понимает концепцию «следующей среды». Оттуда библиотека находит следующую среду после текущей даты.

Какая погода была в Вашингтоне, округ Колумбия 4 июля прошлого года?

Если мы посмотрим на наш последний пример, мы снова увидим, что библиотека NLP правильно обрабатывает местоположение и целевую дату:

Библиотеке снова удалось идентифицировать город Вашингтон, округ Колумбия, и интерпретировать «последний» как относящийся к последнему случаю 4 июля.

Шаг второй — создание Java-кода НЛП

Теперь мы готовы создать первую часть нашего примера Java. Нашим простым будет Java-приложение, которое запрашивает у пользователя текст, обрабатывает текст для запрошенного местоположения и дат, а затем ищет данные о погоде. Для простоты это будет обрабатываться через Java system.out.

Прежде чем переходить к коду, сначала загрузите необходимые библиотеки с CoreNLP. Эти образцы были написаны с использованием версии 4.2.2, но любая последняя версия должна работать.

Если вы настраиваете свой Java-проект с помощью maven, вы можете найти необходимые зависимости maven здесь. Мы добавили следующие зависимости maven:

<dependency> 
 <groupId>edu.stanford.nlp</groupId> 
 <artifactId>stanford-corenlp</artifactId> 
 <version>4.2.2</version> 
</dependency> 
<dependency> 
 <groupId>edu.stanford.nlp</groupId>
 <artifactId>stanford-corenlp</artifactId>
 <version>4.2.2</version>
 <classifier>models</classifier>
 </dependency>

Настройка конвейера НЛП

Первая часть нашего кода — это основной метод Java. В этом методе мы сначала настраиваем конвейер CoreNLP и создаем базовый цикл для ввода пользователем текста. Для получения дополнительной информации об установке и настройке CoreNLP см. Примеры CoreNLP API.

public static void main(String[] args) {
 // set up pipeline properties
 Properties props = new Properties();
 // set the list of annotators to run
 props.setProperty("annotators","tokenize,ssplit,pos,lemma,ner");
 
 // set to use the neural algorithm
 props.setProperty("coref.algorithm", "neural");
        props.setProperty("ner.docdate.usePresent", "true");
        props.setProperty("sutime.includeRange", "true");
        props.setProperty("sutime.markTimeRanges", "true");
        
 // build pipeline
 System.out.printf("Starting pipeline...%n");
 StanfordCoreNLP pipeline = new StanfordCoreNLP(props);
 System.out.printf("Pipeline ready...%n%n");

 }
}

Мы сократили количество аннотаторов для нашего образца, потому что мы в основном сосредоточены на выводе аннотатора «ner». «ner» — это сокращение от «NERCombinerAnnotator», который отвечает за обработку именованных объектов, которые нам нужны для поиска местоположений и дат. Остальные аннотаторы необходимы для работы ner.

Теперь мы добавим цикл к указанному выше основному методу, чтобы пользователь мог вводить текст, а образец обрабатывал данные.

public static void main(String[] args) {
 // set up pipeline properties
 Properties props = new Properties();
 // set the list of annotators to run
 props.setProperty("annotators","tokenize,ssplit,pos,lemma,ner");
 
 // set to use the neural algorithm
 props.setProperty("coref.algorithm", "neural");
        props.setProperty("ner.docdate.usePresent", "true");
        props.setProperty("sutime.includeRange", "true");
        props.setProperty("sutime.markTimeRanges", "true");
        
 // build pipeline
 System.out.printf("Starting pipeline...%n");
 StanfordCoreNLP pipeline = new StanfordCoreNLP(props);
 System.out.printf("Pipeline ready...%n%n");
 
 //loop for ever asking the user for text to process
 try (Scanner in = new Scanner(System.in)) {
  while (true) {
  System.out.printf("Enter text:%n%n");
  
  String text = in.nextLine();
  try {
   processText(pipeline, text);
  } catch (Throwable e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  }
  
  }
 }
}

После того, как пользователь вводит текст, образец передает настроенный конвейер и текст второму методу «processText» для обработки текста.

Обработка текста для местоположения и даты

private static void processText(StanfordCoreNLP pipeline, String text) throws Exception {
 CoreDocument document = new CoreDocument(text);
 // annnotate the document
 pipeline.annotate(document);
 
 if (document.entityMentions()==null || document.entityMentions().isEmpty()) {
  System.out.println("no entities found");
  return;
 }
 
 String city=null;
 String state=null;
 String country=null;
 LocalDate startDate=null;
 LocalDate endDate=null;
     for (CoreEntityMention em : document.entityMentions()) {
      
      
      if (DATE.equals(em.entityType())) {
      Timex timex=em.coreMap().get(TimeAnnotations.TimexAnnotation.class);
      Calendar t1=timex.getRange().first;
      if (t1!=null) {
       startDate=LocalDateTime.ofInstant(t1.toInstant(), ZoneId.systemDefault()).toLocalDate();
      }
      Calendar t2=timex.getRange().second;
      if (t2!=null) {
       endDate=LocalDateTime.ofInstant(t2.toInstant(), ZoneId.systemDefault()).toLocalDate();
      }
      } else if (LOCATION_CITY.equals(em.entityType())) {
      city=em.text();
      } else if (LOCATION_STATE_OR_PROVINCE.equals(em.entityType())) {
      state=em.text();
      } else if (LOCATION_COUNTRY.equals(em.entityType())) {
      country=em.text();
      }
     }
     String location="";
     if (city!=null) location+=location.isEmpty()?city:(","+city);
     if (state!=null) location+=location.isEmpty()?state:(","+state);
     if (country!=null) location+=location.isEmpty()?country:(","+country);
     System.out.printf("Location=%s; fromDate=%s, toDate=%s%n", location,
      startDate!=null?startDate.format(DateTimeFormatter.ISO_LOCAL_DATE):"[Null]",
       endDate!=null?endDate.format(DateTimeFormatter.ISO_LOCAL_DATE):"[Null]"
       );
     
     if (location==null || location.isEmpty()) {
      System.out.println("no location information found");
      return;
     }
     timelineRequestHttpClient(location, startDate, endDate);
 }

Сначала код обрабатывает текст, создавая документ, а затем аннотирует его с помощью конвейера, который мы настроили выше.

Чтобы найти возможные упоминания сущности, мы перебираем список, найденный во время обработки. Мы используем тип объекта, чтобы помочь идентифицировать информацию, которую мы получаем. Если мы обнаружим, что у нас есть дата, мы используем экземпляр Timex, чтобы найти дату начала и окончания.

После этого мы ищем аннотации, связанные с местоположением (название города, штата или провинции или страны). Если мы находим какие-либо из них, мы запоминаем их.

После того, как все сущности были проверены, мы создаем единую строку местоположения, которая представляет собой комбинацию найденных нами объектов местоположения (точно так же, как адрес представляет собой комбинацию города, штата, страны и т. д.). Нам нужна одна строка для передачи этой информации о местоположении в Weather API, чтобы помочь API определить точное запрашиваемое местоположение.

Получение информации из API погоды

Теперь у нас есть местоположение и дата или диапазон дат для нашего запроса, поэтому мы можем перейти к считыванию данных о погоде из Weather API. Для получения дополнительной информации о том, как использовать API погоды в Java и о различных способах чтения данных по сети, ознакомьтесь с разделом Как использовать API погоды на временной шкале для получения исторических данных о погоде и данных прогноза погоды в Java.

В нашем случае нам нужно еще несколько зависимостей maven для обработки сетевого запроса и разбора JSON:

<dependency> 
 <groupId>org.apache.httpcomponents</groupId>
 <artifactId>httpclient</artifactId>
 <version>4.5.12</version>
 </dependency>
<dependency>
 <groupId>org.json</groupId>
 <artifactId>json</artifactId>
 <version>20200518</version>
</dependency>

Weather API используется для запроса данных о погоде в формате JSON:

public static void timelineRequestHttpClient(String location, LocalDate startDate, LocalDate endDate ) throws Exception {
 //set up the end point
 String apiEndPoint="https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline/";
 
 
 String unitGroup="us";
 
 
 StringBuilder requestBuilder=new StringBuilder(apiEndPoint);
 requestBuilder.append(URLEncoder.encode(location, StandardCharsets.UTF_8.toString()));
 
 if (startDate!=null) {
  requestBuilder.append("/").append(startDate.format(DateTimeFormatter.ISO_DATE));
  if (endDate!=null && !startDate.equals(endDate)) {
  requestBuilder.append("/").append(endDate.format(DateTimeFormatter.ISO_DATE));
  }
 }
 
 URIBuilder builder = new URIBuilder(requestBuilder.toString());
 
 builder.setParameter("unitGroup", unitGroup)
  .setParameter("key", API_KEY)
  .setParameter("include", "days")
  .setParameter("elements", "datetimeEpoch,tempmax,tempmin,precip");
HttpGet get = new HttpGet(builder.build());
 
 CloseableHttpClient httpclient = HttpClients.createDefault();
 
 CloseableHttpResponse response = httpclient.execute(get);    
 
 String rawResult=null;
 try {
  if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
  System.out.printf("Bad response status code:%d%n", response.getStatusLine().getStatusCode());
  return;
  }
  
  HttpEntity entity = response.getEntity();
     if (entity != null) {
      rawResult=EntityUtils.toString(entity, Charset.forName("utf-8"));
     }
     
     
 } finally {
  response.close();
 }
 
 parseTimelineJson(rawResult);
 
 }

Первая часть кода создает URL-адрес запроса Timeline Weather API. Сначала в запрос добавляется местоположение. Даты добавляются при необходимости. Если запрашивается одна дата, она добавляется сама по себе, в противном случае будут добавлены две даты. Если к запросу не добавлена ​​дата, будет возвращен 15-дневный прогноз.

Одиночный запрос даты будет выглядеть так:

https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline/Washington,DC/2020-07-04?unitGroup=us&key=YOUR_API_KEY&include=dates&elements=datetimeEpoch,tempmax,tempmin,precip

Запрос диапазона дат будет выглядеть так:

https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline/Washington,DC/2020-07-04/2020-08-04?unitGroup=us&key=YOUR_API_KEY&include=dates&elements=datetimeEpoch,tempmax,tempmin,precip

Обратите также внимание на использование необязательных параметров «элементы» и «включить». Эти параметры используются для фильтрации объема данных, возвращаемых запросом.

include=dates указывает API предоставлять нам информацию только на уровне даты (в противном случае также будет включена почасовая информация).

elements=datetimeEpoch,tempmax,tempmin,precip указывает API возвращать только эти четыре части данных о погоде. Другая информация, такая как ветер, давление и т. д., не будет включена.

Отображение данных о погоде

Последний фрагмент кода выводит очень простую таблицу данных о погоде в командную строку:

private static void parseTimelineJson(String rawResult) {
 
 if (rawResult==null || rawResult.isEmpty()) {
  System.out.printf("No raw data%n");
  return;
 }
 
 JSONObject timelineResponse = new JSONObject(rawResult);
 
 ZoneId zoneId=ZoneId.of(timelineResponse.getString("timezone"));
 
 System.out.printf("Weather data for: %s%n", timelineResponse.getString("resolvedAddress"));
 
 JSONArray values=timelineResponse.getJSONArray("days");
 
 System.out.printf("Date\tMaxTemp\tMinTemp\tPrecip\tSource%n");
 for (int i = 0; i < values.length(); i++) {
  JSONObject dayValue = values.getJSONObject(i);
        ZonedDateTime datetime=ZonedDateTime.ofInstant(Instant.ofEpochSecond(dayValue.getLong("datetimeEpoch")), zoneId);
            
        double maxtemp=dayValue.getDouble("tempmax");
        double mintemp=dayValue.getDouble("tempmin");
        double precip=dayValue.getDouble("precip");
          
        System.out.printf("%s\t%.1f\t%.1f\t%.1f%n", datetime.format(DateTimeFormatter.ISO_LOCAL_DATE), maxtemp, mintemp, precip );
    }
}

Этот код анализирует входящие данные JSON от Weather API. Оттуда код перебирает значения в массиве days и извлекает значения datetime, tempmax, tempmin и precip для каждого дня.

Время обрабатывается путем считывания даты с использованием секунд с начала эпохи и предоставления идентификатора часового пояса местоположения. Это создает экземпляр ZonedDateTime.

Конечным результатом являются простые данные о погоде для местоположения и дат, запрошенных в исходном тексте.

Спасибо за чтение. Пожалуйста, дайте мне знать, если у вас есть какие-либо комментарии или вопросы ниже!

Первоначально опубликовано на https://www.visualcrossing.com 11 июня 2021 г.