среда, 13 июня 2012 г.

Обучение персептрона с помощью ГА


Лабораторная работа

по курсу 

"Интеллектуальные системы":

Обучение нейронной сети с помощью генетических алгоритмов

Выполнил 
студент группы ИУ5-81
Баришок Н.И.


Краткое описание
Разработка компьютерной программы, которая обучает искусственный нейрон распознавать изображения с помощью генетических алгоритмов.
Цель работы
Изучить принципы работы и алгоритм обучения простейших искусственных нейронных сетей (НС).

Требования к исходным данным и функциональности компьютерной программы
  • В программе должна быть реализована возможность задания обучающей выборки из внешних файлов изображений
  • Изображения должны быть черно-белыми (bitmap) и размером не менее 16 (4x4) пикселей.
  • Программа должна иметь два режима работы: обучения и распознавания.
  • Обучение НС должно производиться с помощью генетического алгоритма .
На экранной форме режима обучения должны отображаться:
  • элементы обучающей выборки (изображения)
  • правильные варианты элементов обучающей выборки
  • текущие (итоговые) веса нейронов и значение порога активационной функции
  • размер ошибки, при котором обучение нейрона завершается
  • режим обучения должен иметь два варианта работы:
    • пошаговый - на экране должны отображаться все представители (хромосомы) одного поколения до и после применения каждого оператора (скрещивания, селекции, редукции и мутации).
    • циклический - на экране должны отражаться только агрегированные данные по каждому поколению и итоговый набор хромосом.
На экранной форме режима распознавания должны отображаться:
  • распознаваемое изображение (должно выбираться из всего множества)
  • результат распознавания
  • веса нейронов и значение порога активационной функции
  • значения выхода нейрона
В целом, структура работы алгоритма обучения персептрона остается прежней, однако, применение генетических алгоритмов вносит следующие коррективы:
-значение целевой функции вычисляется для текущей популяции, в случае если выполнено одно из двух условий (коэффициенты подобраны с подобающей точностью,- расчет идет как минимальное значение целевой функции для правильной картинки  минус максимальное значение для неправильной (выбирается из всех значений для неправильных), данное значение должно удовлетворять выбранной пользователем дельты; второе условие - достигнут предел итераций);
-в случае если одно из условий выполнилось - выбираем наиболее подходющий набор весов и заканчиваем выполнение алгоритма
-в случае, если ни одно из условий не выполнилось - для набора хромосом, где в качестве гена - значение веса для ij-ого пиксела картинки, выполняются все стадии генетического алгоритма с целью формирования новой, более приспособленной популяции, а именно: селекция, затем применение генетических операторов - скрещивания и мутации - затем идет формирование новой популяции на базе сформированных значений хромосом. В данном случае вероятность мутации пришлось искуственно завысить с целью, чтобы предотвратить слишком быстрое схождение алгоритма.

