Perceptor:WPF 的人工智能引导导航系统






4.95/5 (121投票s)
通过神经网络获得的知识用于预测用户可能想要导航到的元素。
目录
引言
Perceptor是WPF的人工智能引导导航系统。Perceptor跟踪用户与用户界面交互时的行为。宿主控件的DataContext
的更改表明了用户的导航行为,并触发神经网络的训练。神经网络获得的知识用于预测用户可能想要导航到的IInputElements
。这加快了界面交互,提高了用户效率,并允许动态和不断发展的业务规则创建。
背景
去年(2008年),我被要求为一个ASP.NET应用程序实现一些业务规则。该应用程序的一部分设计用于高容量数据录入,并使用了一个标签界面,员工通过该界面导航、手动验证和修改信息。我实现的规则旨在简化这个过程。当时我意识到,基于业务流程硬编码用户界面的行为过于僵化。人们的工作方式会改变,应用程序的使用方式也因用户而异。此外,随着时间的推移,对这类规则的完善会导致维护成本的增加,以及员工需要重新培训以适应新的改进的应用程序行为。
我设想了一个系统,我们可以让用户通过使用来定义行为。一个能够自己学习如何响应的系统。为此,本文和附带的代码作为概念验证提供。
一个由神经网络驱动的界面
尽管我们拥有WPF这样出色的技术来构建动态且高度响应的界面,但大多数界面本身并不智能;它们在响应用户交互时甚至不具备丝毫的人工智能。也许我们可以将智能界面比作飞行汽车;两者在科幻小说中都更容易实现,都是技术进化的下一步,并且都需要大量的完善才能做到正确。
我希望界面能知道我想要什么,并向我学习。但我也希望它能以一种不打扰我的方式做到这一点,不做出糟糕的假设,这可能是最大的挑战之一。如果没油了需要迫降,那我宁愿留在地面上。
我们都见过人工神经网络(ANN)如何被用于面部识别和光学字符识别等任务。事实上,它们在模式识别方面表现良好,其中存在明确定义的训练数据,而且我们似乎能够以不同的方式利用相同技术来识别用户行为。然而,存在一些挑战,例如处理基于时间渐进式训练,因为训练数据不是预先定义的;网络是边运行边训练的。使用ANN的一个优点是,我们能够为尚未遇到的情况提供预测。
Perceptor使用一个三层神经网络,该网络与一个宿主ContainerControl
和一个DataContext
类型相关联。本文我们不探讨神经网络,因为CP上已经有一些非常好的文章。如果您不熟悉神经网络,我推荐您阅读Sacha Barber关于该主题的一系列文章。我需要提到的是,在实验过程中,人们意识到未来可能会增强为长短期记忆(LSTM)的实现。在此原型中,我们反复使用所有输入来重新训练神经网络,以进行渐进式学习。
用WPF构建飞行汽车
Perceptor使用宿主控件的DataContext
的状态作为输入,以及IInputControls
ID的列表作为输出,来训练神经网络。预测数据和序列化的神经网络在离线时保存在本地,在线时保存在服务器上。

