Ремарка

Эта работа основана на материале, распространяемом под лицензией Creative Commons Attribution 4.0 (CC BY 4.0) International License. Исходный материал: "ОЦЕНКА ПАРАМЕТРОВ ДИФФЕРЕНЦИАЛЬНЫХ УРАВНЕНИЙ ПО НЕТОЧНЫМ НАБЛЮДЕНИЯМ" от Г. Ш. Цициашвили и М. А. Осипова, доступно по адресу https://journals.vsu.ru/sait/article/view/10247/9468 (дата обращения: 14.07.2023). В оригинальный алгоритм/метод не быловнесено изменений, а лишь представлена его реализация и проведен эксперимент.

Отмечу, что в данной работе (коде) используется ранее созданный мною строковый калькулятор, функционирование которого было описано в предыдущей статье (https://habr.com/ru/articles/747920/). В связи с этим, не будем останавливаться на его функциях в данной статье.

Мотивация и о статье

В упомянутой выше статье описывается метод, который позволяет оценить параметры системы дифференциальных уравнений. Так как данная статья вызвала мой интерес, я решила провести численный эксперимент на языке Scala, который мне наиболее близок, чтобы увидеть реальные результаты и более детально ознакомиться с алгоритмом.

\left\{ \begin{aligned} \frac{dx}{dt} &= \sigma (y - x) \\ \frac{dy}{dt} &= x (\rho - z) - y \\ \frac{dz}{dt} &= xy - \beta z \\ \end{aligned} \right. (*1)

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

\left\{ \begin{aligned} \frac{dy_1}{dt} &= b_1 (y_2 - y_1) \\ \frac{dy_2}{dt} &= y_1 (b_2 - y_3) - y_2 \\ \frac{dy_3}{dt} &= y_1y_2 - b_3 y_3 \\ \end{aligned} \right. (*2)

Здесь yi представляют переменные, а bi - константы, которые мы собираемся оценить по наблюдениям.

\frac{dy_i}{dt} = F_i (y_1 ,\ldots , y_m , b_1 ,\ldots , b_m ), \quad i = \overline{1, m} \ \ (*3)

Статья рассматривает подобные системы, и несложно убедиться, что системе Лоренца она соответствует (m = 3).

Утверждается, что если имеются некоторые неточные наблюдения yi' с временным шагом hi,

{y_i}^{'}(ht) = y_i(ht) + \epsilon(hi) \ \ \ (*4) \\ где \\ {y_i}^{'}(ht) - неточное\ наблюдение\ в\ момент\ ht\ (*5)\\ {y_i}^{}(ht) - точное \ значение\ значение\ переменой\ в\ момент\ ht\ (*6) \\ \epsilon(ht) - отклонение\ от\ точного\ значения\ (*7) \\ h - \ шаг\ h = n^{-\alpha} \ где\ \alpha \in (1, 1.5) (*8) \\ t = \overline{-n, n}\ , n - количество\ наблюдений (*9)

то

F_i (\overline{y_1^0}, \ldots, \overline{y_m^0}, \beta_1^0, \ldots, \beta_m^0) = \overline{F_i^0}  (*10)\\где \\ \overline{y_i^0} = \frac{\sum_{i=-n}^{n} y_i}{2 * n + 1} - проще\ говоря\ среднее (*11)\\ \overline{F_i^0} = \frac{\sum_{t=-n}^{n} (y_i(ht)*(ht))}{\sum_{\substack{t=-n \\ }}^{n} (ht)^2 } (*12) \\ собственно\ \beta_i^0 - это\ и\ есть\ наша\ оценка\ параметра (*13)

Из системы, *10, выводятся оценки наших параметров bi, которые примерно равны bi0

Шаг первый - подготовка данных.

Первое, что я сделаю, это определюсь с переменными, которые мы задаем, и определюсь с форматами данных.

object LorenzAttractor extends App {
  
  val n = 1000 // *9
  val alpha = 1.25 // *8
  val h = Math.pow(n, -alpha) // *8

  val bi: List[Double] = List(2, 3, 1) // фиксированные параметры bi
  val yi0: List[Double] = List(1, 2, 1) // начальне значения во временом наге равном 0
  val systems: List[String] = List("b1*(y2-y1)", "y1*(b2-y3)-y2", "y1*y2-b3*y3") // сисиема уравнений *2
  val reverseSystems: List[String] = List("F1/(y2-y1)", "(F2+y2)/y1+y3", "(y1*y2 - F3)/y3") // решение системы *10

}

reverseSystem - это необходимый костыль, так как я еще не придумала, как решать произвольные системы уравнений (возможно, этому вопросу будет посвящена моя будущая статья). Матричные решения подходят только для систем линейных уравнений.

Для дальнейшей работы также необходимо настроить строковый калькулятор, чтобы он мог работать со всеми операциями, которые есть в systems и reverseSystems, а именно: "+", "-", "*", "/".

object LorenzAttractor extends App with StringCalculator with ConstantFabric with BinaryOperationFabric {
    
  addBinaryOperation(createBinary("+", 1, (left, right) => left + right, 0))
  addBinaryOperation(createBinary("-", 1, (left, right) => left - right, 0))
  addBinaryOperation(createBinary("*", 2, (left, right) => left * right, 1))
  addBinaryOperation(createBinary("/", 2, (left, right) => left / right, 1))
  
  ...
  
  bi.zipWithIndex.foreach(b_index => addConstant(createConstant(s"b${b_index._2 + 1}", b_index._1)))

}  
  

Для этого мы используем классы StringCalculator, ConstantFabric иBinaryOperationFabric (смотрите мою статью про строковый калькулятор) и добавляем соответствующие операции.

Чтобы добавить bi в уравнения, мы представляем их как константы в строковом калькуляторе.

Шаг 2 - генерация неточных наблюдений

для начала сделаем небольшой метод, который будет вычеслять значения функций с нашими переменными yk,i

  private def calkFun(fun: String, values: List[Double]): Double = {
    calculate(values.indices.foldLeft(fun)((acc, i) => acc.replace(s"y${i + 1}", s"(${values(i)})")))
  }

Данный метод осуществляет замену всех yi в формулах на соответствующие значения из values и затем вычисляет значение функции в точке Mi(yk,1, ..., yk,m) (метод calculate - часть строкового калькулятора).

Так как система Лоренца *1,2 не имеет аналитического решения, мы будем решать систему численными методами. Это в некотором смысле выгодно, поскольку нам требуются Неточные наблюдения.

В качестве численного метода мы воспользуемся самым элементарным методом - методом Эйлера. Чтобы понять, как именно я им воспользуюсь, достаточно одной формулы.

y_{(k+1),i} = y_{k,i} + h f_i(y_{k,1}, ..., y_{k,m}) (*14) \\

Отдельно сгенерируем наблюдения для положительных k.

  val positivePreciseObservations: List[List[Double]] = (1 to n).foldLeft(List(yi0))((acc, itr) => {
    acc :+ systems.zipWithIndex.map(fun_index => acc.last(fun_index._2) + h * calkFun(fun_index._1, acc.last))
  })

Алгоритм работы данного кода следующий:

  1. Создается переменная positivePreciseObservations, которая инициализируется списком, содержащим начальное наблюдение yi0 в качестве первого элемента. Этот список будет содержать последовательность точных наблюдений системы.

  2. Затем применяется метод foldLeft к диапазону от 1 до n. В каждой итерации itr представляет текущий номер итерации.

  3. Внутри каждой итерации происходит следующее:

    С помощью метода zipWithIndex применяется функция fun_index к каждому элементу списка systems (список функций системы) вместе с соответствующим индексом.

    Для каждой пары (fun, index) выполняется вычисление нового значения, используя последнее значение из acc (последнее точное наблюдение) для соответствующего индекса index. Здесь fun представляет функцию, а acc.last(fun_index._2) получает последнее значение из acc для индекса index.

    Результат вычисления добавляется в acc с помощью оператора :+. Это обновляет список acc, добавляя новое точное наблюдение системы.

  4. По завершении всех итераций получается список positivePreciseObservations, содержащий последовательность "точных" значений системы *6 после каждой итерации во временных шагах hk, где k принимает значения от -n до n.

Для генерации наблюдений для отрицательных значений k, выполним следующие шаги:

Для обратного шага формула выводится из уравнения *14.

  val negativePreciseObservations: List[List[Double]] = (1 to n).foldRight(List(yi0))((itr, acc) => {
    systems.zipWithIndex.map(fun_index => acc.head(fun_index._2) - h * calkFun(fun_index._1, acc.head)) :: acc
  }).init

Затем объединим значения для отрицательных и положительных значений k и выполним транспонирование списка наблюдений.

val preciseObservations: List[List[Double]] = (negativePreciseObservations ::: positivePreciseObservations).transpose

Шаг 3 - генерация неточных наблюдений.

val rnd = new Random()

val nonPreciseObservations: List[List[Double]] = preciseObservations.map { nums =>
  nums.map(num => (1 + Math.pow(-1, rnd.nextInt()) * rnd.nextDouble() * 0.2) * num)
}

В этом шаге к каждому значению из списка "точных значений" добавляется ошибка до 20% от значения.
Math.pow(-1, rnd.nextInt()) определяет направление отклонения (положительное или отрицательное)
rnd.nextDouble() * 0.2 определяет величину отклонения.

Шаг 4 - реализация алгоритма оценки параметров

Реализация алгоритма оценки параметров:

trait SystemParameterEstimator extends StringCalculator {

  def estimator(nonPreciseObservations: List[List[Double]], reverseSystems: List[String], h: Double): List[Double] = {

    ...

  }

  private def replaceVariableValues(fun: String,symbol: String , values: List[Double]): String = {
    ...
  }

}

Мы создадим трейт, который будет отвечать за эту функциональность. Для работы со строковым калькулятором не забываем подмешать трейт "extendsStringCalculator".

estimator - собственно метод, который будет возвращать оценки параметров. Он принимает неточные наблюдения nonPreciseObservations, решение системы *10 reverseSystems и h - шаг.

replaceVariableValues - метод, который заменяет si на соответствующие значения. В нашем случае, это будет использоваться для замены yi и Fi.

  private def replaceVariableValues(fun: String,symbol: String , values: List[Double]): String = {
    values.indices.foldLeft(fun)((acc, i) => acc.replace(s"$symbol${i + 1}", s"(${values(i)})"))
  }

Для вычисления значения n *9 из доступных данных, учитывая, что количество шагов всегда равно 2 * n + 1, формула примет следующий вид.

val n = (nonPreciseObservations.head.size - 1 ) / 2

Далее мы рассчитаем средние значения, используя *11.

val yi0 = nonPreciseObservations.map(yi => yi.sum / yi.size)

Также мы вычислим приближенные значения функций при yi0 с использованием *12.

val Fi0 = nonPreciseObservations.map(yi => {
  (-n to n).zip(yi).map(k_y => k_y._1 * k_y._2 * h).sum / (-n to n).map(k => Math.pow(k * h, 2)).sum
})

Тут все точно по формуле, единственное, что стоит отметить, что для прохождения всех шагов используется диапазон (-n to n).

Последнее, что остается, - это решить систему *10 и получить приближенные значения bi.

reverseSystems.zipWithIndex.map( sys_index => {
  val expression1: String = replaceVariableValues(sys_index._1, "F", Fi0)
  val expression2: String = replaceVariableValues(expression1, "y", yi0)
  calculate(expression2)
})

Берем уравнение (в виде строки), заменяем значения Fi на соответствующие Fi0, а также подменяем yi на их средние значения в нулевой точке (k = 0).

Теперь класс выглядит следующим образом:

trait SystemParameterEstimator extends StringCalculator {

  def estimator(nonPreciseObservations: List[List[Double]], reverseSystems: List[String], h: Double): List[Double] = {

    val n = (nonPreciseObservations.head.size - 1 ) / 2

    val yi0 = nonPreciseObservations.map(yi => yi.sum / yi.size)

    val Fi0 = nonPreciseObservations.map(yi => {
      (-n to n).zip(yi).map(k_y => k_y._1 * k_y._2 * h).sum / (-n to n).map(k => Math.pow(k * h, 2)).sum
    })

    reverseSystems.zipWithIndex.map( sys_index => {
      val expression1: String = replaceVariableValues(sys_index._1, "F", Fi0)
      val expression2: String = replaceVariableValues(expression1, "y", yi0)
      calculate(expression2)
    })

  }

  private def replaceVariableValues(fun: String,symbol: String , values: List[Double]): String = {
    values.indices.foldLeft(fun)((acc, i) => acc.replace(s"$symbol${i + 1}", s"(${values(i)})"))
  }

}

Последний шаг

Давайте подмешаем трейт SystemParameterEstimator в класс, где проводится эксперимент, с помощью ключевого слова with SystemParameterEstimator.

object LorenzAttractor extends App with StringCalculator with ConstantFabric with BinaryOperationFabric with SystemParameterEstimator {

Затем вызовем метод estimate на полученных данных и выведем результат.

  println(estimator(nonPreciseObservations, reverseSystems, h))

Результат

при параметрах:

  val n = 1000
  val alpha = 1.25
  val h = Math.pow(n, -alpha)

  val bi: List[Double] = List(2, 3, 1)
  val yi0: List[Double] = List(1, 2, 1)

я получила результат:

List(1.9609705152167594, 3.1061318517029735, 0.9548191490247522) - b1, b2, b3 соответственно

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

Заключение

Я знаю что одного примера мало и я могла бы сделать эксперименты на различных системах с различными параметрами, построить гистограммы частот, построит зависимость среднего отклонения от дисперсии и так далее.

Этим собственно я и собираюсь заняться, но уже в следующей статье)

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

P.S. с проектом можно ознакомится на GitHub

https://github.com/PicoPicoRobotWoman/estimation_of_parameters_of_differentia_equations

Комментарии (11)


  1. Arastas
    15.07.2023 06:49

    Что ж вы то набираете формулы, то картинки вставляете?


    1. PicoPicoRobotWoman Автор
      15.07.2023 06:49

      Tex достаточно простой формат, не вижу проблем в вставке формул


      1. Arastas
        15.07.2023 06:49

        Так и я о том - если TeX простой формат, то зачем вставлять плохо выглядящие картинки?


        1. PicoPicoRobotWoman Автор
          15.07.2023 06:49

          Картинка у меня всего одна одна, как обложка для статьи, внутри статьи только формулы. Честно


          1. Arastas
            15.07.2023 06:49

            Я про формулы 5-13. Если это не картинка, то, возможно, стоит их обновить.


  1. belch84
    15.07.2023 06:49
    +8

    Тема публикации перекликается с известным разделом теории динамических систем, который посвящен реконструкции систем. Реконструкция — это построение динамической системы (например, в виде системы Обыкновенных Дифференциальных Уравнений) по одному или нескольким временным рядам, представляющим собой результаты наблюдений каких-то физических величин. Не осмеливался заниматься реконструкцией для неточных наблюдений, с точными когда-то экспериментировал.
    Реконструкция (или восстановление) системы по нескольким временным рядам — более простая задача, поскольку в ней заранее известно количество динамических переменных, описывающих систему.
    Как и описано в статье, по системе Лоренца (решая её)


    image
    image

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


    Вот результат конкретного эксперимента


    Реконструкция системы Лоренца, правая часть в виде полиномов второго порядка

    `Реконструкция Лоренца


    Уравнения системы:
    Y1' = c1+c2Y1+c3Y2+c4Y3+c5Y1^2+c6Y1Y2+c7Y2^2+c8Y1Y3+c9Y2Y3+c10Y3^2
    Y2' = c11+c12Y1+c13Y2+c14Y3+c15Y1^2+c16Y1Y2+c17Y2^2+c18Y1Y3+c19Y2Y3+c20Y3^2
    Y3' = c21+c22Y1+c23Y2+c24Y3+c25Y1^2+c26Y1Y2+c27Y2^2+c28Y1Y3+c29Y2Y3+c30Y3^2


    Начальные условия:
    X0 = 1, X1 = 7.998
    Y1(X0) = 9.0294515
    Y2(X0) = 14.557099
    Y3(X0) = 18.442343


    Параметры (константы) системы:
    C1=-0.706157679, C2=-9.903306103, C3=9.90869937, C4=0.086167599,
    C5=0.009366051, C6=-0.007094675, C7=0.000585685, C8=-0.002520329,
    C9=0.001717728, C10=-0.002387849,C11=-6.651345298,C12=27.03746801,
    C13=-0.740066127,C14=0.833818399, C15=0.108642436, C16=-0.099410223,
    C17=0.020862284, C18=-0.973958104,C19=-0.002888791,C20=-0.023869912,
    C21=-5.402205382,C22=-0.254148545,C23=0.099799627, C24=-1.899216532,
    C25=0.144377007, C26=0.856194606, C27=0.031540784, C28 = 0.007928282,
    C29=-0.003421554,C30=-0.024331327
    `

    Видно, что значения image приблизительно соответствуют искомым.
    Вот как реконструкция выглядит на графике
    Графики первой компоненты исходной и реконструированной системы Лоренца
    image

    Реконструкция по одному временному ряду — задача более сложная, поскольку в этом случае предварительно следует как-то оценить количество динамических переменных, определяющих систему. Для этого существуют специальные методы.


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


    Графики первых двух компонент системы Лоренца с параметрами из статьи

    Зеленый график — первая компонента, красный — вторая


    image

    Подробнее о реконструкции можно почитать в
    Лоскутов А.Ю., Михайлов А.С. Введение в синергетику,
    а также здесь


  1. Zara6502
    15.07.2023 06:49
    +2

    Немного занудства:

    Эта работа основана на материале, распространяемом под лицензией Creative Commons Attribution 4.0 (CC BY 4.0) International License. Исходный материал: "ОЦЕНКА ПАРАМЕТРОВ ДИФФЕРЕНЦИАЛЬНЫХ УРАВНЕНИЙ ПО НЕТОЧНЫМ НАБЛЮДЕНИЯМ" от Г. Ш. Цициашвили и М. А. Осипова, доступно по адресу https://journals.vsu.ru/sait/article/view/10247/9468 (дата обращения: 14.07.2023). В оригинальный алгоритм/метод не быловнесено изменений, а лишь представлена его реализация и проведен эксперимент.

    Отмечу, что в данной работе (коде) используется ранее созданный мною строковый калькулятор, функционирование которого было описано в предыдущей статье (https://habr.com/ru/articles/747920/). В связи с этим, не будем останавливаться на его функциях в данной статье.

    Я бы оформил вот так:

    Эта работа основана на материале, распространяемом под лицензией Creative Commons Attribution 4.0 (CC BY 4.0) International License. Исходный материал: "ОЦЕНКА ПАРАМЕТРОВ ДИФФЕРЕНЦИАЛЬНЫХ УРАВНЕНИЙ ПО НЕТОЧНЫМ НАБЛЮДЕНИЯМ" от Г. Ш. Цициашвили и М. А. Осипова, доступно по адресу (дата обращения: 14.07.2023). В оригинальный алгоритм/метод не было внесено изменений, а лишь представлена его реализация и проведен эксперимент.

    Отмечу, что в данной работе (коде) используется ранее созданный мною строковый калькулятор, функционирование которого было описано в предыдущей статье. В связи с этим, не будем останавливаться на его функциях в данной статье.


    1. PicoPicoRobotWoman Автор
      15.07.2023 06:49

      спасибо


  1. quaer
    15.07.2023 06:49

    Скажите, пожалуйста, какое прикладное применение? Правильно понимаю, что если есть сэмплированная амплитудно-частотная и фазо-частотная характеристика, то этим способом можно реконструировать передаточную функцию? Если да, можете создать пример для функций 5-го порядка, к примеру?


    1. PicoPicoRobotWoman Автор
      15.07.2023 06:49

      1) Реконструкция динамических систем - прикладной раздел математики и она много где используется в прогназировании и управлении. Первое что мне приходит на ум - это сбор данных косательно погодных показателей с различных датчиков через равные интервалы времени. В таком случае заная "уравнения" которым подчиняется система можно востановить ее параметры. упомяну что в оригинальной статье указано что метод работает только если в точке с нулевым временом шагом якобиан в этой точке не должен быть равен нулю (в статье этот момент упущен за ненадобностью)

      2) "сэмплированная амплитудно-частотная и фазо-частотная характеристика" тут ничего сказать не могу поскольку не знаю что это и с таким не работала, могу посоветовать обратиться к автором метода, их контакты указаны в оригинальной статье.

      3) если под функциями пятого порядка имеюсят ввиду полином пятого порядка то я не вижу проблем.


  1. lonimo
    15.07.2023 06:49

    В тексте,, есть несколько моментов, которые можно улучшить. Сначала, описания переменных и формул в начале текста могут быть немного запутанными, особенно для тех, кто не знаком с системой Лоренца. Было бы полезно предоставить более подробное объяснение каждого уравнения и переменной, чтобы читатели могли лучше понять их смысл и значение.
    Также некоторые шаги в алгоритме оценки параметров не ясны. Например, как именно вы решаете систему уравнений и получаете оценки параметров? Было бы хорошо, если бы вы предоставили более подробное объяснение этого процесса.Кроме того, не ясно, каким образом вы генерируете неточные наблюдения и какая роль у параметров hi и epsilon в этом процессе. Было бы полезно предоставить более подробное объяснение этих параметров и их значения.

    В целом, текст нуждается в большей ясности и подробностях.