Код, отвечающий за выполнение вышеописанных функций, можно посмотреть ниже:
private void CarryOutWeightsSelection()
        {
            ObservableCollection<double> tempLocals = new ObservableCollection<double>();
            double localdelta = 0;
            foreach (ObservableCollection<double> weight in weightsSet)
            {
                localdelta = 0;
                double y1res = this.EvaluateThreshHoldFunc(this.etalonPictures.ElementAt(0), weight);
                localdelta += Math.Abs(y1res - 1);

                foreach (ObservableCollection<bool> pic in this.trainingPictures)
                {
                    double y0res = this.EvaluateThreshHoldFunc(pic, weight);
                    localdelta += Math.Abs(y0res);
                }
                tempLocals.Add(localdelta);

                if (this.Delta >= localdelta)
                {

                    m_weights = new ObservableCollection<double>(weight);
                    
                    m_thresholdFunc = this.EvaluateThreshHoldFunc(this.etalonPictures.ElementAt(0), m_weights);

                    int _index = 0;
                    foreach (PixelRow prow in this.CheckImageRows)
                        foreach (Pixel px in prow.Pixels){
                            px.ElementaryWeight = m_weights.ElementAt(_index);
                            _index++;
                }
                    this.OnRaiseUpdateEvent();
                    break;
                }
                

            }

            if (iteration == this.GenerationsLimit)
            {
                m_weights = new ObservableCollection<double>(tempWeightSelected);
                this.CalculationsDone = true;

                m_thresholdFunc = this.EvaluateThreshHoldFunc(this.etalonPictures.ElementAt(0), m_weights);

                int _index = 0;
                foreach (PixelRow prow in this.CheckImageRows)
                    foreach (Pixel px in prow.Pixels)
                    {
                        px.ElementaryWeight = m_weights.ElementAt(_index);
                        _index++;
                    }
                this.OnRaiseUpdateEvent();
                return;
            }

            if (localMinimums.Count == 0 || ((localMinimums.Min() >= tempLocals.Min()) && localMinimums.Count != 0))
            {
                double var = tempLocals.Min();
                tempWeightSelected = weightsSet.ElementAt(tempLocals.IndexOf(var));
                this.localMinimums.Add(var);
                this.OnRaiseUpdateEvent(var, var);// second for minimums changes only
            }
            else
                this.OnRaiseUpdateEvent(tempLocals.Min());
            if (this.Delta > localdelta)
            {
                this.CalculationsDone = true;
                return;
            }
            iteration++;
            this.MakeGeneticStep();
        }

        private void MakeGeneticStep()
        {
            ObservableCollection<ObservableCollection<double>> tempWeightsSet = new ObservableCollection<ObservableCollection<double>>();
            //tournament selection method
            int l_viagraAnalisator = 0;

            m_needPopulationViagra = false;

            for (int indexer = 0; indexer < weightsSet.Count; indexer++)
            {
                ObservableCollection<double> firstParticipant = weightsSet.ElementAt((new Random()).Next(0, weightsSet.Count));
                ObservableCollection<double> secondParticipant = null;
                do// remove this cycle by adding different numbers into constructor
                {
                    ObservableCollection<double> temp = weightsSet.ElementAt((new Random(DateTime.Now.Millisecond)).Next(0, weightsSet.Count));
                    if (firstParticipant != temp)
                    {
                        secondParticipant = temp; break;
                    }
                    if (l_viagraAnalisator == weightsSet.Count * weightsSet.Count)
                    {
                        secondParticipant = temp;
                        m_needPopulationViagra = true;
                        break;
                    }
                    if (l_viagraAnalisator == weightsSet.Count * (weightsSet.Count-1)) Thread.Sleep(5);
                    l_viagraAnalisator++;

                } while (true);


                tempWeightsSet.Add(this.DoSelection(firstParticipant, secondParticipant));
            }

            ObservableCollection<ObservableCollection<double>> onemorecollection = new ObservableCollection<ObservableCollection<double>>(tempWeightsSet);
            if (!m_needPopulationViagra)
            {
                onemorecollection.Clear();
                l_viagraAnalisator = 0;
                for (int i = 0; i < weightsSet.Count / 2; i++)
                {
                    //take 2 random elements
                    int pos1, pos2;
                    pos1 = (new Random()).Next(0, tempWeightsSet.Count);
                    ObservableCollection<double> tempCol1 = tempWeightsSet.ElementAt(pos1);
                    ObservableCollection<double> tempCol2 = new ObservableCollection<double>();

                    do
                    {
                        if (tempWeightsSet.Count == 2)
                        {
                            pos2 = 1 - pos1;
                            tempCol2 = tempWeightsSet.ElementAt(pos2); break;
                        }
                        pos2 = (new Random()).Next(0, tempWeightsSet.Count);
                        ObservableCollection<double> tempo = tempWeightsSet.ElementAt(pos2);
                        if (tempo != tempCol1)
                        {
                            tempCol2 = tempo; break;
                        }
                        if (l_viagraAnalisator == weightsSet.Count * weightsSet.Count)
                        {
                            tempCol2 = tempo; //big probability of algorithm has come to a point
                            m_needPopulationViagra = true;
                            break;
                        }
                        l_viagraAnalisator++;
                    } while (true);
                    int point1 = (new Random(DateTime.Now.Millisecond)).Next(0, 16);

                    int point2 = (new Random(DateTime.Now.Millisecond)).Next(point1, 16);
                    for (int indexer = point1; indexer <= point2; indexer++)
                    {
                        double vv = tempCol1.ElementAt(indexer);
                        tempCol1.Insert(indexer, tempCol2.ElementAt(indexer));
                        tempCol1.RemoveAt(indexer + 1);

                        tempCol2.Insert(indexer, vv);
                        tempCol2.RemoveAt(indexer + 1);
                    }
                    onemorecollection.Add(tempCol1);
                    onemorecollection.Add(tempCol2);
                    if (pos1 < pos2) { tempWeightsSet.RemoveAt(pos2); tempWeightsSet.RemoveAt(pos1); }
                    else if (pos1 == pos2)
                    {
                        tempWeightsSet.RemoveAt(pos1);
                    }
                    else
                    {
                        tempWeightsSet.RemoveAt(pos1);
                        tempWeightsSet.RemoveAt(pos2);
                    }

                }
            }
            //mutation
            ObservableCollection<ObservableCollection<double>> nextcollection = new ObservableCollection<ObservableCollection<double>>();
            double l_mutationProbability = 0;
            if (m_needPopulationViagra) l_mutationProbability = 0.15;
            else l_mutationProbability = 0.5;
            for (int indexer = 0; indexer < onemorecollection.Count; indexer++)
            {
                ObservableCollection<double> nexttemp = onemorecollection.ElementAt(indexer);
                Random p = new Random();
                if (p.NextDouble() > l_mutationProbability)//triple mutation
                {
                    int pos = p.Next(0, 16);
                    nexttemp.Insert(pos, Math.Round((new Random()).NextDouble() * 2 - 1, 3));
                    nexttemp.RemoveAt(pos + 1);
                    Thread.Sleep(TimeSpan.FromTicks(20));
                    //pos = p.Next(pos, 16);
                    //nexttemp.Insert(pos, Math.Round((new Random()).NextDouble() * 2 - 1, 3));
                    //nexttemp.RemoveAt(pos + 1);

                    Thread.Sleep(TimeSpan.FromTicks(20));
                    pos = p.Next(pos, 16);
                    nexttemp.Insert(pos, Math.Round((new Random()).NextDouble() * 2 - 1, 3));
                    nexttemp.RemoveAt(pos + 1);

                    Thread.Sleep(TimeSpan.FromTicks(20));
                }
                nextcollection.Add(nexttemp);
            }

            weightsSet = nextcollection;

        }

        private ObservableCollection<double> DoSelection(ObservableCollection<double> firstParticipant, ObservableCollection<double> secondParticipant)
        {
            double deviation1 = 0, deviation2 = 0;
            foreach (ObservableCollection<bool> pic in this.trainingPictures)
            {
                deviation1 += (double)Math.Abs(this.EvaluateThreshHoldFunc(pic, firstParticipant));
                deviation2 += (double)Math.Abs(this.EvaluateThreshHoldFunc(pic, secondParticipant));
            }
            double temp = this.EvaluateThreshHoldFunc(this.etalonPictures.ElementAt(0), firstParticipant);
            if (temp < 1)
                deviation1 += 1 - temp;

            temp = this.EvaluateThreshHoldFunc(this.etalonPictures.ElementAt(0), secondParticipant);
            if (temp < 1)
                deviation2 += 1 - temp;

            return (deviation1 < deviation2) ? firstParticipant : secondParticipant;
        }