图:Perceptor系统概述。
Perceptor监控宿主控件的DataContext
的变化。通过这样做,而不是仅跟踪控件的状态,我们能够收集更多关于用户如何影响系统状态的信息。我们不仅能推断用户行为,还能推断系统行为,因为系统能够响应内部或外部事件来修改DataContext。换句话说,如果我们仅仅跟踪控件,我们将无法关联那些在界面中没有可视化表示的属性。通过跟踪DataContext
,我们可以更深入地分析结构,甚至可以改进我们为神经网络生成输入的方式。我们可以有效地深入DataContext
,以提高Perceptor预测的粒度和质量。
我们神经网络的输入由NeuralInputGenerator
生成。它接收控件的DataContext
属性暴露的对象,并将其转换为double[]
,然后可以用于训练或驱动神经网络。
/// <summary>
/// Generates the input for a neural network.
/// </summary>
/// <param name="instance">The object instance that is analysed
/// in order to produce the result.</param>
/// <param name="newInstance">if <c>true</c>
/// then this is the first time the neural network
/// has been trained in this session.</param>
/// <returns>The input stimulus for a neural network.</returns>
public double[] GenerateInput(object instance, bool newInstance)
{
ArgumentValidator.AssertNotNull(instance, "instance");
var clientType = instance.GetType();
if (lastKnownType == null || lastKnownType != clientType)
{
lock(lastKnownTypeLock)
{
if (lastKnownType == null || lastKnownType != clientType)
{
Initialize(clientType);
}
}
}
var resultSize = propertyCount + 1;
var doubles = new double[resultSize];
/* The first index is reserved as an indicator
* for whether this is a new instance. */
doubles[0] = newInstance ? trueLevel : falseLevel;
for (int i = 1; i < resultSize; i++)
{
var info = propertyInfos.Values[i - 1];
if (info.PropertyType == typeof(string))
{
var propertyValue = info.GetValue(instance, null);
doubles[i] = propertyValue != null ? trueLevel : falseLevel;
}
else if (info.PropertyType == typeof(bool))
{
var propertyValue = info.GetValue(instance, null);
doubles[i] = (bool)propertyValue ? trueLevel : falseLevel;
}
else if (!typeof(ValueType).IsAssignableFrom(info.PropertyType))
{ /* Not a value type. */
var propertyValue = info.GetValue(instance, null);
doubles[i] = propertyValue != null ? trueLevel : falseLevel;
}
}
return doubles;
}
这里我们检查提供的实例的属性,并根据属性是否已填充等规则,填充double[]
。
此方法生成的输入为我们提供了DataContext的指纹,以及界面模型的离散表示。有机会优化NeuralInputGenerator
,以增加其对已知字段类型的识别,甚至添加子对象分析。
持久化
ADO.NET实体框架用于访问与用户和控件ID关联的预测数据表。当Perceptor连接到宿主控件时,它将尝试检索该用户和特定宿主控件ID的现有预测数据。它通过首先检查宿主控件是否已分配Perceptor.PersistenceProvider
附加属性来做到这一点。如果是,Perceptor将尝试使用该提供程序进行持久化。可以通过实现IPersistPredictionData
接口来利用此用于持久化预测数据的可扩展点。
当宿主控件的窗口关闭时,Perceptor将尝试保存其预测数据。在示例应用程序中,我们将预测数据与用户ID关联。以下示例摘录演示了如何做到这一点。
public void SavePredictionData(LearningData predictionData)
{
log.Debug("Attempting to save prediction data." + predictionData);
if (Testing)
{
return;
}
var learningUIService = ChannelManagerSingleton.Instance.GetChannel<ILearningUIService>();
Debug.Assert(learningUIService != null);
learningUIService.SavePredictionData(testUserId, predictionData);
}
示例概述
下载包含一个示例应用程序,旨在演示Perceptor如何用于引导用户输入元素。它显示了一个员工姓名列表,每个员工被选中后会填充应用程序的“员工详细信息”选项卡和“老板”面板。
图:示例应用程序的初始屏幕截图。
每次字段修改导致DataContext
修改时,ANN就会被触发,并获取一个候选输入预测。如果预测的置信度高于预定义阈值,用户将可以选择直接导航到预测的输入控件。
Perceptor的学习过程概述如下所示。
学习阶段
图:学习阶段
一旦Perceptor获得了足够的知识来做出自信的预测,它就可以用于导航到预测的元素。
预测阶段
图:预测阶段
Perceptor
的一个特性是自动展开,当预测的元素恰好位于Expander
中时。一旦检测到自信的预测,就会立即发生此展开。
在示例应用程序中,我们可以看到如何突出显示一个自信预测的元素。
图:Perceptor引导用户到下一个预测的元素。
转移焦点
WPF中的确定性焦点转移可能很棘手。当我们调用UIElement
的Focus()
时,不能保证元素会获得焦点。这就是为什么此方法返回true
表示成功的原因。在Perceptor中,我们使用FocusForcer
类在用户界面中移动焦点。UIElement.Focus()
如果在IsEnabled
、IsVisible
或Focusable
为false
的情况下返回false
,则在焦点转移时返回true
。然而,当在处理当前聚焦元素的PreviewLostKeyboardFocus
等事件的同一线程上执行时,调用将返回false
,因为该元素还未准备好放弃焦点。因此,我们使用FocusForcer
和一个扩展方法来在需要时以异步方式执行焦点更改。以下摘录显示了FocusForcer
如何尝试聚焦指定的元素。
static void FocusControl(UIElement element)
{
ArgumentValidator.AssertNotNull(element, "element");
Keyboard.Focus(element);
var focusResult = element.Focus();
if (focusResult)
{
return;
}
element.Dispatcher.Invoke(DispatcherPriority.Background, (Action)delegate
{
focusResult = element.Focus();
Keyboard.Focus(element);
if (!focusResult)
{
CommitFocusedElement();
focusResult = element.Focus();
Keyboard.Focus(element);
}
if (!focusResult)
{
log.Warn(string.Format("Unable to focus UIElement {0} "
+ "IsVisible: {1}, Focusable: {2}, Enabled: {3}",
element, element.IsVisible, element.Focusable,
element.IsEnabled));
}
});
}
初始化Perceptor时,我们为容器控件的每个IInputElement
在神经网络中创建一个输出神经元。
/// <summary>
/// Initializes Perceptor from a container element.
/// It is the <code>DataContext</code> of this element
/// that is monitored for changes.
/// </summary>
/// <param name="host">The parent element.</param>
void InitializeFromHost(FrameworkElement host)
{
ArgumentValidator.AssertNotNull(host, "host");
this.host = host;
host.DataContextChanged += OnHostDataContextChanged;
host.CommandBindings.Add(new CommandBinding(
NavigateForward, OnNavigateForward, OnCanNavigateForward));
host.CommandBindings.Add(new CommandBinding(
NavigateBackward, OnNavigateBackward, OnCanNavigateBackward));
host.CommandBindings.Add(new CommandBinding(
ResetLearning, OnResetLearning, OnCanResetLearning));
outputNeuronCount = 0;
inputElementIndexes.Clear();
/* Each IInputElement in the user interface
* gets an output neuron in the neural network. */
var inputElements = host.GetChildrenOfType<IInputElement>();
foreach (var inputElement in inputElements)
{
inputElementIndexes.Add(inputElement, outputNeuronCount);
inputElement.PreviewLostKeyboardFocus -= OnInputElementPreviewLostKeyboardFocus;
inputElement.PreviewLostKeyboardFocus += OnInputElementPreviewLostKeyboardFocus;
outputNeuronCount++;
}
var window = host.GetWindow();
if (window != null)
{
/* We shall save the network when the window closes. */
window.Closing += window_Closing;
}
}
使用Perceptor
为了让Perceptor监控任何容器控件,我们使用附加属性,如以下示例所示。
<TabControl Name="tabControl_Main" Grid.Row="2" VerticalAlignment="Stretch" SelectedIndex="0"
LearningUI:Perceptor.Enabled="true"
LearningUI:Perceptor.PersistenceProvider="{Binding ElementName=rootElement}" />
PersistenceProvider
属性并非必需。但它的存在是为了让我们能够自定义用户预测数据如何在会话之间保存。在示例下载中,我们使用窗口将预测数据传输到和从ILearningUIService
WCF服务。由于这是一个混合智能客户端,Perceptor允许用户在服务不可用时离线工作,并且在PersistenceProvider
不可用或引发Exception
时,会回退到将预测数据持久化到用户本地文件系统。以下摘录显示了IPersistPredictionData
接口。
/// <summary>
/// Provides persistence services for Perceptor.
/// </summary>
public interface IPersistPredictionData
{
/// <summary>
/// Saves the prediction data so that it may be loaded
/// via <see cref="LoadPredictionData"/>.
/// </summary>
/// <param name="predictionData">The prediction data.</param>
void SavePredictionData(PerceptorData predictionData);
/// <summary>
/// Loads the prediction data that has been persisted
/// via <see cref="SavePredictionData"/>.
/// </summary>
/// <param name="id">The unique id of the prediction data.</param>
/// <returns>The PerceptorData with the matching id.</returns>
PerceptorData LoadPredictionData(string id);
}
Perceptor公开了三个路由命令,它们是
NavigateForward
用于将焦点切换到下一个预测的UIElement
。NavigateBackward
用于返回到先前具有焦点的UIElement。当执行NavigateForward
时,当前具有焦点的元素会被放入堆栈。ResetLearning
用于重新创建神经网络,以便忘记之前的学习。
服务通道管理
为了高效地管理通道,我实现了一个名为ChannelManagerSingleton
的类。在之前的文章中,我写了一些关于Silverlight版本的内容,所以我这里不再重复。但我会提到,从那时起,我制作了一个WPF版本(包含在下载中),并支持双工服务。双工服务使用回调实例和服务类型的组合作为唯一键进行缓存。这样,我们仍然可以对服务进行集中管理,即使涉及到回调实例。以下摘录显示了GetDuplexChannel
方法以及双工通道的创建和缓存方式。
public TChannel GetDuplexChannel<TChannel>(object callbackInstance)
{
if (callbackInstance == null)
{
throw new ArgumentNullException("callbackInstance");
}
Type serviceType = typeof(TChannel);
object service;
var key = new DuplexChannelKey { ServiceType = serviceType, CallBackInstance = callbackInstance };
duplexChannelsLock.EnterUpgradeableReadLock();
try
{
if (!duplexChannels.TryGetValue(key, out service))
{ /* Value not in cache, therefore we create it. */
duplexChannelsLock.EnterWriteLock();
try
{
var context = new InstanceContext(callbackInstance);
/* We don't cache the factory as it contains a list of channels
* that aren't removed if a fault occurs. */
var channelFactory = new DuplexChannelFactory<TChannel>(context, "*");
service = channelFactory.CreateChannel();
var communicationObject = (ICommunicationObject)service;
communicationObject.Faulted += OnDuplexChannelFaulted;
duplexChannels.Add(key, service);
communicationObject.Open();
ConnectIfClientService(service, serviceType);
}
finally
{
duplexChannelsLock.ExitWriteLock();
}
}
}
finally
{
duplexChannelsLock.ExitUpgradeableReadLock();
}
return (TChannel)service;
}
使用White进行WPF单元测试
黑盒测试可以补充您现有的单元测试。我比较喜欢黑盒测试的一个优点是,我们是在一个真实的运行环境中测试功能,并且依赖关系也会被测试。另一个优点是测试与实现无关。例如,在Perceptor的开发过程中,我更改了许多实现,但我的黑盒测试仍然保持不变。过去我曾使用NUnitForms进行黑盒测试。这是我第一次涉足WPF的黑盒测试,我需要找到另一个工具,因为NUnitForms不支持WPF。所以我决定尝试White项目。White使用UIAutomation,因此可以同时用于Windows Forms和WPF应用程序。
开始使用White只需引用White程序集并在单元测试中启动应用程序实例,如下面的摘录所示。
[TestInitialize]
public void TestInitialize()
{
var startInfo = new ProcessStartInfo("DanielVaughan.LearningUI.Wpf.exe",
DanielVaughan.LearningUI.App.TestingArg);
application = Core.Application.Launch(startInfo);
window = application.GetWindow(DanielVaughan.LearningUI.Window_Main.WindowTitle,
InitializeOption.NoCache);
}
为了让Perceptor在测试期间不尝试使用WCF,我们使用了一个参数来告知它正在进行黑盒测试。一旦我们启动应用程序,我们就使用White来获取应用程序的可测试表示。
测试方法使用window
实例来定位和操作UIElements
。除其他功能外,我们还可以设置文本框值、点击按钮和切换选项卡。似乎有些元素尚不支持,例如Expander
控件。我使用的是一个相当旧的发布版本,其他人最好通过svn客户端获取并构建源代码。
[TestMethod]
public void WindowShouldLearnFromNavigation()
{
Assert.IsNotNull(window);
textBox_ApplicationSearch = window.Get<TextBox>("textBox_Search");
Assert.IsNotNull(textBox_ApplicationSearch);
var resetButton = window.Get<Button>("Button_ResetLearning");
Assert.IsNotNull(resetButton);
var tabPageSelection = window.Get<TabPage>("TabItem_SelectEmployee");
Assert.IsNotNull(tabPageSelection);
...
var forwardButton = window.Get<Button>("Button_Forward");
Assert.IsNotNull(forwardButton);
forwardButton.Click();
Thread.Sleep(pausePeriodMs);
Assert.IsTrue(tabItemDetails.IsSelected);
Assert.IsTrue(phoneTextBox.IsFocussed, "phoneTextBox should be focused.");
}
黑盒测试的另一个好处是我们不必担心创建模拟对象。当然,与传统的白盒测试相比,黑盒测试也有其缺点。但我们没有理由不能同时使用两者!
图:单元测试的测试结果。
可能的应用
Perceptor的一个版本可以在Visual Studio中使用,当选择一个特定的设计器以及特定的状态时,它可以显示相应的工具窗口。Perceptor在手机界面等领域可能特别有用,因为用户与界面的交互能力受到有限物理输入控件的限制。同样,残疾人(其操作用户界面的能力有限)也可能受益。
也许这种预测性UI技术可以归类为第五代用户界面技术(5GUI)。这个建议基于编程语言分类方式,特别是5GL的定义。以下摘录自维基百科的条目。
而第四代编程语言是为构建特定程序而设计的,第五代语言则是为了让计算机在没有程序员的情况下解决给定问题。这样,程序员只需要担心需要解决什么问题和需要满足什么条件,而不用担心如何实现一个例程或算法来解决它们。
随着时间的推移,Perceptor学会了用户界面应该如何工作,从而消除了程序员干预的需要。因此,将其归类为5GUI。
结论
在本文中,我们已经看到Perceptor如何跟踪用户与用户界面交互时的行为,并触发神经网络的训练。我们还看到了Perceptor如何能够将其预测数据本地或远程保存。神经网络获得的知识用于预测用户的导航行为。这使得界面可以动态演进,而不受僵化、预定义的业务规则的束缚。
通过将AI应用于用户界面,我们拥有巨大的机会来提高我们软件的可用性。可以减轻将行为直接硬编码到用户界面中的负担,并且规则可以动态演进和随时间完善。通过结合WPF等技术所提供的视觉吸引力和丰富性,我们可以超越仅仅被动的UI,提供更高级别的用户体验。
我希望您觉得这个项目有用。如果有用,我将不胜感激您能对其进行评分和/或在下方留下反馈。这将帮助我写出更好的下一篇文章。
未来的增强
- 修改神经网络以使用长短期记忆(LSTM)或替代的渐进式循环学习策略。
历史
2009年3月
- 初始发布。