Скриншоты, описывающие за ход выполнения алгоритма:



Архив с решением доступен здесь

В данной работе была использована библиотека dynamic display library для отображения хода выполнения алгоритма в режиме реального времени. Данная библиотека доступна на сайте.

При выполнении ЛР были использованы следующие источники:
-"интеллектуальная" часть:
+Рутковская Д., Пилиньский М., Рутковский Л. - Нейронные сети, генетические алгоритмы и нечеткие системы (2006)
+конспекты лекций
-программная часть:
+msdn.microsoft.com
+stackoverflow.com
+статьи по теме: работа с потоками в C#
Описание разработки:
-язык программирования: C#
-техннология: WPF
-среда разработки: VS 2010
* для корректной работы прораммы на ПК необходимо установить:
-.net frameork v4.0
-entity framework v4.2

Экспертная система "Book advisor"


Лабораторная работа

по курсу 

"Интеллектуальные системы":

Разработка экспертной системы

Выполнил 
студент группы ИУ5-81
Баришок Н.И.


Краткое описание
Разработка программного модуля диалога с пользователем, логика работы которого определяется вне тела программы и может быть изменена пользователем без обновления ее кода.
Задания лабораторной работы:
Выбрать предметную область и формализовать задачу, которая может быть решена с помощью ЭС:
  • Определить перечень объектов принятия решения O.
  • Определить свойства (атрибуты) объектов принятия решения A и их значения.
  • Определить параметры P других объектов, событий, процессов и т.д., которые влияют на выбор конкретных значений свойств объектов O.
Разработать схему разветвленного диалога с пользователем:
  • Придумать множество вопросов Q, позволяющих определить значения параметров P
  • Разработать граф диалога вопросов и ответов
  • Составить правила выбора последующего вопроса на основе ответов предыдущего.
Разработать базу знаний (БЗ) и компьютерную программу (программный модуль), которая опрашивает пользователя на основе спроектированной схемы диалога (Q->P):
  • Создание базы знаний для хранения вариантов ответов, вопросов и правил их задавания.
  • Разработка программы, которая считывает вопросы из БЗ, анализирует ответы и на основе правил БЗ определяет очередность последующих вопросов.
  • Разработать интерфейс с пользователем, который обеспечивает эффективный режим диалога.
Реализовать программные модули, которые осуществляют обработку результатов диалога с пользователем:
  • P -> A: Обработка (анализ) полученных данных P для определения атрибутов A
  • A ->Oi: Принятие решения на основе полученных характеристик A - выбор одного из заранее определенных вариантов решения (объекта O)
  • Вывод результата (рекомендации) пользователю.
В рамках выбранной предметной области ЭС, где уже определены объекты принятия решения (O), их свойства (A), а также параметры других объектов, событий, процессов и т.д (P), спроектировать множество продукционных правил (R).

Доработать базу знаний (БЗ) и компьютерную программу из л/р ES-1, реализовав в ней возможность вывода правил (P->A):
  • Создание в БЗ информационных структур для хранения продукционных правил различного типа (с одной или несколькими посылками).
  • Разработка программы (программного модуля), которая осуществляет обработку (вывод) правил для определения свойств объекта принятия решения (A).
  • Разработка компоненты, которая на основе полученных свойств осуществляет выбор конкретного объекта или совета.
  • Интеграция с программными модулями л/р ES-1.

Разработанная модель базы знаний, ее поля и типы полей, связи между сущностями показаны на рисунке ниже:

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

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

Код, связанный с выбором вопросов, анализом ответов, в целом с работой с БЗ (в основном использованы лямбда-выражения для работы с БЗ):
        private void AskQuestion(QuestionRule qr)
        {
            CurrentQuestion = (from quest in qList
                               where quest.Order == qr.NextQuestionIndex
                               select quest).FirstOrDefault();
        }

        public void PrepareNextQuestion()
        {
            if (!this.CurrentQuestion.IsOpened)
            {
                this.CurrentQuestion.AskedParameter.Value = this.SelectedCAnswer.Value;//closed question, so simply copy selected answer
            }
            else //opened(
            {
                double var;
                if (Double.TryParse((string)this.SelectedOpenedAnswer, out var))
                {
                    //analyse and save answer
                    for (int i = 0; i < CurrentQuestion.AnswerVariants.Count; i++)
                    {
                        AnswerVariant aw = CurrentQuestion.AnswerVariants.ElementAt(i);
                        if (var > (aw as OpenedAnswerVariant).MinValue && var <= (aw as OpenedAnswerVariant).MaxValue)
                        {
                            CurrentQuestion.AskedParameter.Value = (aw as OpenedAnswerVariant).Mapping;
                            break;
                        }
                    }
                    if (CurrentQuestion.AskedParameter.Value == null) throw new Exception("Что-то не так с получением AskedParameter");//change for only proper data might be written
                }
                else
                {
                    throw new Exception("Что-то не так с получением AskedParameter");
                }
            }

            List<AskedParameter> aps = (from qt in CurrentQuestion.Parameter.Questions
                                        select qt.AskedParameter).OrderBy(o => o.Number).ToList<AskedParameter>();//get all asked parmeters asceding

            List<Dependency> deps = (from p in CurrentQuestion.Parameter.Dependencies
                                     select p).ToList();//get all dependencies

            if (CurrentQuestion.IsStartQuestion && CurrentQuestion.IsFinishQuestion) //simple question
            {
                for (int i = 0; i < CurrentQuestion.Parameter.Dependencies.Count; i++)
                {
                    SimpleDependency sdep = CurrentQuestion.Parameter.Dependencies.ElementAt(i) as SimpleDependency;
                    if (CurrentQuestion.AskedParameter.Value == sdep.AskedPropertyValue)
                    {
                        CurrentQuestion.Parameter.Value = sdep.BParameterValue;
                        break;
                    }
                }
            }

            else //complex(
            {
                if (CurrentQuestion.IsFinishQuestion && !CurrentQuestion.IsStartQuestion)
                    if (deps != null)
                        if (deps.Count != 0)
                            this.FillParameter(aps, deps);
            }
            //ask next question using rule

            //select rule
            QuestionRule qr = (from temp in CurrentQuestion.QuestionRules
                               where (temp.AnswerSelected == CurrentQuestion.AskedParameter.Value) || (temp.AnswerSelected == null)
                               select temp).FirstOrDefault();

            //are we finishing?
            if (qr.NextQuestionIndex == -1)
            {
                if (qr.Question.Parameter.Value == null) this.FillParameter(aps, deps);
                this.TestFinished = true;
                return;
            }
            if (qr.Question.Parameter.Value == null)
                if (qr.Question.Parameter != (from q in qList where q.Order == qr.NextQuestionIndex select q).First().Parameter)
                    this.FillParameter(aps, deps);

            this.AskQuestion(qr);

        }


        private void FillParameter(List<AskedParameter> aps, List<Dependency> deps)
        {
            if (deps.ElementAt(0) is TripleDependency)
            {
                //dowork for tripledependency analysis
                IEnumerable<TripleDependency> firstIteration = from temp in deps
                                                               where (((temp as TripleDependency).FPropValue == ((from a in aps where a.Number == (temp as TripleDependency).FPropNumber select a).First()).Value))
                                                               select (TripleDependency)temp;
                if (firstIteration == null) return;
                IEnumerable<TripleDependency> secondIteration = from temp in firstIteration
                                                                where temp.SPropValue == ((from a in aps where a.Number == temp.SPropNumber select a).First()).Value
                                                                select temp;
                if (secondIteration == null) return;
                TripleDependency finalIteration = (from temp in secondIteration
                                                   where temp.ThPropValue == ((from a in aps where a.Number == temp.ThPropNumber select a).First()).Value
                                                   select temp).FirstOrDefault();
                if (finalIteration != null)
                    CurrentQuestion.Parameter.Value = finalIteration.BParameterValue;

            }
            else if (deps.ElementAt(0) is DoubleDependency)
            {
                IEnumerable<DoubleDependency> firstIteration = from temp in deps
                                                               where ((temp as DoubleDependency).FPropValue == ((from a in aps where a.Number == (temp as DoubleDependency).FPropNumber select a).First()).Value)
                                                               select (DoubleDependency)temp;
                if (firstIteration == null) return;

                DoubleDependency finalIteration = (from temp in firstIteration
                                                   where temp.SPropValue == ((from a in aps where a.Number == temp.SPropNumber select a).First()).Value
                                                   select temp).FirstOrDefault();
                if (finalIteration != null)
                    CurrentQuestion.Parameter.Value = finalIteration.BParameterValue;
            }
            else throw new Exception("ЧТО-ТО ПОШЛО НЕ ТАК, ПАРЕНЬ!");
        }
Скриншоты сессии опроса пользователя представлены ниже:




Архив с решением доступен здесь

При выполнении ЛР были использованы следующие источники:
-"интеллектуальная" часть:
+Введение в эксепртные системы; Правила построения экспертных систем.
+конспекты лекций
-программная часть:
+msdn.microsoft.com
+stackoverflow.com
Описание разработки:
-язык программирования: C#
-техннология: WPF + EntityFramework
-среда разработки: VS 2010
* для корректной работы прораммы на ПК необходимо установить:
-.net frameork v4.0
-entity framework v4.2
-Tools for entity designer
-SQL Server compact

Распознавание изображений (персептрон)


Лабораторная работа

по курсу 

"Интеллектуальные системы":

Распознавание изображений с помощью персептрона

Выполнил 
студент группы ИУ5-81
Баришок Н.И.


Краткое описание
Разработка программы, которая обучает искусственную нейронную сеть (персептрон) распознавать два или более черно-белых изображения.

Цель работы
Изучить принципы работы и алгоритм обучения простейших искусственных нейронных сетей (НС).

Требования к исходным данным и функциональности компьютерной программы
  • В программе должна быть реализована возможность задания обучающей выборки из внешних файлов изображений
  • Изображения должны быть черно-белыми (bitmap) и размером не менее 9 (3x3) пикселей.
  • Программа должна иметь два режима работы: обучения и распознавания.
  • Обучение должно производиться по стандартному алгоритму обучения персептрона с использованием дельта-правила.
  • В программе должны задаваться следующие настройки:
    • количество входов нейрона, которое соответствует общему числу пикселей изображения
    • коэффициент скорости обучения (если его значение постоянно)
    • правильные варианты элементов обучающей выборки
    • размер ошибки, при котором обучение персепетрона завершается (опционально)
  • На экранной форме режима обучения должны отображаться:
    • элементы обучающей выборки (изображения)
    • настройки алгоритма обучения
    • текущие (итоговые) веса нейронов и значение порога активационной функции
    • протоколы результатов обучения (значения весов для каждой итерации)
  • На экранной форме режима распознавания должны отображаться:
    • распознаваемое изображение (должно выбираться из всего множества)
    • результат распознавания
    • веса нейронов и значение порога активационной функции
    • значения выходов всех нейронов до и после применения активационной функции
Алгоритмы обучения и распознавания персептрона:

Персептрон - простейшая однонаправленная нейронная сеть.


В качестве функции f применяется биполярная функция активации:
Сигнал Х на выходе линейной части перспетрона задается выражением

здесь w0 = v, u0=-1
Задача персептрона заклчается в классификации вектора u=[u1,...,un]T в смысле отнесения его к классу либо L1 или L2. Персептрон относит вектор к классу L1, если выхдной сигнал равен 1, и l2, если -1. После этого персептрон разделяет N-мерное пространство входных векторов u на 2 полупространства, разделяемые (n-1)-мерной плоскостью, задаваемой уравнением
Обучение персептрона заключается в реккурентной коррекции вектора весов w(n), согласно формулам:
Пошаговое выполнение алгоритма в программе:


Участок кода, ответственный за подбор весов персептрона:
double delta = 0;
            this.InitWeights();
            for (int i = 0; i < etalonPictures.Count; i++)
            {
                ObservableCollection<bool> currentitem = etalonPictures.ElementAt(i);
                double value = 0;
                int j = 0;
                foreach (bool item in currentitem)
                {
                    value += Convert.ToInt32(item) * m_weights[j];
                    j++;
                }

                delta = Math.Round((1 - value) * this.RightTrainingSpeed, 2);
                this.UpdataWeights(currentitem, delta);
            }
            for (int i = 0; i < trainingPictures.Count; i++)
            {
                ObservableCollection<bool> currentitem = trainingPictures.ElementAt(i);
                double value = 0;
                int j = 0;
                foreach (bool item in currentitem)
                {
                    value += Convert.ToInt32(item) * m_weights[j];
                    j++;
                }

                delta = Math.Round((0 - value) * this.WrongTrainingSpeed, 2);
                this.UpdataWeights(currentitem, delta);
            }
            ObservableCollection<bool> item1 = etalonPictures.ElementAt(0);
            double temp = this.EvaluateThreshHoldFunc(item1);
            m_thresholdFunc = Math.Round(temp, 2) - 0.01;

Полный проект решения можно найти здесь

При выполнении ЛР были использованы следующие источники:
-"интеллектуальная" часть:
+Рутковская Д., Пилиньский М., Рутковский Л. - Нейронные сети, генетические алгоритмы и нечеткие системы (2006)
+конспекты лекций
-программная часть:
+msdn.microsoft.com
+stackoverflow.com
+статьи по теме: mvvm-pattern in wpf
Описание разработки:
-язык программирования: C#
-техннология: WPF
-среда разработки: VS 2010
* для корректной работы прораммы на ПК необходимо установить:
-.net frameork v4.0
-entity framework v4.2